본문 바로가기

Java/Java IO+NIO

Java TCP 소켓 프로그래밍 예제 - 채팅프로그램 만들기 (멀티 쓰레드)

반응형

TCP 소켓 프로그래밍 튜토리얼, 채팅 프로그램

TCP/IP를 이용한 소켓 프로그래밍으로 채팅 예제를 만들며 이해해본다.

코드를 살펴보며 정리한다.

* 채팅 서버 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package nio;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
 
public class ChatServer {
    public static void main(String[] args) {
        try {
            ServerSocket server = new ServerSocket(10001);
            HashMap<String, Object> hm = new HashMap<String, Object>();
            while(true) {
                System.out.println("접속을 기다립니다.");
                Socket sock = server.accept();
                ChatThread chatThread = new ChatThread(sock, hm);
                chatThread.start();
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
 
class ChatThread extends Thread{
    private Socket sock;
    private String id;
    private BufferedReader br;
    private HashMap<String, Object> hm;
    private boolean initFlag = false;
    public ChatThread(Socket sock, HashMap<String,Object> hm) {
        this.sock = sock;
        this.hm = hm;
        try {
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
            br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
            id = br.readLine();
            broadcast(id + "님이 접속하셨습니다.");
            System.out.println("접속한 사용자의 아이디 : "+id);
            synchronized (hm) {
                hm.put(this.id, pw);
            }
            initFlag = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void run() {
        try {
            String line = null;
            while((line = br.readLine()) != null) {
                if(line.equals("/quit")) {
                    break;
                }
                if(line.indexOf("/to"== 0) {
                    sendmsg(line);
                }else {
                    broadcast(id+" : "+line);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            synchronized (hm) {
                hm.remove(id);
            }
            broadcast(id+"님이 접속을 종료했습니다.");
            try {
                if(sock != null) {
                    sock.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
    }
    public void sendmsg(String msg) {
        int start = msg.indexOf(" "+ 1;
        int end = msg.indexOf(" ",start);
        if(end != -1) {
            String to = msg.substring(start, end);
            String msg2 = msg.substring(end +1);
            Object obj = hm.get(to);
            if(obj != null) {
                PrintWriter pw = (PrintWriter)obj;
                pw.println(id + "님이 다음의 귓속말을 보내셨습니다. : " + msg2);
                pw.flush();
            }
        }
    }
    public void broadcast(String msg) {
        synchronized (hm) {
            Collection<Object> collection = hm.values();
            Iterator<?> iter = collection.iterator();
            while(iter.hasNext()) {
                PrintWriter pw = (PrintWriter)iter.next();
                pw.println(msg);
                pw.flush();
            }
        }
    }
}
 
cs

채팅 서버에서는 먼저 ServerSocket을 생성해서 해당 포트로 Connection 신호가 오기를 기다린다. (.accept())

.accept() 메소드에서 반환하는 클라이언트 소켓을 통해 바로 Read, Write 하지않고 멀티 유저를 수용하기 위해 유저마다 쓰레드를 생성해준다.

각 쓰레드에서는 쓰레드가 생성될 때 소켓 통신을 할 PrintWriter와 BufferedReader를 만들고 접속한 유저의 아이디와 출력스트림(PrintWriter)을 HashMap에 key, value로 넣는다.

그리고 run()에서 쓰레드가 실행되면서 메시지가 들어오면 다른 유저 모두에게 broadcast로 메시지를 보낸다.

(귓속말은 생략)

간단하게 만든 예제지만 굳이 분석해보면 일단 멀티 쓰레드를 이용해서 다중 접속을 허용하게 했다.

다만 쓰레드를 만드는 것이 자원소모가 많기 때문에 무제한으로 접속하게 해서 안된다.

보통 쓰레드 개수를 제한해놓는 코드를 쓴다. (MAX = 100 이런식으로)

또한 소켓 클라이언트가 접속을 했을 때는 상관없는데 서버입장에서는 소켓 클라이언트가 연결이 여전히 잘되어 있는지 확인하는 코드가 없다.

예를들어 소켓 클라이언트가 정상종료했을 때는 해당 소켓을 닫아버리지만 정전같이 갑작스런 종료에는 대비하지 않았다. (정전이 되면 클라이언트에서 소켓을 닫지 않고 서버에 소켓을 닫았다고 알리지 않기 때문)

* 멀티 쓰레드를 사용할 때 동기화 처리를 비롯해 주의해야 할 것이 많다.


* 채팅 클라이언트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package nio;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
 
public class ChatClient {
    public static void main(String[] args) {
        if(args.length != 2) {
            System.out.println("사용법 : java ChatClient id 접속할 서버 ip");
            System.exit(1);
        }
        Socket sock = null;
        BufferedReader br = null;
        PrintWriter pw = null;
        boolean endflag = false;
        try {
            sock = new Socket(args[1], 10001);//아아디,포트
            pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
            br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
            BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
            
            pw.println(args[0]);
            pw.flush();
            InputThread it = new InputThread(sock,br);
            it.start();
            String line = null;
            while((line = keyboard.readLine()) != null) {
                pw.println(line);
                pw.flush();
                if(line.equals("/quit")) {
                    endflag = true;
                    break;
                }
            }
            System.out.println("클라이언트 접속 종료");
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(pw != null) {
                    pw.close();
                }
                if(br != null) {
                    br.close();
                }
                if(sock != null) {
                    sock.close();
                }
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
 
class InputThread extends Thread{
    private Socket sock = null;
    private BufferedReader br = null;
    public InputThread(Socket sock,BufferedReader br) {
        this.sock = sock;
        this.br = br;
    }
    public void run() {
        try {
            String line = null;
            while((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(br != null) {
                    br.close();
                }
                if(sock != null) {
                    sock.close();
                }
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
cs

채팅 클라이언트는 먼저 해당 서버에 아이디와 포트번호로 연결을 시도한다.

그 후 서버와 마찬가지로 소켓을 통해 입출력 스트림을 생성한다. (PrintWriter, BufferedReader)

하지만 클라이언트에서 하나 더 생각해야 할 것은 바로 키보드로 받는 입력이다.

만약 키보드로 입력받는 부분과 서버와 통신하는 부분을 같은 쓰레드에서 사용했으면 키보드로 입력을 하는 동안에는 서버에서 오는 메시지들을 받을 수 없게 된다.

따라서 서버에서 들어오는 메시지를 받는 부분을 쓰레드로 생성해서 오는 즉시 받는다.

* 참고로 이것은 채팅방이 없는 예제다. 즉 방이 하나다. 앞으로는 여러개의 방을 만드는 예제를 만들어봤으면 좋겠다.

(또한 한글 인코딩 처리를 하지 않으므로 한글로 메시지를 보내면 깨진다.)


위 소스로 테스트할 때 command line에서 실행을 할 텐데 default 패키지가 아니면 에러가 난다.


 - 해결 방법

예를들어 ~~\eclipse-workspace\nio\bin 디렉토리 밑에 ChatClient.class 파일이 있다면

"java nio.ChatClient aaa localhost" 이런식으로 실행해야한다. (bin디렉토리에서 실행)

여기서 nio는 패키지 이름이고 만약 패키지 이름이 com.tistory.jeongpro 라면

"java com.tistory.jeongpro.ChatClient bbb localhost" 이렇게 입력해야한다.


참고

자바 I/O & NIO 네트워크 프로그래밍, 한빛미디어

반응형
  • 간혹보면 C# 클라이언트 같은 경우는 소켓 라이브러리에서 몇 초 이상 입력이 없으면 스스로 소켓을 닫아버리는 좋은 기능? 코드?가 있습니다.
    반면에 서버에서는 그런 기능이 없기 때문에 어떤 방식으로든 클라이언트 소켓이 온전하게 존재하는지 확인을 해야합니다.
    그렇게 끊어진 연결을 제거해야만 자원관리가 잘 됩니다.