본문 바로가기

BackEnd/Java

[java] 이것이 자바다 ch19 네트워크 입출력(UDP, 동시 요청 처리)

728x90
반응형

4. UDP

   ▷ 발신자가 일방적으로 수신자에게 데이터를 보내는 방식. TCP처럼 연결 요청 및 수락 과정이 없기 때문에 TCP보다 데이터 전송 속도가 상대적으로 빠릅니다.

   ▷ 데이터 전달의 신뢰성보다 속도가 중요하다면 UDP를 사용하고, 데이터 전달의 신뢰성이 중요하다면 TCP를 사용합니다.

   ▷ DatagramSocket은 발신점과 수신점에 해당하고 DatagramPacket은 주고받는 데이터에 해당합니다.

 

◎ UDP 서버

1. DatagramSocket 객체를 생성할 때에는 다음과 같이 바인딩할 Port 번호를 생성자 매개값으로 제공
DatagramSocket datagramSocket = new DatagramSocket(50001);​

2. receiver() 메소드는 데이터를 수신할 때까지 블로킹되고, 데이터가 수신되면 매개값으로 주어진 DatagramPacket에 저장
DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
datagramSocket.receive(receivePacket);​

 

3. DatagramPacket 생성자의 첫 번째 매개값은 수신된 데이터를 저장할 배열이고 두 번째 매개값은 수신할 수 있는 최대 바이트 수
byte[] bytes = receivePacket.getData();
int num = receivePacket.getLength();

String data = new String(bytes, 0, num, "UTF-8");​


4. getSocketAddress() 메소드를 호출하면 정보가 담긴 SocketAddress 객체를 얻을 수 있음

SocketAddress socketAddress = receivePacket.getSocketAddress();


5. SocketAddress 객체는 클라이언트로 보낼 DatagramPacket을 생성할 때 네 번째 매개값으로 사용

String data = "처리 내용";
byte[] bytes = data.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket(bytes, 0, bytes.length, socketAddress);


6. DatagramPacket을 클라이언트로 보낼 때는 DatagramSocket의 send() 메소드를 이용

datagramSocket.send(sendPacket);


7. UDP 서버를 종료하고 싶을 경우에는 DatagramSocket의 close() 메소드를 호출

datagramSocket.close();

 

◎ UDP 서버 예제

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.Scanner;

public class NewsServer extends Thread {
	private static DatagramSocket datagramSocket = null;

	public static void main(String[] args) throws Exception {
		System.out.println("---------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요");
		System.out.println("---------------------------------------");
		
		// UDP 서버 시작
		startServer();
		
		// 키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
		// TCP 서버 종료
		stopServer();
	}
	
	public static void startServer() {
		// 작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					// DatagramSocket 생성 및 Port 바인딩
					datagramSocket = new DatagramSocket(50001);
					System.out.println("[서버] 시작됨");
					
					while(true) {
						// 클라이언트가 구독하고 싶은 뉴스 주제 얻기
						DatagramPacket receivePacket = new DatagramPacket
								(new byte[1024], 1024);
						datagramSocket.receive(receivePacket);
						String newsKind =
								new String(receivePacket.getData(), 0,
								receivePacket.getLength(), "UTF-8");
						
						// 클라이언트의 IP와 Port 얻기
						SocketAddress socketAddress = receivePacket.getSocketAddress();
						
						
						// 10개의 뉴스를 클라이언트로 전송
						for(int i=1; i<=10; i++) {
							String data = newsKind + ": 뉴스" + i;
							byte[] bytes = data.getBytes("UTF-8");
							DatagramPacket sendPacket =
								new DatagramPacket(bytes, 0, bytes.length, socketAddress);	
							datagramSocket.send(sendPacket);
						}
					}
				} catch (Exception e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}
		};
		// 스레드 시작
		thread.start();
	}
	public static void stopServer() {
		// DatagramSocket을 닫고 Port 언바인딩
		datagramSocket.close();
		System.out.println("[서버] 종료됨 ");
	}
}

//	출력 : 
//	---------------------------------------
//	서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요
//	---------------------------------------
//	[서버] 시작됨

 

 

◎ UDP 클라이언트

   ▷ 서버에 요청 내용을 보내고 그 결과를 받는 역할
   ▷ UDP 클라이언트를 위한 DatagramSocket 객체는 기본 생성자로 생성. Port 번호는 자동 부여
