Recent Posts
Recent Comments
Link
11-24 13:13
Today
Total
관리 메뉴

삶 가운데 남긴 기록 AACII.TISTORY.COM

NIO TCP 비동기 채널방식 채팅 서버/클라이언트 본문

DEV&OPS/Java

NIO TCP 비동기 채널방식 채팅 서버/클라이언트

ALEPH.GEM 2022. 5. 26. 17:03

비동기 채널

비동기 채널은 non-blocking 방식으로 동작하는데 차이점은 스레드 풀에게 작업 처리를 요청하고 즉시 리턴한뒤 콜백 메소드를 호출하여 작업 완료 후 실행되는 코드들은 콜백 메소드에 담아서 처리하는 방식입니다.

 

비동기 채널 그룹

비동기 채널 그룹은 같은 스레드 풀을 공유하는 비동기 채널들의 모음이라고 할수 있습니다.

하나의 스레드 풀을 사용한다면 모든 비동기 채널은 같은 채널 그룹에 속해야 합니다.

 

채팅 서버(비동기 채널 방식)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.Executors;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class AsyncServer extends Application{
	//비동기 채널 그룹
	AsynchronousChannelGroup channelGroup;
	//클라이언트 연결 수락
	AsynchronousServerSocketChannel serverSocketChannel;
	List<Client> connections = new Vector<Client>();
	
	//서버 시작
	void startServer() {
		try {
			//비동기 채널 그룹 생성
			channelGroup = AsynchronousChannelGroup.withFixedThreadPool(Runtime.getRuntime().availableProcessors(), Executors.defaultThreadFactory());
			//서버소켓 채널 열기
			serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup);
			//아이피 포트 바인드
			serverSocketChannel.bind(new InetSocketAddress(5001));
		}catch(Exception e) {
			if(serverSocketChannel.isOpen()) {
				stopServer();
			}
			return;
		}
		Platform.runLater(()->{
			displayText("[서버 시작]");
			btnStartStop.setText("stop");
		});
		
		//콜백 메소드 CompletionHandler 구현
		serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>(){

			@Override
			public void completed(AsynchronousSocketChannel socketChannel, Void attachment) {
				try {
					String message = "[연결 수락: "+socketChannel.getRemoteAddress() + " : "+Thread.currentThread().getName() + "]";
					Platform.runLater(()->displayText(message));
				}catch(IOException e) {
					
				}
				
				Client client = new Client(socketChannel);
				connections.add(client);
				Platform.runLater(()->displayText("[연결 개수: "+connections.size() + "]"));
				serverSocketChannel.accept(null, this); //실제 accept동작
			}

			@Override
			public void failed(Throwable exc, Void attachment) {
				if(serverSocketChannel.isOpen()) {
					stopServer();
				}
			}
			
		}); //accept()종료
		
	} //startServer()종료
	
	//서버 종료
	void stopServer() {
		try {
			connections.clear();
			if(channelGroup != null && !channelGroup.isShutdown()) {
				channelGroup.shutdown();
			}
			Platform.runLater(()->{
				displayText("[서버 종료]");
				btnStartStop.setText("start");
			});
		}catch(Exception e) {
			
		}
	}
	
	//데이터를 통신하는 객체
	class Client{
		AsynchronousSocketChannel socketChannel;

		public Client(AsynchronousSocketChannel socketChannel) {
			this.socketChannel = socketChannel;
			receive();
		}
		
		//클라이언트로부터 데이터 받기
		void receive() {
			ByteBuffer byteBuffer = ByteBuffer.allocate(100);
			socketChannel.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>(){

				@Override
				public void completed(Integer result, ByteBuffer attachment) {
					try {
						String message = "[요청 처리: "+socketChannel.getRemoteAddress() +" : "+Thread.currentThread().getName() + "]";
						Platform.runLater(()->displayText(message));
						
						attachment.flip();
						Charset charset = Charset.forName("utf-8");
						String data = charset.decode(attachment).toString();
						
						for(Client  client : connections) {
							client.send(data); //모든 클라이언트에게 보내기
						}
						
						ByteBuffer byteBuffer = ByteBuffer.allocate(100);
						socketChannel.read(byteBuffer, byteBuffer, this); //데이터 다시 읽기
					}catch(Exception e) {
						
					}
				}

				@Override
				public void failed(Throwable exc, ByteBuffer attachment) {
					try{
						String message = "[클라이언트 통신 안됨: "+socketChannel.getRemoteAddress()+" : " + Thread.currentThread().getName() + "]";
						Platform.runLater(()->displayText(message));
						connections.remove(Client.this);
						socketChannel.close();
					}catch(IOException e) {
						
					}
					
				}
				
			});
		}
		
		//클라이언트로 데이터 전송
		void send(String data) {
			Charset charset = Charset.forName("utf-8");
			ByteBuffer byteBuffer = charset.encode(data);
			socketChannel.write(byteBuffer, null, new CompletionHandler<Integer, Void>(){

				@Override
				public void completed(Integer result, Void attachment) {
					// TODO Auto-generated method stub
					
				}

				@Override
				public void failed(Throwable exc, Void attachment) {
					try{
						String message = "[클라이언트 통신 안됨: "+socketChannel.getRemoteAddress()+" : " + Thread.currentThread().getName() + "]";
						Platform.runLater(()->displayText(message));
						connections.remove(Client.this);
						socketChannel.close();
					}catch(IOException e) {
						
					}					
				}
				
			});
		}
		
	}

	//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 {
					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);
		}

}

 

