ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] NIO 기반 입출력 및 네트워킹 - TCP 블로킹 채널
    CSE/Java 2015. 9. 4. 16:25
    NIO 기반 입출력 및 네트워킹은 여러 절로 구성되어 있습니다.




    6. TCP 블로킹 채널

     NIO를 이용해서 TCP 서버/클라이언트 애플리케이션을 개발하려면 블로킹, 넌블로킹, 비동기 구현 방식 중에서 하나를 결정해야 합니다.


     이 결정에 따라 구현이 완전히 달라지기 때문입니다. 


     다소 복잡해지기도 했지만 네트워크 입출력의 성능과 효율성 면에서 선택의 폭이 넓어졌기 때문에 최적의 네트워크 애플리케이션을 개발할 수 있게 되었습니다.


     이번 절에서는 블로킹 방식만 설명하고, 넌블로킹과 비동기 방식은 다른 절에서 설명하도록 하겠습니다.





     6.1 서버소켓 채널과 소켓 채널의 용도

      NIO에서 TCP 네트워크 통신을 위해 사용하는 채널은 java.nio.channels.ServerSocketChannel java.nio.channels.SocketChannel 입니다.


      이 두 채널은 IO의 ServerSocket과 Socket에 대응되는 클래스로, IO가 버퍼를 사용하지 않고 블로킹 입출력 방식만 지원한다면 ServerSocketChannel, SocketChannel은 버퍼를 이용하고 블로킹과 넌블로킹 방식을 모두 지원합니다.


      사용 방법은 IO와 큰 차이점은 없는데, 다음 그림처럼 ServerSocketChannel은 클라이언트 SocketChannel의 연결 요청을 수락하고 통신용 SocketChannel을 생성합니다.












     6.2 서버소켓 채널 생성과 연결 수락

      서버를 개발하려면 우선 ServerSocketChannel 객체를 얻어야 합니다.


      ServerSocketChannel은 정적 메소드인 open()으로 생성하고, 블로킹 방식으로 동작하기 위해 configureBlocking(true) 메소드를 호출합니다.


      기본적으로 블로킹 방식으로 동작하지만, 명시적으로 설정하는 이유는 넌블로킹과 구분하기 위해서입니다.


      포트에 바인딩하기 위해서는 bind() 메소드가 호출되어야 하는데, 포트 정보를 가진 InetSocketAddress 객체를 파라미터로 주면 됩니다.



    1
    2
    3
    4
    5
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
     
    ServerSocketChannel.configureBlocking(true);
     
    ServerSocketChannel.bind(new InetSocketAddress(5001));
    cs





      포트 바인딩까지 끝났다면  ServerSocketChannel은 클라이언트 연결 수락을 위해 accept() 메소드를 실행해야 합니다.


      accept() 메소드는 클라이언트가 연결 요청을 하기 전까지 블로킹되기 때문에 UI 및 이벤트를 처리하는 스레드에서 accept() 메소드를 호출하지 않도록 합니다.


      클라이언트가 연결 요청을 하면 accept()는 클라이언트와 통신할 SocketChannel을 만들고 리턴합니다.



    1
    2
    SocketChannel socketChannel = serverSocketChannel.accept();
     
    cs





      연결된 클라이언트의 IP와 포트 정보를 알고 싶다면 SocketChannel의 getRemoteAddress() 메소드를 호출해서 SocketAddress를 얻으면 됩니다.


      실제 리턴되는 것은 InetSocketAddress 인스턴스이므로 다음과 같이 타입 변환할 수 있습니다.


      


    1
    2
    InetSocketAddress socketAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
     
    cs





      InetSocketAddress에는 다음과 같이 IP와 포트 정보를 리턴하는 메소드들이 있습니다.

     


     리턴 타입

     메소드명(파라미터) 

     설명 

     String 

     getHostName() 

     클라이언트 IP 리턴 

     int 

     getPort() 

     클라이언트 포트 번호 리턴 

     String 

     toString() 

     "IP:포트번호" 형태의 문자열 리턴 





      더 이상 클라이언트를 위해 연결 수락이 필요 없다면 ServerSocketChannel의 close() 메소드를 호출해서 포트를 언바인딩시켜야 합니다. 그래야 다른 프로그램에서 해당 포트를 재사용할 수 있습니다.



    1
    2
    serverSocketChannel.close();
     
    cs




      다음 예제는 반복적으로 accept() 메소드를 호출해서 다중 클라이언트 연결을 수락하는 가장 기본적인 예제입니다.



      * ServerExam.java


    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
     
    package pathexam;
     
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
     
    public class ServerExam {
     
        public static void main(String[] args) {
            ServerSocketChannel serverSocketChannel = null;
     
            try {
                serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.configureBlocking(true);
                serverSocketChannel.bind(new InetSocketAddress(5001));
     
                while (true) {
                    System.out.println("[연결 기다림]");
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    InetSocketAddress isa = (InetSocketAddress) socketChannel.getRemoteAddress();
                    System.out.println("[연결 수락함] " + isa.getHostName());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
     
            if (serverSocketChannel.isOpen()) {
                try {
                    serverSocketChannel.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
     
    }
     
    cs














     6.3 소캣 채널 생성과 연결 요청

      클라이언트가 서버에 연결 요청을 할 때에는 java.nio.channels.SocketChannel 을 이용합니다.


      SocketChannel은 정적 메소드인 open()으로 생성하고, 블로킹 방식으로 동작하기 위해 configureBlocking(true) 메소드를 호출합니다.


      서버 연결 요청은 connect() 메소드를 호출하면 되는데, 서버 IP와 포트 정보를 가진 InetSocketAddress 객체를 파라미터로 주면됩니다.


      connect() 메소드는 연결이 완료될 때까지 블로킹되고, 연결이 완료되면 리턴합니다.


      다음은 로컬 PC의 5001 포트에 바인됭된 서버에 연결을 요청하는 코드입니다.



    1
    2
    3
    4
    5
    6
    SocketChannel socketChannel = SocketChannel.open();
     
    SocketChannel.configureBlocking(true);
     
    SocketChannel.connect(new InetSocketAddress("localhost"5001));
     
    cs




      connect() 메소드는 서버에 연결될 때까지 블로킹되므로 UI 및 이벤트를 처리하는 스레드에서 connect() 메소드를 호출하지 않도록 해야합니다.


      블로킹되면 UI 갱신이나 이벤트 처리를 할 수 없기 때문입니다. 연결된 후, 클라이언트 프로그램을 종료하거나, 필요에 따라서 연결을 끊고 싶다면 다음과 같이  SocketChannel의 close()를 호출하면 됩니다.



    1
    2
     
    SocketChannel.close();
    cs



      다음 예제는 localhost 5001 포트로 연결 요청하는 코드입니다. connect() 메소드가 정상적으로 리턴되면 연결 성공한 것입니다.



      * ClientExam.java


    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
    package pathexam;
     
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.channels.SocketChannel;
     
    public class ClientExam {
     
        public static void main(String[] args) {
            SocketChannel socketChannel = null;
     
            try {
                socketChannel = SocketChannel.open();
                socketChannel.configureBlocking(true);
     
                System.out.println("[연결 요청]");
                socketChannel.connect(new InetSocketAddress("localhost"5001));
                System.out.println("[연결 성공]");
     
            } catch (Exception e) {
                e.printStackTrace();
            }
     
            if (socketChannel.isOpen()) {
                try {
                    socketChannel.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
     
    }
     
    cs











     6.4 소켓 채널 데이터 통신

      클라이언트가 연결 요청하고 서버가 수락했다면, 양쪽 SocketChannel 객체의 read(), write() 메소드를 호출해서 데이터 통신을 할 수 있습니다.


      이 메소드들은 모두 버퍼를 가지고 있기 때문에 버퍼로 읽고, 쓰는 작업을 해야 합니다.







      다음은 SocketChannel의 write () 메소드를 이용해서 문자열을 보내는 코드입니다.



    1
    2
    3
    4
    Charset charset = Charset.forName("UTF-8");
    ByteBuffer buffer = charset.encode("Hello Server");
    socketChannel.write(buffer);
     
    cs



      다음은 SocketChannel의 read () 메소드를 이용해서 문자열을 받는 코드입니다.


    1
    2
    3
    4
    5
    6
    ByteBuffer buffer = ByteBuffer.allocate(100);
    int byteCount = socketChannel.read(buffer);
    buffer.flip();
    Charset charset = Charset.forName("UTF-8");
    String data = charset.decode(buffer).toString();
     
    cs




      다음 예제는 연결 성공 후, 클라이언트가 먼저 "Hello Server"를 보내면, 서버가 이 데이터를 받고 "Hello Client"를 클라이언트로 보내면 클라이언트가 이 데이터를 받는 예제입니다.



      * ClientExam.java


    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
     
    package pathexam;
     
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.Charset;
     
    public class ClientExam {
     
        public static void main(String[] args) {
            SocketChannel socketChannel = null;
     
            try {
                socketChannel = SocketChannel.open();
                socketChannel.configureBlocking(true);
     
                System.out.println("[연결 요청]");
                socketChannel.connect(new InetSocketAddress("localhost"5001));
                System.out.println("[연결 성공]");
     
                ByteBuffer byteBuffer = null;
                Charset charset = Charset.forName("UTF-8");
     
                byteBuffer = charset.encode("Hello Server");
                socketChannel.write(byteBuffer);
                System.out.println("[데이터 보내기 성공]");
     
                byteBuffer = ByteBuffer.allocate(100);
                int byteCount = socketChannel.read(byteBuffer);
                byteBuffer.flip();
     
                String data = charset.decode(byteBuffer).toString();
                System.out.println("[데이터 받기 성공]: " + data);
     
            } catch (Exception e) {
                e.printStackTrace();
            }
     
            if (socketChannel.isOpen()) {
                try {
                    socketChannel.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
     
    }
     
    cs






      * ServerExam.java


    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
     
    package pathexam;
     
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.Charset;
     
    public class ServerExam {
     
        public static void main(String[] args) {
            ServerSocketChannel serverSocketChannel = null;
     
            try {
                serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.configureBlocking(true);
                serverSocketChannel.bind(new InetSocketAddress(5001));
     
                while (true) {
                    System.out.println("[연결 기다림]");
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    InetSocketAddress isa = (InetSocketAddress) socketChannel.getRemoteAddress();
                    System.out.println("[연결 수락함] " + isa.getHostName());
     
                    ByteBuffer byteBuffer = null;
                    Charset charset = Charset.forName("UTF-8");
     
                    byteBuffer = ByteBuffer.allocate(100);
                    int byteCount = socketChannel.read(byteBuffer);
                    byteBuffer.flip();
     
                    String data = charset.decode(byteBuffer).toString();
                    System.out.println("[데이터 받기 성공]: " + data);
     
                    byteBuffer = charset.encode("Hello Client");
                    socketChannel.write(byteBuffer);
                    System.out.println("[데이터 보내기 성공]");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
     
            if (serverSocketChannel.isOpen()) {
                try {
                    serverSocketChannel.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
     
    }
     
    cs










      데이터를 받기 위해 read() 메소드를 호출하면 상대방이 데이터를 보내기 전까지는 블로킹(blocking) 되는데, read() 메소드가 블로킹 해제되고 리턴되는 경우는 다음 세 가지입니다.




     블로킹이 해제되는 경우

     리턴 값 

     상대방이 데이터를 보냄 

     읽은 바이트 수 

     상대방이 정상적으로 SocketChannel의 close() 호출 

     -1 

     상대방이 비정상적으로 종료 

     IOException 발생 




      상대방이 정상적으로 SocketChannel의 close() 를 호출하고 연결을 끊었을 경우와 상대방이 비정상적으로 종료된 경우는 예외 처리를 해서 이쪽도 SocketChannel을 닫기위해 close() 메소드를 호출해야 합니다.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    try {
        
        int byteCount = socketChannel.read(byteBuffer);
     
        if (readByteCount == -1) {
            throw new IOException();
        }
     
    catch (Exception e) {
        try {
            socketChannel.close();    
        } catch (Exception e2) { }
    }
     
    cs








     6.5 스레드 병렬 처리

      TCP 블로킹 방식은 데이터 입출력이 완료되기 전까지 read()와 write() 메소드가 블로킹 됩니다.


      만약 애플리케이션을 실행시키는 main 스레드가 직접 입출력 작업을 담당하게 된다면 입출력이 완료될 때까지 다른 작업을 할 수 없는 상태가 됩니다. 예를 들어 서버 애플리케이션은 지속적으로 클라이언트의 연결 수락 기능을 수행해야 하는데, 입출력에서 블로킹되면 이 작업을 할 수 없게 됩니다.


      또한 클라이언트1과 입출력하는 동안에는 클라이언트2와 입출력을 할 수 없게 됩니다. 그렇기 때문에 클라이언트 연결(채널) 하나에 작업 스레드 하나를 할당해서 병렬 처리해야 합니다.





      

      위 그림과 같이 스레드 병렬 처리를 할 경우 수천 개의 클라이언트가 동시에 연결되면 수천 개의 스레드가 서버에 생성되기 때문에 서버 성능이 급격히 저하되고, 다운되는 현상이 발생할 수 있습니다.


      클라이언트의 폭증으로 인해 서버의 과도한 스레드 생성을 방지하려면 스레드풀을 사용하는 것이 바람직합니다. 다음은 스레드풀을 이용할 경우 서버 구현 방식을 보여줍니다.








      스레드풀은 스레드 수를 제한해서 사용하기 때문에 갑작스런 클라이언트의 폭증은 작업 큐의 작업량만 증가시킬 뿐 스레드 수에는 변함이 없기 때문에 서버 성능은 완만히 저하됩니다. 다만 대기하는 작업량이 증가하기 때문에 개별 클라이언트에서 응답을 늦게 받을 수 있습니다. 이 경우 서버의 하드웨어 사양에 맞게 적절히 스레드풀의 스레드 수를 늘려주면 됩니다.
















     6.6 채팅 서버 구현

      채팅 서버를 구현해보면서 ExecutorService(스레드풀), ServerSocketChannel, SocketChannel들이 어떻게 사용되는지 이해해봅시다.



       서버 클래스 구조


     다음은 서버 클래스의 구조를 보여줍니다.


      * ServerExam.java


    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
     
    package pathexam;
     
    import java.nio.channels.ServerSocketChannel;
    import java.util.List;
    import java.util.Vector;
    import java.util.concurrent.ExecutorService;
     
    import javafx.application.Application;
     
    public class ServerExam extends Application {
     
        ExecutorService executorService;
        ServerSocketChannel serverSocketChannel;
        List<Client> connections = new Vector<Client>();
     
        void startServer() {
            // 서버 시작 코드
        }
     
        void stopServer() {
            // 서버 종료 코드
        }
     
        class Client {
            // 데이터 통신 코드
        }
     
        //////////////////////
     
        // UI 생성 코드
     
    }
     
    cs





       startServer() 메소드


     start 버튼을 클릭하면 startServer() 메소드가 실행되는데, startServer() 메소드에서는 ExecutorService 생성, ServerSocketChannel 생성 및 포트 바인딩, 연결 수락 코드가 필요합니다. 다음은 CPU 코어 수의 맞게 스레드를 생성해서 관리하는 ExecutorService를 생성합니다. 스레드풀 생성 후, 5001 번 포트에서 클라이언트의 연결을 수락하는 ServerSocketChannel을 생성합니다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     
    void startServer() {
        // 스레드 풀 생성
        executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
     
        try {
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(true);
            serverSocketChannel.bind(new InetSocketAddress(5001));
     
        } catch (Exception e) {
            if (serverSocketChannel.isOpen()) {
                stopServer();
            }
        }
     
    }
    cs


      



      다음은 연결 수락 작업을 Runnable 객체로 만들고 스레드풀의 작업 스레드로 실행시키는 코드입니다. ServerSocketChannel은 반복해서 클라이언트 연결 요청을 기다려야 하므로 스레드풀의 작업 스레드상에서 accept() 메소드를 반복적으로 호출해주어야 합니다.



    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
    void startServer() {
        // 스레드 풀 생성
        executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
     
        try {
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(true);
            serverSocketChannel.bind(new InetSocketAddress(5001));
     
        } catch (Exception e) {
            if (serverSocketChannel.isOpen()) {
                stopServer();
            }
        }
     
        Runnable runnable = new Runnable() {
     
            @Override
            public void run() {
                Platform.runLater(() -> {
                    displayText("[서버 시작]");
                    btnStartStop.setText("stop");
                });
     
                while (true) {
                    try {
                        SocketChannel socketChannel = serverSocketChannel.accept();
     
                        String msg = "[연결 수락: " + socketChannel.getRemoteAddress() + ": "
                                + Thread.currentThread().getName() + "]";
                        
                        Client client = new Client(socketChannel);
                        connections.add(client);
     
                        Platform.runLater(()->displayText("[연결 개수: " + connections.size() + "]"));
                    } catch (Exception e) {
                        if (serverSocketChannel.isOpen()) {
                            stopServer();
                        }
                        
                        break;
                    }
                }
            }
        };
        
        executorService.submit(runnable);
     
    }
    cs






       stopServer() 메소드


     stop 버튼을 클릭하면 stopServer() 메소드가 실행되는데, stopServer() 메소드에는 연결된 모든 SocketChannel 닫기, ServerSocketChannel 닫기, ExecutorService 종료 코드가 필요합니다.



    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
    void stopServer() {
        try {
            Iterator<Client> iterator = connections.iterator();
            
            while (iterator.hasNext()) {
                Client client = iterator.next();
                client.socketChannel.close();
                iterator.remove();
            }
            
            if (serverSocketChannel != null && serverSocketChannel.isOpen()) {
                serverSocketChannel.close();
            }
            
            if (executorService != null && executorService.isShutdown()) {
                executorService.shutdown();
            }
            
            Platform.runLater(()->{
                displayText("[서버 종료]");
                btnStartStop.setText("start");
            });
        } catch (Exception e) {
            
        }
    }
    cs





      Client 클래스


     서버는 다수의 클라이언트가 연결하기 때문에 클라이언트를 관리해야 합니다. 클라이언트별로 고유한 데이터를 저장할 필요도 있기 때문에 Client 클래스를 작성하고, 연결 수락 시 마다 Client 인스턴스를 생성해서 관리하는 것이 좋습니다. Client 클래스에는 데이터 받기 및 보내기 코드가 포함됩니다. 다음은 Client 클래스의 구조를 보여줍니다.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Client {
        SocketChannel socketChannel;
     
        public Client(SocketChannel socketChannel) {
            this.socketChannel = socketChannel;
            receive();
        }
     
        void receive() {
            // 데이터 받기
        }
     
        void send(String data) {
            // 데이터 전송
        }
    }
    cs





      다음 코드는 클라이언트의 데이터를 받는 receive() 메소드입니다. 스레드 풀의 작업 스레드가 처리하도록 Runnable로 작업을 정의하고 있고, 클라이언트가 보낸 데이터를 반복적으로 받기 위해 무한 루프를 돌면서 SocketChannel의 read() 메소드를 호출하고 있습니다.



    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
    void receive() {
        Runnable runnable = new Runnable() {
     
            @Override
            public void run() {
                while (true) {
                    try {
                        ByteBuffer byteBuffer = ByteBuffer.allocate(100);
                        
                        // 클라이언트가 비정상 종료를 했을 경우 IOException 발생
                        int byteCount = socketChannel.read(byteBuffer);
                        
                        // 클라이언트가 정상적으로 SocketChannel의 close()를 호출헀을 경우
                        if (byteCount == -1) {
                            throw new IOException();
                        }
                        
                        String msg = "[요청 처리: " + socketChannel.getRemoteAddress() + ": " + Thread.currentThread().getName() + "]";
                        
                        Platform.runLater(()->displayText(msg));
                        
                        byteBuffer.flip();
                        Charset charset = Charset.forName("UTF-8");
                        String data = charset.decode(byteBuffer).toString();
                        
                        for (Client client : connections) {
                            client.send(data);
                        }
                        
                    } catch (Exception e) {
                        try {
                            connections.remove(Client.this);
                            
                            String msg = "[클라이언트 통신 안됨: " +
                                    socketChannel.getRemoteAddress() + ": " +
                                    Thread.currentThread().getName() + "]";
                            
                            Platform.runLater(()->displayText(msg));
                            socketChannel.close();
                        } catch (IOException e2) {
                            
                        }
                        
                        break;
                    }
                }
            }
            
        };
        
        executorService.submit(runnable);
    }
    cs





      다음 코드는 데이터를 클라이언트로 보내는 send() 메소드입니다. 스레드풀의 작업 스레드가 처리하도록 작업을 Runnable로 정의하고 있습니다.



    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
    void send(String data) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Charset charset = Charset.forName("UTF-8");
                    ByteBuffer byteBuffer = charset.encode(data);
                    socketChannel.write(byteBuffer);
                } catch (Exception e) {
                    try {
                        String msg = "[클라이언트 통신 안됨: " + socketChannel.getRemoteAddress() + ": "
                                + Thread.currentThread().getName() + "]";
     
                        Platform.runLater(() -> displayText(msg));
                        connections.remove(Client.this);
                        socketChannel.close();
     
                    } catch (IOException e2) {
     
                    }
                }
            }
        };
        executorService.submit(runnable);
    }
    cs









       UI 생성 코드


     다음은 ServerExample UI 생성 코드를 보여줍니다. 프로그램적 레이아웃을 이용해서 컴포넌트를 배치했습니다.


    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
    TextArea txtDisplay;
    Button btnStartStop;
     
    @Override
    public void start(Stage primaryStage) throws Exception {
        BorderPane root = new BorderPane();
        root.setPrefSize(500300);
     
        txtDisplay = new TextArea();
        txtDisplay.setEditable(false);
        BorderPane.setMargin(txtDisplay, new Insets(0020));
        root.setCenter(txtDisplay);
     
        btnStartStop = new Button("start");
        btnStartStop.setPrefHeight(30);
        btnStartStop.setMaxWidth(Double.MAX_VALUE);
     
        btnStartStop.setOnAction(e -> {
            if (btnStartStop.getText().equals("start")) {
                startServer();
            } else if (btnStartStop.getText().equals("stop")) {
                stopServer();
            }
        });
     
        root.setBottom(btnStartStop);
     
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("app.css").toString());
        primaryStage.setScene(scene);
        primaryStage.setTitle("Server");
        primaryStage.setOnCloseRequest(event -> stopServer());
        primaryStage.show();
     
    }
     
    void displayText(String text) {
        txtDisplay.appendText(text + "\n");
    }
     
    public static void main(String[] args) {
        launch(args);
    }
    cs





      29 라인은 외부 css 파일(app.css)을 Scene에 적용하고 있습니다. 그 이유는 TextArea의 배경색 때문인데, TextArea는 여러 겹의 컨테이너로 둘러싸여 있는 복잡한 구조를 가지고 있어 단순히 TextArea의 setStyle()로 배경색을 바꿀 수 없습니다. 그래서 다음과 같이 외부 CSS 클래스 선택자를 이용해서 컨테이너 배경생을 변경했습니다.



     * app.css


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*text-area 배경색 */
    .text-area {
        -fx-background-color: gold;
    }
    /*scroll-pane 배경색 */
    .text-area .scroll-pane {
        -fx-background-color: transparent;
    }
    // viewport 배경색
    .text-area .scroll-pane .viewport{
        -fx-background-color: transparent;
    }
    // content 배경색
    .text-area .scroll-pane .content{
        -fx-background-color: transparent;
    }
     
     
    cs










     6.7 채팅 클라이언트 구현

      채팅 클라이언트를 구현해보면서 SocketChannel이 어떻게 사용되는지 이해해봅시다.



      클라이언트 클래스 구조


     

      * ClientExam.java


    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
    package pathexam;
     
    import java.nio.channels.SocketChannel;
     
    import javafx.application.Application;
     
    public class ClientExam extends Application {
     
        SocketChannel socketChannel;
     
        void startClient() {
     
        }
     
        void stopClient() {
     
        }
     
        void receive() {
     
        }
     
        void send(String data) {
     
        }
     
        ///////////////////////
        // UI 생성 코드
     
        public static void main(String[] args) {
     
        }
     
    }
     
    cs





      startClient() 메소드



    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
     
    void startClient() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    socketChannel = SocketChannel.open();
                    socketChannel.configureBlocking(true);
                    socketChannel.connect(new InetSocketAddress("localhost"5001));
                    Platform.runLater(() -> {
                        try {
                            displayText("[연결 완료: " + socketChannel.getRemoteAddress() + "]");
                            btnConn.setText("stop");
                            btnSend.setDisable(false);
                        } catch (Exception e) {
                        }
                    });
                } catch (Exception e) {
                    Platform.runLater(() -> displayText("[서버 통신 안됨]"));
     
                    if (socketChannel.isOpen()) {
                        stopClient();
                    }
                    return;
                }
                receive();
            }
        };
     
        thread.start();
    }
    cs






       stopClient() 메소드


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     
    void stopClient() {
        try {
            Platform.runLater(() -> {
                displayText("[연결 끊음]");
                btnConn.setText("start");
                btnSend.setDisable(true);
            });
     
            if (socketChannel != null && socketChannel.isOpen()) {
                socketChannel.close();
            }
        } catch (IOException e) {
     
        }
    }
     
    cs





       receive() 메소드



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
     
    void receive() {
        while (true) {
            try {
                ByteBuffer byteBuffer = ByteBuffer.allocate(100);
     
                int byteCount = socketChannel.read(byteBuffer);
     
                if (byteCount == -1) {
                    throw new IOException();
                }
     
                byteBuffer.flip();
                Charset charset = Charset.forName("UTF-8");
                String data = charset.decode(byteBuffer).toString();
     
                Platform.runLater(() -> displayText("[받기 완료] " + data));
            } catch (Exception e) {
                Platform.runLater(() -> displayText("[서버 통신 안됨]"));
                stopClient();
                break;
            }
        }
    }
    cs





       send(String data) 메소드



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void send(String data) {
        Thread thread = new Thread() {
     
            @Override
            public void run() {
                try {
                    Charset charset = Charset.forName("UTF-8");
                    ByteBuffer byteBuffer = charset.encode(data);
                    socketChannel.write(byteBuffer);
                    Platform.runLater(() -> displayText("[보내기 완료]"));
                } catch (Exception e) {
                    Platform.runLater(() -> displayText("[서버 통신 안됨]"));
                    stopClient();
                }
            }
     
        };
    }
     
     
    cs






       UI 생성 코드


    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
     
     
    TextArea txtDisplay;
    TextField txtInput;
    Button btnConn, btnSend;
     
    @Override
    public void start(Stage primaryStage) throws Exception {
        BorderPane root = new BorderPane();
        root.setPrefSize(500300);
     
        txtDisplay = new TextArea();
        txtDisplay.setEditable(false);
        BorderPane.setMargin(txtDisplay, new Insets(0020));
        root.setCenter(txtDisplay);
     
        BorderPane bottom = new BorderPane();
        txtInput = new TextField();
        txtInput.setPrefSize(6030);
        BorderPane.setMargin(txtInput, new Insets(0111));
     
        btnConn = new Button("start");
        btnConn.setPrefSize(6030);
        btnConn.setOnAction(e -> {
            if (btnConn.getText().equals("start")) {
                startClient();
            } else if (btnConn.getText().equals("stop")) {
                stopClient();
            }
        });
     
        btnSend = new Button("send");
        btnSend.setPrefSize(6030);
        btnSend.setDisable(true);
        btnSend.setOnAction(e -> send(txtInput.getText()));
     
        bottom.setCenter(txtInput);
        bottom.setLeft(btnConn);
        bottom.setRight(btnSend);
     
        root.setBottom(bottom);
     
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("app.css").toString());
        primaryStage.setScene(scene);
        primaryStage.setTitle("Client");
        primaryStage.setOnCloseRequest(evnt -> stopClient());
        primaryStage.show();
     
    }
     
    void displayText(String text) {
        txtDisplay.appendText(text + "\n");
    }
     
    public static void main(String[] args) {
        launch(args);
    }
    cs













    * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.

    댓글

Designed by Tistory.