Java

[Java] 99. 1:N 양방향 통신

Song hyun 2024. 5. 24. 10:23
728x90
반응형

[Java] 1:N 양방향 통신

1. 서버 측 시나리오 코드

2. 클라이언트 측 시나리오 코드

3. 실행 시나리오 코드


1. 필요 개념

(1) 서버와 클라이언트 소켓

-서버는 하나의 ServerSocket을 통홰 여러 클라이언트의 연결 요청을 기다린다.

-클라이언트는 각각의 Sockert을 통해 서버에 연결을 요청, 연결 후 서버와 통신한다.

 

(2) 멀티 스레딩

-서버는 각 클라이언트와의 통신을 별도의 스레드에서 처리한다. 이를 통해 여러 클라이언트와 동시에 통신할 수 있다.

-각 클라이언트는 서버와의 통신을 처리하는 자체 스레드를 가진다.

 

(3) 동기화 및 자원 관리

-여러 스레드가 동시에 데이터를 읽고 쓸 수 있기에, 데이터의 일관성을 유지하기 위한 동기화가 필요하다.

-서버는 연결된 클라이언트 소켓을 관리하고, 클라이언트가 연결을 끊을 때 자원을 적절히 해제해야 한다.

 

(4) 데이터 송수신

- 서버와 클라이언트는 서로 데이터를 주고 받을 수 있어야 한다. 이를 위해 입력 스트림과 출력 스트림을 사용한다.


 

2. 시나리오 코드 작성 

-위의 개념을 숙지하고 시나리오 코드를 작성해보자.

 

(1) Vector 클래스: java.util 패키지에 포함된 동기화된 리스트 구현체이다. 동기화된 메서드를 제공해 멀티스레드에서 안전하게 사용할 수 있다. (-> 하지만 이런 동기화 메서드는 성능에 영향을 미치기도 한다!)

 

(2) ConcurrentHashMap vs HashMap vs HashTable

-HashMap: 비동기화된 맵 구현으로 단일 스레드 환경에서 사용된다. 

안전하지 않기 때문에 멀티 스레드 환경에서 사용하면 안된다.

-HashTable: 동기화된 맵 구현으로 모든 메서드가 동기화되어 있다.

동기화 메서드 사용으로 성능 저하가 발생할 수 있다.

- ConcurrentHashMap: 동시성 제어가 추가된 고성능 맵 구현이다.

내부적으로 세분화된 잠금을 사용해 높은 동시성을 제공하고,  멀티스레드 환경에서 적합하다.


1. 서버 측 시나리오 코드

(1) 상수 PORT=5000을 선언, 초기화한다. (=포트 번호)
(2) 멀티 스레드의 안정적인 동작을 위해 Vector(PrintWriter 형) 객체 clientWriters를 만든다.
1:n 소켓 양방향 통신에서는 서버가 하나이지만, 여러명의 클라이언트들이 존재하게 된다.

(3) main Thread
-5000이라는 포트 번호와 연결되는 serverSocket을 생성한다.
-serverSocket.accept()를 새로운 Socket 객체에 초기화한다.(=클라이언트와 이어지는 socket 생성)
-생성된 ClientHandler 객체의 파라미터로 socket을 넣고, .start()를 실행한다.

(4) ClientHander 클래스 (extends Thread)
-지역변수로 socket, out(PrintWriter), in(BufferedReader)를 선언한다.
-사용자 정의 생성자를 이용해, 파라미터 내의 socket을 지역변수 socket에 초기화한다.

(5) run() 메서드(=.start() 시 실행되는 메서드)
-in에 socket.getInputStream-inputStreamReader-BufferedReader를 초기화한다.
즉 in은 클라이언트로부터 받은 데이터를 한 줄 씩 출력하게 된다.
-out에 socket.getOutputStream(),true-PrintWriter를 초기화한다.
즉, out은 서버가 출력한 데이터를 클라이언트에게 넘겨주게 된다.

-클라이언트들을 담는 clientWriters에 .add(out)을 실행함으로써 클라이언트들에게 메세지를 보내게 된다.
-while문을 이용해 클라이언트들이 메세지를 보낼 때까지 대기하게 하고, 만약 클라이언트들이 메세지를 보내면 해당 데이터를 message에 담아 broadcastMessage의 파라미터값에 넣게 된다!
-try-catch-finally를 통해 socket을 닫는다.

(5) broadCastMessage() 메서드 : 모든 클라이언트에게 메세지 보내기
-for문을 사용해, client의 수만큼, 각각의 클라이언트들에게 메세지를 보내게 된다.

package ch06;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Vector;

public class MultiClientServer {