채팅 클라이언트(비동기 방식)

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
import java.util.concurrent.Executors;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class AsyncClient extends Application {
	AsynchronousChannelGroup channelGroup;
	AsynchronousSocketChannel socketChannel;
	
	//연결 시작
	void startClient() {
		try {
			channelGroup = AsynchronousChannelGroup.withFixedThreadPool(Runtime.getRuntime().availableProcessors(), Executors.defaultThreadFactory());
			socketChannel = AsynchronousSocketChannel.open(channelGroup);
			socketChannel.connect(new InetSocketAddress("localhost", 5001), null, new CompletionHandler<Void,Void>(){

				@Override
				public void completed(Void result, Void attachment) {
					Platform.runLater(()->{
						try {
							displayText("[연결 완료: "+socketChannel.getRemoteAddress()+"]");
							btnConn.setText("stop");
							btnSend.setDisable(false);
						}catch(Exception e) {
							
						}
					});
					receive(); //서버에서 보낸 데이터 받기
				}

				@Override
				public void failed(Throwable exc, Void attachment) {
					Platform.runLater(()->displayText("[서버 통신 실패]"));
					if(socketChannel.isOpen()) {
						stopClient();
					}
				}
				
			});
		}catch(Exception e) {}
	}
	
	//연결 종료
	void stopClient() {
		Platform.runLater(()->{
			displayText("[연결 종료]");
			btnConn.setText("start");
			btnSend.setDisable(true);
		});
		if(channelGroup!=null && !channelGroup.isShutdown()) {
			channelGroup.shutdown();
		}
	}
	
	//서버로부터 데이터 받기
	void receive() {
		ByteBuffer byteBuffer = ByteBuffer.allocate(100);
		socketChannel.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>(){

			@Override
			public void completed(Integer result, ByteBuffer attachment) {
				try {
					attachment.flip();
					Charset charset = Charset.forName("utf-8");
					String data = charset.decode(attachment).toString();
					Platform.runLater(()->displayText(data));
					
					ByteBuffer byteBuffer = ByteBuffer.allocate(100);
					socketChannel.read(byteBuffer, byteBuffer,this); //데이터 다시 읽기
				}catch(Exception e) {
					
				}
			}

			@Override
			public void failed(Throwable exc, ByteBuffer attachment) {
				Platform.runLater(()->displayText("[서버 통신 실패]"));
				stopClient();
			}
			
		});
	}
	
	//서버로 데이터 전송
	void send(String data) {
		Charset charset = Charset.forName("utf-8");
		ByteBuffer byteBuffer = charset.encode(data);
		socketChannel.write(byteBuffer, null, new CompletionHandler<Integer, Void>(){

			@Override
			public void completed(Integer result, Void attachment) {
				//Platform.runLater(()->displayText("[보내기 완료]"));
			}

			@Override
			public void failed(Throwable exc, Void attachment) {
				Platform.runLater(()->displayText("[서버 통신 실패]"));
				stopClient();
			}
			
		});
	}
	
	// UI생성 코드
		TextArea txtDisplay;
		TextField txtInput;
		Button btnConn, btnSend;
		
		@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);
			
			BorderPane bottom = new BorderPane();
			txtInput = new TextField();
			txtInput.setPrefSize(60, 30);
			BorderPane.setMargin(txtInput, new Insets(0,1,1,1));
			
			btnConn = new Button("start");
			btnConn.setPrefSize(60, 30);
			//start stop 버튼이벤트 처리
			btnConn.setOnAction(e->{
				if(btnConn.getText().equals("start")) {
					startClient();
				}else if(btnConn.getText().equals("stop")) {
					stopClient();
				}
			});
			
			btnSend = new Button("send");
			btnSend.setPrefSize(60, 30);
			btnSend.setDisable(true);
			//send 버튼 이벤트 처리
			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(event->stopClient());
			primaryStage.show();
		}
		
		void displayText(String text) {
			txtDisplay.appendText(text+"\n");
		}

		public static void main(String[] args) {
			launch(args);
		}

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90

'DEV&OPS > Java' 카테고리의 다른 글

byte 와 16진수(Hex) String 변환  (0) 2022.05.26
BASE64 (64진법)  (2) 2022.05.26
NIO TCP Non-Blocking Channel  (0) 2022.05.26
NIO TCP 서버/클라이언트 채팅 프로그램  (0) 2022.05.26
NIO 파일 채널  (0) 2022.05.25