자바

자바 NIO TCP 넌블로킹 채팅 서버, 채팅 클라이언트 구현하기 Java Nonblocking chatting server, chatting server

알통몬_ 2017. 4. 25. 09:58
반응형


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

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

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

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

 


채팅 서버 구현하기 Chatting Server  :

Selector 셀렉터와 넌블로킹 ServerSocketChannel, SocketChannel 을 이용해

넌블로킹 채텅 서버 구현에 대해 보겠습니다.

본 포스팅에는 JavaFX에 대한 내용이 나옵니다.

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


1. 서버 클래스의 구조

public class ServerExample extends Application {

 Selector selector; // 넌블로킹의 핵심인 Selector 필드 선언

 ServerSocketChannel serverSocketChannel; // 클라이언트 연결 수락하는 ServerSocketChannel

                                                                                         필드 선언

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

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

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

 void startServer() {// 서버 시작 코드}

 

 void stopServer() {// 서버 종료 코드} 

 

 void accept(SelectionKey selectionKey) {//연결 수락 코드}

 

 class Client {// 데이터 통신 코드}


UI 생성 코드

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



2. startServer() 메소드

void startServer() {

  try {

   selector = Selector.open(); // 셀렉터 생성

   serverSocketChannel = ServerSocketChannel.open(); // 생성

   serverSocketChannel.configureBlocking(false); // 넌블로킹 설정

   serverSocketChannel.bind(new InetSocketAddress(5001)); // 포트에 바인딩

   serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

                   셀럭터를 등록, 작업 유형을 OP_ACCEPT로 지정

  } catch (Exception e) {

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

   return;// 예외발생시 서버소켓채널이 열려있으면 stop() 호출

  } 

  

  Thread thread = new Thread() {

   @Override

   public void run() {

    while(true) {

     try {

      int keyCount = selector.select();//작업 처리가 준비된

                                                                            채널이 있을 때까지 대기

      if(keyCount == 0) { continue; }

      Set<SelectionKey> selectedKeys = selector.selectedKeys();

                                           //작업 처리 준비가 된 키를 얻고 Set 컬렉션으로 리턴.

      Iterator<SelectionKey> iterator = selectedKeys.iterator();

      while (iterator.hasNext()) {

          SelectionKey selectionKey = iterator.next();

          if (selectionKey.isAcceptable()) {//연결 수락 작업일 경우

           accept(selectionKey);

          } else if (selectionKey.isReadable()) {//읽기 작업일 경우

           Client client = (Client)selectionKey.attachment();

           client.receive(selectionKey);

          } else if (selectionKey.isWritable()) {// 쓰기 작업일 경우

           Client client = (Client)selectionKey.attachment();

           client.send(selectionKey);

          }

          iterator.remove();//선택된 키셋에서 처리 완료된

      }                         SelectionKey를 제거

     } catch (Exception e) {

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

      break;

     }

    }

   }

  };

  thread.start();스레드 시작

  

  Platform.runLater(()->{

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

   btnStartStop.setText("stop");

  });

 

 }


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(selector!=null && selector.isOpen()) {

    selector.close();

   }

   Platform.runLater(()->{

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

    btnStartStop.setText("start");   

   }); 

  } catch (Exception e) {}

 } 




작업 스레드는 SelectionKey 의 isAcceptable() 이 true를 리턴하면(작업유형이 OP_ACCEPT)

accept()를 호출하고, accept()는 연결을 수락하고 Client 객체를 생성하는 역할을 합니다.

4. accept(SelectionKey selectionKey) 메소드

void accept(SelectionKey selectionKey) {

  try {

   ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();//SelectionKey로부터 ServerSocketChannel을 얻음.

   SocketChannel socketChannel = serverSocketChannel.accept();

   //ServerSocketChannel의 accept()를 호출하면, SocketChannel리턴.

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

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

   

   Client client = new Client(socketChannel);// Client생성하고 

                                                                     connections컬렉션에 추가

   connections.add(client);

   

   Platform.runLater(()->displayText("[연결 개수: " + connections.size() + "]"));

  } catch(Exception e) {

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

  }

 

 }


5. Client 클래스와 receive(SelectionKey selectionKey), send(SelectionKey selectionKey)

class Client {

  SocketChannel socketChannel;

  String sendData; // 클라이언트에 보낼 데이터를 저장할 필드

  

  Client(SocketChannel socketChannel) throws IOException {

   this.socketChannel = socketChannel;

   socketChannel.configureBlocking(false); // 넌블로킹 지정

   SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ); // 읽기작업 유형으로 Selector에 등록

   selectionKey.attach(this);//SelectionKey에 자신을 첨부 객체로 저장.

  }

  

  void receive(SelectionKey selectionKey) {

   try {

    ByteBuffer byteBuffer = ByteBuffer.allocate(100);

    

    //상대방이 비저상 종료를 했을 경우 자동 IOException 발생

    int byteCount = socketChannel.read(byteBuffer); .. 데이터 받기

   

    //상대방이 SocketChannel의 close() 메소드를 호출할 경우

    if(byteCount == -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.sendData = data;

     SelectionKey key = client.socketChannel.keyFor(selector);

     key.interestOps(SelectionKey.OP_WRITE);//모든 클라이언트에게

    }                                                        문자열을 전송

    selector.wakeup(); // 변경된 작업 유형일 감지하도록 하기위해

                                               Selector의 select()블로킹을 해제하고 다시 실행

   } catch(Exception e) {

    try {

     connections.remove(this);

     String message = "[클라이언트 통신 안됨: " + socketChannel.getRemoteAddress() + ": " + Thread.currentThread().getName() + "]";

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

     socketChannel.close();

    } catch (IOException e2) {}

   }

  }

  

  void send(SelectionKey selectionKey) {

   try {

    Charset charset = Charset.forName("UTF-8");

    ByteBuffer byteBuffer = charset.encode(sendData);

    socketChannel.write(byteBuffer);//데이터 보내기

    selectionKey.interestOps(SelectionKey.OP_READ);//작업 유형 변경

    selector.wakeup();변경된 작업 유형일 감지하도록 하기위해

                                               Selector의 select()블로킹을 해제

   } catch(Exception e) {

    try {

     String message = "[클라이언트 통신 안됨: " + socketChannel.getRemoteAddress() + ": " + Thread.currentThread().getName() + "]";

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

     connections.remove(this);

     socketChannel.close();

    } catch (IOException e2) {}

   }

  }

 

 } 


6. UI 생성 코드

TextArea txtDisplay;

 Button btnStartStop;

 

 @Override

 public void start(Stage primaryStage) throws Exception {

  BorderPane root = new BorderPane();

  root.setPrefSize(500, 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);

 

 }


채팅 클라이언트도 넌블로킹 SocketChannel 로 구현할 수 있지만, 일반적으로 넌블로킹 방식은

서버를 개발할 때 많이 사용됩니다.

클라이언트의 경우 이전에 포스팅했던 TCP 블로킹 방식을 사용하면 되겠습니다!
http://altongmon.tistory.com/296

이상입니다.



반응형