	private static final int PORT = 5000; 
	// 하나의 변수에 자원을 통으로 관리하기 기법 --> 자료구조
	// 자료구조 ---> 코드 단일, 멀티 ---> 멀티 스레드 --> 자료 구조 ??
	// 객체 배열 <-- Vector<> : 멀티 스레드에 안정적이다. 
	private static Vector<PrintWriter> clientWriters = new Vector<>();
	
	
	public static void main(String[] args) {
		System.out.println("Server started....");
		
		try (ServerSocket serverSocket = new ServerSocket(PORT)){
			
			while(true) {
				// 1. serverSocket.accept() 호출하면 블록킹 상태가 된다. 멈춰있음 
				// 2. 클라이언트가 연결 요청하면 새로운 소켓 객체 생성이 된다. 
				// 3. 새로운 스레드를 만들어 처리 ... (클라이언트가 데이터를 주고 받기 위한 스레드) 
				// 4. 새로운 클라이언트가 접속 하기 까지 다시 대기 유지(반복) 
				Socket socket = serverSocket.accept();
				
				// 새로운 클라이언트가 연결 되면 새로운 스레가 생성된다. 
				new ClientHandler(socket).start();  
				
			}
			
		} catch (Exception e) {
			
		}

	} // end of main
	
	// 정적 내부 클래스 설계 
	private static class ClientHandler extends Thread {
		
		private Socket socket;
		private PrintWriter out; 
		private BufferedReader in; 
		
		public ClientHandler(Socket socket) {
			this.socket = socket;
		}
		
		// 스레드 start() 호출시 동작 되는 메서드 - 약속 
		@Override
		public void run() {
			
			try {
				in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				out = new PrintWriter(socket.getOutputStream(), true);
				
				// 여기서 중요 ! - 서버가 관리하는 자료구조에 자원 저장(클,연결된 소켓->outStream)   
				clientWriters.add(out);
				String message; 
				while( (message = in.readLine() ) != null  ) {
					System.out.println("Received : " + message);
					broadcastMessage(message);
				}
		
				
			} catch (Exception e) {
				//e.printStackTrace();
			} finally {
				try {
					socket.close();
					System.out.println("...... 클라이언트 연결 해제 ....... ");
				} catch (IOException e) {
					//e.printStackTrace();
				}
			}
		}
	}  // end of ClientHandler
	
	// 모든 클라이언트에게 메시지 보내기- 브로드캐스트 
	private static void broadcastMessage(String message) {
		for(PrintWriter writer : clientWriters) {
			writer.println(message);
		}
	}

}

 

2. 클라이언트 측 시나리오 코드

package ch06;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public abstract class AbstractClient {

	
	private String name;
	private Socket socket;
	private PrintWriter socketWriter;
	private BufferedReader socketReader;
	private BufferedReader keyboardReader;
	
	public AbstractClient(String name) {
		this.name=name;
	}
	
	// 외부에서 나의 멤버 변수에 참조 변수를 주입받을 수 있도록 setter 메서드 설계
	
	protected void setSocket(Socket socket) {
		this.socket=socket;
	}
	
	public final void run() {
		try {
			connectToServer();
			setupStreams();
			startService(); // join() 걸어둔 상태
		} catch (IOException e) {
			System.out.println(">>> 접속 종료 <<<");
		} finally {
			cleanup();
		}
	}
	
	protected abstract void connectToServer() throws IOException;
	
	private void setupStreams() throws IOException{
		socketReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		socketWriter = new PrintWriter(socket.getOutputStream(),true);
		keyboardReader = new BufferedReader(new InputStreamReader(System.in));
	}
	
	private void startService() throws IOException{
		Thread readThread = createReadThread();
		Thread writeThread = createWriteThread();
		
		// 스레드 시작
		readThread.start();
		writeThread.start();
		
		// 메인 스레드 대기 처리
		try {
			readThread.join();
			writeThread.join();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private Thread createWriteThread() {
		return new Thread(()->{
			try {
				String msg;
				while((msg=keyboardReader.readLine())!=null) {
					socketWriter.println("["+name+"] : "+msg);
				}	
			} catch (Exception e) {
				e.printStackTrace();
			}
			
		});
	}
	
	
	private Thread createReadThread() {
		return new Thread(()->{
			try {
				String msg;
				while((msg=socketReader.readLine())!=null) {
					System.out.println("방송옴 : "+msg);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
	}
	
	private void cleanup() {
		if(socket!=null){
			try {
				socket.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}
	
	
}

 

 

3. 실행 시나리오 코드

package ch06;

import java.io.IOException;
import java.net.Socket;

public class ChatClient extends AbstractClient {

	public ChatClient(String name) {
		super(name);
		
	}

	@Override
	protected void connectToServer() throws IOException {
		// AbstractClient는 부모 클래스이다.
		// AbstractClient가 작동하려면, 서버측과 연결된 소켓을 주입해야 한다.
		super.setSocket(new Socket("192.168.0.48",5000));
	}
	
	public static void main(String[] args) {
		ChatClient chatClient = new ChatClient("1");
		chatClient.run();
	}

}

 

728x90
반응형