티스토리 뷰

반응형

TCP 소켓 프로그래밍 02 - 채팅


앞선 소켓 프로그래밍 포스팅에서는 Server와 Client 사이에 1:1 통신 구현에 대해서 공부 하였습니다. 하지만 이번 포스팅에서 다룰 내용은 서버가 여러명의 Client의 접속을 받고, 접속한 Client끼리 메세지를 주고 받을 수 있는 1간단한 채팅 프로그램에 대해 구현을 해볼 생각입니다. 간단한 TCP 소켓 프로그래밍에 대한 공부는 아래 포스팅을 참조해주세요.

 

http://lktprogrammer.tistory.com/62

 

■ 서버 구현 방식

▶기본적으로 앞에서 1:1 통신 방식에서는 메인 쓰레드 영역에서 Client의 접속을 받고 동시에 데이터를 주고 받았습니다. 일 대 다 통신으로 구현되어야 하는 채팅 프로그램에서 마찬가지 방식으로 구현 될 경우 문제점이 첫 번째 Client의 접속을 받고 난 이후 다음으로 들어오는 Client에 대한 접속을 받지 못합니다.

 

이를 해결하기 위해서는 서버의 메인 쓰레드는 Client의 접속을 받고 접속 한 Client와 데이터를 주고 받는 서브 쓰레드를 생성하여 주는 행위를 반복하게 됩니다. 

▶위 그림처럼 Server는 Client의 접속과 Receiver를 생성해주는 일만 반복하게 됩니다. 그런데 여기서 문제는 Client1과 Client2가 통신을 어떻게 하느냐는 것입니다. 즉 서버는 채팅방에 참여한 Client들의 정보를 가지고 있어야 하고 이 정보를 통하여 특정 Client에서 보내는 메세지를 수신받아 채팅방에 참여하고 있는 모든 Client에게 전송을 해 줄수 있습니다.

 

 

 

아래 예제에서는 User라는 채팅방에 있는 Client의 정보를 관리해주는 객체를 구현하였고 User는 한 채팅방에 존재하는 Client의 이름과 DataOutputStream객체를 쌍으로 하는 HaspMap을 가지고 있습니다. 그리고 Client 접속이 이루어지면 Haspmap의 Client의 정보를 저장하는 메서드와 채팅방을 나갔을 때 제거를 해주는 메서드를 각각 구현을 하게 됩니다.  지금까지 서버의 동작방식은 아래 그램과 같습니다.

▶다음은 Client3는 세 번째로 입장한 Client로 가정을 하고 Client3가 채팅방에서 메세지를 전송하였다고 가정을 해보겠습니다.

▶Client3가 보낸 메세지에 대하여 Receiver는 User의 sendMsg() 메서드를 호출합니다. sendMsg()는 HaspMap에 저장 되어 있는 모든 Client에게 Client3가 전송한 메세지를 전송합니다.

■ 클라이언트 구현 방식

▶Client는 서버에 비해서는 비교적 간단합니다. 하지만 서버와 마찬가지로 클라이언트도 두 가지 작업을 동시에 반복하기 때문에 쓰레드가 필요합니다. Client의 메인 쓰레드는 서버로부터 넘어오는 데이터를 수신하여 화면에 표시해주는 기능을 수행하고 서브 쓰레드인 Send1는 Client의 키보드로부터 데이터를 읽어들여서 서버로 송신하는 기능을 수행하게 됩니다.

 

■ 서버 구현

Server.java

public class Server {
    public static void main(String arg[])
    {
        //접속한 Client와 통신하기 위한 Socket
        Socket socket = null;    
        //채팅방에 접속해 있는 Client 관리 객체                
        User user = new User();        
        //Client 접속을 받기 위한 ServerSocket            
        ServerSocket server_socket=null;              
        
        int count = 0;                            
        Thread thread[]= new Thread[10];             
        
        try {
            server_socket = new ServerSocket(서버포트);
            //Server의 메인쓰레드는 게속해서 사용자의 접속을 받음
            while(true)
            {
                socket = server_socket.accept();

                thread[count] = new Thread(new Receiver(user,socket));
                thread[count].start();
                count++;
            }
        }catch(Exception e) {};
    }
}

Main을 포함하는 Server.java 입니다. 14~21라인에서 무한 반복문을 돌면서 Client의 접속을 받고 Client 접속이 이루어지면 Receiver를 생성합니다. Receiver를 생성하면서 생성자 인자로 user 객체와 접속한 Client의 소켓을 넘겨주고 있습니다.

 

Receiver.java

 

