안녕하세요 알통몬입니다. 공감 및 댓글은 포스팅 하는데 아주아주 큰 힘이 됩니다!! 포스팅 내용이 찾아주신 분들께 도움이 되길 바라며 더 깔끔하고 좋은 포스팅을 만들어 나가겠습니다^^
|
채팅 서버 구현하기 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
이상입니다.
'자바' 카테고리의 다른 글
자바 NIO TCP 비동기 채널 - 서버 소켓 채널, 소켓 채널, 소켓 채널 데이터 통신 (0) | 2017.04.27 |
---|---|
NIO TCP 비동기 채널의 특징, 비동기 채널 그룹 (0) | 2017.04.26 |
자바 NIO TCP 넌블로킹 - 선택된 키셋, 채널 작업 처리 (0) | 2017.04.24 |
자바 NIO TCP 넌블로킹 채널 - 넌블로킹 방식의 특징, 셀렉터의 생성과 등록 (0) | 2017.04.21 |
자바 NIO TCP 블로킹과 인터럽트 (0) | 2017.04.20 |