String data = "요청 내용";
byte[] bytes = data.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket(
	bytes, bytes.length, new InetSocketAddress("localhost", 50001)
);​

   ▷ 생성된 DatagramPacket을 매개값으로해서 DatagramSocket의 send() 메소드를 호출하면
        UDP 서버로 DatagramPacket이 전송
datagramSocket.send(sendPacket);​


   ▷ DatagramSocket을 닫으려면 close() 메소드를 호출
atagramSocket.close();​

 

◎ UDP 클라이언트 예제

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

public class NewsClient {

	public static void main(String[] args) {
		try {
			// DatagramSocket 생성
			DatagramSocket datagramSocket = new DatagramSocket();
			
			// 구독하고 싶은 뉴스 주제 보내기
			String data = "정치";
			byte[] bytes = data.getBytes("UTF-8");
			DatagramPacket sendPacket = new DatagramPacket(
				bytes, bytes.length, new InetSocketAddress("localhost", 50001)
			);
			datagramSocket.send(sendPacket);
			
			while(true) {
				// 뉴스 받기
				DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
				datagramSocket.receive(receivePacket);
				
				// 문자열로 변환
				String news = new String(receivePacket.getData(), 0,
						receivePacket.getLength(), "UTF-8");
				System.out.println(news);
				
				// 10번째 뉴스를 받으면 while문 종료
				if(news.contains("뉴스10")) {
					break;
				}
			}
			
			// DatagramSocket 닫기
			datagramSocket.close();
		} catch(Exception e) {
		}
	}
}

//	출력 : 
//	정치: 뉴스1
//	정치: 뉴스2
//	정치: 뉴스3
//	정치: 뉴스4
//	정치: 뉴스5
//	정치: 뉴스6
//	정치: 뉴스7
//	정치: 뉴스8
//	정치: 뉴스9
//	정치: 뉴스10


Server를 먼저 실행시켜야 Client를 실행할 수 있습니다.

UDP는 발신자가 수신자에게 데이터를 일방적으로 보내기 때문에 전송 속도가 빠릅니다.
하지만 신뢰성은 TCP보다 낮습니다. 즉, 정확도가 낮다는 말입니다!!

 

 

5. 서버의 동시 요청 처리

◎ 서버의 동시 요청 처리

   ▷ 일반적으로 서버는 다수의 클라이언트와 통신입니다.

   ▷ 서버는 클라이언트들로부터 동시에 요청을 받아서 처리하고, 처리 결과를 개별 클라이언트로 보내줍니다.

 

   ▷ accept()와 receive()를 제외한 요청 처리 코드를 별도의 스레드에서 작업합니다.

 

스레드를 처리할 때 클라이언트의 폭증으로인한 서버의 과도한 스레드 생성을 방지하기 위해 스레드풀을 사용하는 것이 바람직합니다.

스레드풀은 작업 처리 스레드 수를 제한해서 사용하기 떄문에 갑작스런 클라이언트 폭증이 발생해도 크게 문제가 되지 않습니다. 단, 스레드풀 내의 작업 큐의 대기 작업 증가되어 클라이언트에서 응답을 늦게 받을 수 있습니다.

 

 

◎ TCP EchoServer 동시 요청 처리

   ▷ 스레드풀을 이용해서 클라이언트의 요청을 동시에 처리

 

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EchoServer {
	private static ServerSocket serverSocket = null;
	private static ExecutorService executorService = Executors.newFixedThreadPool(10);
	
	public static void main(String[] args) {
		System.out.println("---------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요");
		System.out.println("---------------------------------------");
		
		// TCP 서버 시작
		startServer();
		
		// 키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
		// TCP 서버 종료
		stopServer();
	}
	
	public static void startServer() {
		// 작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					// ServerSocket 생성 및 Port 바인딩
					serverSocket = new ServerSocket(50001);
					System.out.println("[서버] 시작됨");
					
					// 연결 수락 및 데이터 통신
					while(true) {
						// 연결 수락
						Socket socket = serverSocket.accept();
						
						executorService.execute(() ->{
							try {
								// 연결된 클라이언트 정보 얻기
								InetSocketAddress isa =
										(InetSocketAddress) socket.getRemoteSocketAddress();
								System.out.println("[서버] " + isa.getHostName() + "의 연결 요청을 수락함");
								
								// 데이터 받기
								DataInputStream dis = new DataInputStream(socket.getInputStream());
								String message = dis.readUTF();
								
								// 데이터 보내기
								DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
								dos.writeUTF(message);
								dos.flush();
								System.out.println("[서버] 받은 데이터를 다시 보냄: " + message);
								
								// 연결 끊기
								socket.close();
								System.out.println("[서버] " + isa.getHostName() + "의 연결을 끊음");
							} catch(IOException e) {
							}
						});
					}
				} catch(IOException e) {
					System.out.println("[서버] " + e.getMessage());								
				}
			}
		};
		
		// 스레드 시작
		thread.start();
	}
	
	public static void stopServer() {
		try {
			// ServerSocket을 닫고 Port 언바인딩
			serverSocket.close();
			executorService.shutdownNow();
			System.out.println("[서버] 종료됨 ");
		} catch(IOException e1) {}
	}
}

