자바

자바 NIO TCP 블로킹 채널로 채팅 서버 구현하기

알통몬_ 2017. 4. 19. 09:55
반응형


안녕하세요 알통몬입니다.

공감 및 댓글은 포스팅 하는데 아주아주 큰 힘이 됩니다!!

포스팅 내용이 찾아주신 분들께 도움이 되길 바라며

더 깔끔하고 좋은 포스팅을 만들어 나가겠습니다^^

 


ExecutorService(스레드풀) 과 ServerSocketChannel, SocketChannel 이 어떻게 사용되는지를

채팅 서버 구현을 통해 알아보겠습니다.


이 포스팅에서는 JAVAFX에 대한 내용이 들어있습니다.

아래 URL로 가셔서 JAVAFX에 대해 공부하시는 것을 추천드립니다.

http://blog.naver.com/rain483/220605517395


1. 서버 클래스의 구조

 public class ServerExample extends Application {

 ExecutorService executorService; // 스레드풀 필드 선언

 ServerSocketChannel serverSocketChannel; // 서버 소켓 채널 필드 선언

 List<Client> connections = new Vector<Client>(); // 연결된 클라이언트를 저장하는

                                                                List<Client>타입의 connections필드 선언

                                                                스레드에 안전한 Vector로 초기화

 

 void startServer() { }

 

 void stopServer() { }


      class Client {   }

      

      ////////////////////////////////

     UI 생성 코드

}


2. startServer()

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(); }

   return;

  }

  

  Runnable runnable = new Runnable() { // 연력 수락 작업을 Runnable로 정의

   @Override

   public void run() {

    Platform.runLater(()->{ 

     displayText("[[서버 시작]]");

     btnStartStop.setText("stop");

    });   

    while(true) {

     try {

      SocketChannel socketChannel = serverSocketChannel.accept();

                                      // 클라이언트의 요청을 기다림.

      String message = "[[연결 수락: " + socketChannel.getRemoteAddress()  + ": " + Thread.currentThread().getName() + "]]";

      Platform.runLater(()->displayText(message));

  

      Client client = new Client(socketChannel);

      connections.add(client); // 클래이언트 객체 생성 후 connections                                                                      컬렉션에 추가

      

      Platform.runLater(()->displayText("[[연결 개수: " + connections.size() + "]]")); // 연결된 클라이언트 수

     } catch (Exception e) {

      if(serverSocketChannel.isOpen()) { stopServer(); }

      break;

     }

    }

   }

  };

  executorService.submit(runnable);

 }


3. stopServer()

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(()->{ // 작업 스레드는 UI 변경 불가하므로 -> Platform.runLater()사용

    displayText("[[서버 멈춤]]");

    btnStartStop.setText("start");   

   });   

  } catch (Exception e) {}

 

 } 


4. Client 클래스

class Client {

 SocketChannel socketChannel; // 통신을 위한 SocketChannel을 필드로 선언

  

 Client(SocketChannel socketChannel) { //생성자 선언(매개값으로 소켓 채널 필드 초기화)

                                                                                         receive() 호출

  this.socketChannel = socketChannel;

  receive();

 }

  

  void receive() { // 클라이언트의 데이터를 받기 위해 receive() 선언

   Runnable runnable = new Runnable() { // 스레드풀의 작업 스레드가 처리하도록

                                                                  Runnable로 작업 정의

    @Override

    public void run() {

     while(true) { // 클라이언트가 보낸 데이터를 반복적으로 받기 위한

                                                  무한 루프

      try {

      ByteBuffer byteBuffer = ByteBuffer.allocate(100);

       

      //클라이언트가 비정상 종료를 했을 경우 IOException 발생

      int readByteCount = socketChannel.read(byteBuffer);

       

     //클라이언트가 정상적으로 SocketChannel의 close()를 호출했을 경우

      if(readByteCount == -1) { 

      throw new IOException();

      }

       

       String message = "[[요청 처리: " + socketChannel.getRemoteAddress() + ": " + Thread.currentThread().getName() + "]]";

       Platform.runLater(()->displayText(message));

       

       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 message = "[[클라이언트 통신 안됨: " + socketChannel.getRemoteAddress() + ": " + Thread.currentThread().getName() + "]]";

        Platform.runLater(()->displayText(message));

        socketChannel.close();

       } catch (IOException e2) {}

       break;

      }

     }

    }

   };

   executorService.submit(runnable); // 스레드 풀에서 처리

  }

 

  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 message = "[[클라이언트 통신 안됨: " + socketChannel.getRemoteAddress() + ": " + Thread.currentThread().getName() + "]]";

       Platform.runLater(()->displayText(message));

       connections.remove(Client.this);

       socketChannel.close();

      } catch (IOException e2) {}

     }

    }

   };

   executorService.submit(runnable); // 스레드풀에서 처리

  }

 }


5. UI 생성 코드 = JavaFX

TextArea txtDisplay;

 Button btnStartStop;

 

 @Override

 public void start(Stage primaryStage) throws Exception {

  BorderPane root = new BorderPane();

  root.setPrefSize(500, 300); // 윈도우 사이즈 500 X 300

  

  txtDisplay = new TextArea();

  txtDisplay.setEditable(false);

  BorderPane.setMargin(txtDisplay, new Insets(0,0,2,0));

  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);

 }

}


다음 포스팅에서는 TCP 블로킹 채널로 클라이언트 구현하기에 대해 공부하겠습니다.

이상입니다.

반응형