▶생성자에서 Client로부터 닉네임 정보를 읽어들이고 AddClient 메서드를 호출합니다. 그리고 Receiver Thread는 Client로부터 메세지를 읽어들이고 읽어들인 메세지를 SendMsg 메서드를 호출함으로써 다른 Client에게 전달합니다. 그리고 Catch문이 실행이 되었다는 것은 Client가 접속을 끊었다는 것을 의미하므로 User객체에서 해당 Client의 정보를 제거해줍니다.

 

User.java

public class Receiver implements Runnable{

    Socket socket;
    DataInputStream in;
    String name;
    User user = new User();

    public Receiver(User user,Socket socket) throws Exception
    {
        this.user = user;
        this.socket = socket;
        //접속한 Client로부터 데이터를 읽어들이기 위한 DataInputStream 생성
        in = new DataInputStream(socket.getInputStream());
        //최초 사용자로부터 닉네임을 읽어들임
        this.name = in.readUTF();
        //사용자 추가해줍니다.
        user.AddClient(name, socket);
    }

    public void run()
    {
        try
        {
            
            while(true)
            {
                String msg = in.readUTF();
                user.sendMsg(msg , name);
            }
        }catch(Exception e) {
            
            user.RemoveClient(this.name);
        }        
    }
}
public class User {
    
    HashMap<String,DataOutputStream> clientmap 
                                             = new HashMap<String,DataOutputStream>(); 

    public synchronized void AddClient(String name,Socket socket) 
    {                                                                        
        try {
            sendMsg(name+" 입장하셨습니다.","Server");
            clientmap.put(name, new DataOutputStream(socket.getOutputStream()));
            System.out.println("채팅 참여 인원 : "+clientmap.size());
        }catch(Exception e){}

    }
    public synchronized void RemoveClient(String name)  
    {
        try {
            clientmap.remove(name);
            sendMsg(name + " 퇴장하셨습니다.", "Server");
            System.out.println("채팅 참여 인원 : "+clientmap.size());
        }catch(Exception e) {}
    }

    public synchronized void sendMsg(String msg, String name)throws Exception 
    {
        Iterator iterator = clientmap.keySet().iterator();
        while(iterator.hasNext())
        {
            String clientname =(String)iterator.next();
            clientmap.get(clientname).writeUTF(name + ":" + msg);
        }
    }
}

 

▶마지막 User.java입니다.

 

 

■클라이언트 구현


Client.java

public class Client {
    public static void main(String[] arg)
    {
        Socket socket = null;            //Server와 통신하기 위한 Socket
        DataInputStream in = null;        //Server로부터 데이터를 읽어들이기 위한 입력스트림
        BufferedReader in2 = null;        //키보드로부터 읽어들이기 위한 입력스트림

        DataOutputStream out = null;    
        
        try {

            socket = new Socket("서버주소",서버 포트);    //서버로 접속
            
            in = new DataInputStream(socket.getInputStream());            
            in2 = new BufferedReader(new InputStreamReader(System.in)); 
            out = new DataOutputStream(socket.getOutputStream());        

            //채팅에 사용 할 닉네임을 입력받음
            System.out.print("닉네임을 입력해주세요 : ");
            String data = in2.readLine();            
            
            //서버로 닉네임을 전송
            out.writeUTF(data);               
            //사용자가 채팅 내용을 입력 및 서버로 전송하기 위한 쓰레드 생성 및 시작
            Thread th = new Thread(new Send(out));
            th.start();
        }catch(IOException e) {}
        try {
            //클라이언트의 메인 쓰레드는 서버로부터 데이터 읽어들이는 것만 반복.
            while(true)
            {
                String str2 = in.readUTF();        
                System.out.println(str2);
            }
        }catch(IOException e) {}
    }
}

서버에 접속이 이루어지고 나면 19라인에서 닉네임을 입력받고 22라인에서 서버로 닉네임을 전송합니다. 그 후에 Send Thread를 실행하고 난 뒤에는 서버로부터 넘어오는 데이터를 읽어와서 화면에 출력해주는 기능을 반복적으로 수행하게 됩니다.

 

Send.java

public class Send implements Runnable{
    DataOutputStream out;
    BufferedReader in2 = new BufferedReader(new InputStreamReader(System.in));
    public Send(DataOutputStream out)
    {
        this.out = out;
    }
    public void run()
    {
        while(true)
        {
            
            try
            {
                String msg = in2.readLine();    //키보드로부터 입력을 받음
                out.writeUTF(msg);                //서버로 전송
            }catch(Exception e) {}
        }
    }
}

▶Send쓰레드는 사용자가 키보드로 입력한 내용을 서버로 전송하는 기능을 반복적으로 수행합니다.

반응형