//출력 : 
//---------------------------------------
//서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요
//---------------------------------------
//[서버] 시작됨



아래의 코드들이 추가되어서 클라이언트의 요청을 동시에 처리하도록 했습니다.

// 1. 10개의 스레드로 요청을 처리하는 스레드풀을 생성함
private static ExecutorService executorService = Executors.newFixedThreadPool(10);

// 2. 작업 큐에 처리 작업 넣기, Runnable은 함수형 인터페이스라 람다식으로 표현 가능
executorService.execute(() ->{ ... });

// 3. 스레드풀 종료
executorService.shutdownNow();

 

 

◎ UDP NewsServer 동시 요청 처리

   ▷ 스레드풀을 이용해서 클라이언트의 요청을 동시에 처리

 

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewsServer extends Thread {
	private static DatagramSocket datagramSocket = null;
	private static ExecutorService executorService =
			Executors.newFixedThreadPool(10);

	public static void main(String[] args) throws Exception {
		System.out.println("---------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요");
		System.out.println("---------------------------------------");
		
		// UDP 서버 시작
		startServer();
		
		// 키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
		// TCP 서버 종료
		stopServer();
	}
	
	public static void startServer() {
		// 작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					// DatagramSocket 생성 및 Port 바인딩
					datagramSocket = new DatagramSocket(50001);
					System.out.println("[서버] 시작됨");
					
					while(true) {
						// 클라이언트가 구독하고 싶은 뉴스 주제 얻기
						DatagramPacket receivePacket = new DatagramPacket
								(new byte[1024], 1024);
						datagramSocket.receive(receivePacket);
						
						executorService.execute(() ->{
							try {
								String newsKind =
										new String(receivePacket.getData(), 0,
												receivePacket.getLength(), "UTF-8");
								
								// 클라이언트의 IP와 Port 얻기
								SocketAddress socketAddress = receivePacket.getSocketAddress();
								
								
								// 10개의 뉴스를 클라이언트로 전송
								for(int i=1; i<=10; i++) {
									String data = newsKind + ": 뉴스" + i;
									byte[] bytes = data.getBytes("UTF-8");
									DatagramPacket sendPacket =
											new DatagramPacket(bytes, 0, bytes.length, socketAddress);	
									datagramSocket.send(sendPacket);
								}							
							} catch(Exception e){
							}							
						});
					}
				} catch (Exception e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}
		};
		// 스레드 시작
		thread.start();
	}
	public static void stopServer() {
		// DatagramSocket을 닫고 Port 언바인딩
		datagramSocket.close();
		executorService.shutdownNow();
		System.out.println("[서버] 종료됨 ");
	}
}

//	출력 : 
//	---------------------------------------
//	서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요
//	---------------------------------------
//	[서버] 시작됨

 

앞의 예제와 같이 아래의 코드들이 추가되어서 클라이언트의 요청을 동시에 처리하도록 했습니다.

// 1. 10개의 스레드로 요청을 처리하는 스레드풀을 생성함
private static ExecutorService executorService = Executors.newFixedThreadPool(10);

// 2. 작업 큐에 처리 작업 넣기, Runnable은 함수형 인터페이스라 람다식으로 표현 가능
executorService.execute(() ->{ ... });

// 3. 스레드풀 종료
executorService.shutdownNow();

 

UDP는 발신자가 수신자에게 데이터를 일방적으로 보내기 때문에 전송 속도가 빠르지만 그만큼 데이터 전달이 정확하지 않을 수 있습니다.

 

그리고 스레드풀을 이용하면 이용자 폭증을 조금은 막고 동시에 처리할 수 있겠어요!!

 

많은 분들의 피드백은 언제나 환영합니다! 많은 댓글 부탁드려요~~

 

728x90
반응형