ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JAVA] 채팅 만들기 완전 분석 ( 에코 서버 - 클라이언트 )
    IT, 프로그래밍/Java 2019. 1. 9. 09:40






    보통 자바를 배우는 초심자에 있어서 과제의 끝판왕(?)이라고 불리는 채팅 프로그램에 대해서 알아보겠습니다.

    채팅 프로그램을 만들기 위해서는 아래와 같은 배경지식이 필요합니다.


    • 네트워크 기초 (TCP/IP)
    • JAVA I/O (Stream)
    • Thread (Multi-Chatting 구현시 사용)

    채팅의 원리



    서버-클라이언트 : 서버와 클라이언트 모두 네트워크에 연결되어 IP를 할당 받고 있는 디바이스 입니다. 서버는 클라이언트가 필요한 자원을 가지고 있습니다. 클라이언트는 서버에 특정 자원에 대한 요청을 하면 서버는 그 자원을 찾아서 응답을 해 줍니다.

    지금 이 글을 보고 있는 것도 티스토리에 있는 서버에 브라우저를 통해 이 글에 대한 자원을 요청했고, 서버가 그에 응답으로 자원을 주었기 때문에 볼 수 있는것이 가능한것이죠.


    TCP/IP (Transmission Control Protocol / Internet Protocol,  전송 제어 프로토콜 / 인터넷 프로토콜) : 다른 컴퓨터와 통신을 하기 위한 통신 규약 중 하나로 OSI 계층 중에 전송 계층에서 사용됩니다. 인터넷에서 서로 연결된 컴퓨터 프로그램 간에 데이터를 안정적이고 순서대로 교환할 수 있게 해주는 신뢰성이 높은 방식입니다. 전송제어와 흐름제어 방식 등을 사용합니다.

    >> 깊은 이해가 필요하다면 여기로 <<


    Socket : 컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점입니다. 쉽게 생각하면 톨게이트로 생각하면 됩니다. 클라이언트에서 출발한 패킷이 회선을 거쳐서 서버 컴퓨터의 맨 마지막 하단부인 소켓에 도달해서 데이터를 교환합니다. 소켓은 인터넷 프로토콜, 원격지와 로컬의 IP 주소와 포트에 관한 정보 등을 담고 있습니다.


    소켓 통신 절차




    먼저 서버에서 네트워크 안의 다른 컴퓨터에게 오는 요청을 감지할 수 있는 소켓을 열어둡니다.

    클라이언트에서는 소켓을 생성하여 서버쪽의 IP Address와 Port 번호를 가지고 connection 요청을 합니다.

    그러면 서버측 소켓에서 이 요청을 감지하게 되고, 이후 Handshake 과정을 거쳐서 accept 합니다.

    이 HandShake 과정은 3-way와 4-way가 있는데 여기서는 자세히 다루지 않겠습니다.


    (자료출처 : 영문 위키)


    클라이언트에서 서버로 동기 부호인 SYN을 보내면 서버에서 긍정 부호인 ACK와 SYN을 응답합니다. 다시 클라이언트에서 ACK를 전송하여 서버가 받아들이면 accept 상태가 됩니다.

    잘 감이 안오시면 전화할 때 먼저 여보세요? 거기 OO씨 댁 맞으신가요? 하고 먼저 물어보는 걸 생각해 보세요. 만약 상대방이 OO라는 이름의 사람과 동일한 사람이면 내가 필요한 이야기를 하겠죠? 


    accept된 이후에는 소켓을 close 할 때 까지 서로 데이터를 보내고 받을 수 있습니다. 자바의 경우 소켓 클래스 안에 있는 InputStream과 OutputStream을 지원하여 데이터를 읽고 쓸 수 있게 해 줍니다.

    단, TCP 방식의 경우 메시지를 보내면 (write) 응답을 받을 때 까지(read) 멈춰있는 상태가 됩니다.

    여담이지만 우리의 일상인 월드 와이드 웹으로 이루어진 사이트의 기반인 HTTP의 경우에도 이 TCP 방식을 사용합니다.


    사용되는 클래스 설명



     InetAddress

     IP(Internet Protocol)에 대한 정보를 가지고 있는 클래스. 즉 Host와 Address 등에 관한 정보를 담고 있   다. 이 클래스는 static 클래스로 동적으로 생성하지 않는다.

     Socket

     Client Socket을 구현하는 클래스. 두 기기 사이에 종단점에 위치하여 데이터를 교환한다.

     Server Socket

     Client에서 들어오는 요청을 기다리는 Server Socket을 구현하는 클래스. 

     InputStreamReader

     byte 단위로 들어오는 InputStream에 대하여 char 단위로 읽고쓰는 Reader 인터페이스를 제공해 주는   보조스트림

     BufferedReader

     Buffer를 통해 char 단위로 읽고 쓸 수 있게 해주는 보조 스트림

     OutputStreamWriter

     byte 단위로 쓰는 OutputStream에 대해 특정 인코딩의 char 단위로 읽고 쓰게 해주는 보조 스트림

     PrintWriter

     text로 대표되는 객체들을 출력할 수 있게 해주는 보조 스트림


    간단한 설명을 드리자면, 주 스트림은 데이터를 가져오는 원천 (File이나 Network, Keyboard 등등..)에 직접 연결되는 스트림이고 보조 스트림은 이 주 스트림에 연결하여 필요 목적에 따라 데이터를 가공할 수 있는 클래스 입니다. 주 스트림과 보조 스트림은 데코레이터 패턴을 사용하여 결합합니다.



    Server 측 코드 작성



    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
     
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.OutputStreamWriter;
    import java.io.PrintWriter;
    import java.net.InetAddress;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.Scanner;
     
    public class TCPServer {
     
        public static final int PORT = 6077;
     
        public static void main(String[] args) {
     
            ServerSocket serverSocket = null;
     
            InputStream is = null;
            InputStreamReader isr = null;
            BufferedReader br = null;
     
            OutputStream os = null;
            OutputStreamWriter osw = null;
            PrintWriter pw = null;
            Scanner sc = new Scanner(System.in);
     
            try {
                // 1. Server Socket 생성
                serverSocket = new ServerSocket();
     
                // 2. Binding : Socket에 SocketAddress(IpAddress + Port) 바인딩 함
     
                InetAddress inetAddress = InetAddress.getLocalHost();
                String localhost = inetAddress.getHostAddress();
     
                serverSocket.bind(new InetSocketAddress(localhost, PORT));
     
                System.out.println("[server] binding " + localhost);
     
                // 3. accept(클라이언트로 부터 연결요청을 기다림)
     
                Socket socket = serverSocket.accept();
                InetSocketAddress socketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
     
                System.out.println("[server] connected by client");
                System.out.println("[server] Connect with " + socketAddress.getHostString() + " " + socket.getPort());
     
                while (true) {
     
                    // inputStream 가져와서 (주 스트림) StreamReader와 BufferedReader로 감싸준다 (보조 스트림)
                    is = socket.getInputStream();
                    isr = new InputStreamReader(is, "UTF-8");
                    br = new BufferedReader(isr);
     
                    // outputStream 가져와서 (주 스트림) StreamWriter와 PrintWriter로 감싸준다 (보조 스트림)
                    os = socket.getOutputStream();
                    osw = new OutputStreamWriter(os, "UTF-8");
                    pw = new PrintWriter(osw, true);
     
                    String buffer = null;
                    buffer = br.readLine(); // Blocking
                    if (buffer == null) {
     
                        // 정상종료 : remote socket close()
                        // 메소드를 통해서 정상적으로 소켓을 닫은 경우
                        System.out.println("[server] closed by client");
                        break;
     
                    }
     
                    System.out.println("[server] recived : " + buffer);
                    pw.println(buffer);
     
                }
     
                // 3.accept(클라이언트로 부터 연결요청을 기다림)
                // .. blocking 되면서 기다리는중, connect가 들어오면 block이 풀림
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
     
                try {
     
                    if (serverSocket != null && !serverSocket.isClosed())
                        serverSocket.close();
     
                } catch (Exception e) {
                    e.printStackTrace();
                }
     
                sc.close();
     
            }
     
        }
     
    }
    cs


    1. 먼저 서버측 소켓을 만들어 제공할 ip 주소와 포트를 명시한 후에 bind 메소드를 호출합니다. (Line 34~43)

    2. bind 되었으면 클라이언트의 요청이 있을 시에 accept 메소드를 호출하게 되고, 클라이언트과 연결되는 소켓을 하나 만듭니다.  (Line 47~51)

    3. 연결 이후에 소켓에서 InputStream과 OutputStream을 가져와서 IO를 수행할 준비를 합니다. 보조스트림을 연결합니다. (Line 55~64) 

    4. 클라이언트로 부터 온 문자열을 읽습니다. 만약 넘어오는 문자열이 null이면 클라이언트가 소켓을 닫았다는 뜻이므로 while 루프를 탈출하여 서버를 종료합니다. (Line 65~76)

    5. 받은 문자열을 그대로 클라이언트에게 전송합니다.


    ++ 여기서 유의해야 할 부분은 PrintWriter를 생성할 때 autoFlush 옵션을 true로 주었을 때 println 메소드를 사용해야 autoFlush가 작동된다는 것입니다. 만약 다른 메소드를 사용할 시에 버퍼를 비워주는 메소드를 명시해 주어야합니다.


    ++ while문으로 무한루프를 돌리는 이유는 한 번의 요청과 응답이 끝나면 소켓이 종료되기 때문입니다. 요청과 응답을 계속 이어지기 위해 무한루프로 클라이언트가 소켓을 닫을 때 까지 계속 연결을 유지시킵니다.


    Client 측 코드 작성



    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
     
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.OutputStreamWriter;
    import java.io.PrintWriter;
    import java.net.InetAddress;
    import java.net.InetSocketAddress;
    import java.net.Socket;
    import java.util.Scanner;
     
    public class TCPClient {
     
        public static void main(String[] args) {
     
            // 클라이언트 소켓 생성
     
            Socket socket = new Socket();
            Scanner sc = new Scanner(System.in);
     
            InputStream is = null;
            InputStreamReader isr = null;
            BufferedReader br = null;
     
            OutputStream os = null;
            OutputStreamWriter osw = null;
            PrintWriter pw = null;
     
            // new InetSocketAddress(InetAddress.getLocalHost() 6077
     
            try {
                socket.connect(new InetSocketAddress(InetAddress.getLocalHost(), 6077));
                System.out.println("[client] connected with server");
     
                while (true) {
     
                    is = socket.getInputStream();
                    isr = new InputStreamReader(is, "UTF-8");
                    br = new BufferedReader(isr);
     
                    os = socket.getOutputStream();
                    osw = new OutputStreamWriter(os, "UTF-8");
                    pw = new PrintWriter(osw, true);
     
                    // 읽는거
                    System.out.print(">>");
                    String data = sc.nextLine();
     
                    if ("exit".equals(data))
                        break;
     
                    pw.println(data);
     
                    data = br.readLine();
                    System.out.println("<< " + data);
     
                }
     
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } finally {
                try {
                    if (socket != null && !socket.isClosed()) {
                        socket.close();
                    }
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
     
                sc.close();
     
            }
     
        }
     
    }
    cs


    서버 측 코드와 유사하며 Socket Connect 이후 readLine과 println 메소드를 통해 서버로 부터 데이터를 주고 받습니다.

    여기서 서버로 들어오는 클라이언트 측 메시지를 어떻게 broadcast 해 주냐에 따라서 1:1 채팅을 혹은 1:n 채팅을 구현할 수도 있습니다.

    더 궁금하신거 있으시면 댓글로 남겨주세요~~!

Designed by Tistory.