1: N 소켓 양방향 통신 - 25

목차

    1: N 소켓 양방향 통신

    1:N 소켓 양방향 통신에서는 하나의 서버가 여러 클라이언트와 동시에 양방향 통신을 할 수 있습니다. 이는 채팅 애플리케이션, 멀티플레이어 게임 서버 등에서 흔히 사용됩니다.

    1. 필요 개념

    1. 서버와 클라이언트 소켓:
      • 서버는 하나의 ServerSocket을 통해 여러 클라이언트의 연결 요청을 기다린다.
      • 클라이언트는 각각의 Socket을 통해 서버에 연결을 요청하고, 연결된 후 서버와 통신한다.
    2. 멀티스레딩:
      • 서버는 각 클라이언트와의 통신을 별도의 스레드에서 처리한다. 이를 통해 여러 클라이언트와 동시에 통신할 수 있다.
      • 각 클라이언트는 서버와의 통신을 처리하는 자체 스레드를 가진다.
    3. 동기화 및 자원 관리:
      • 여러 스레드가 동시에 데이터를 읽고 쓸 수 있으므로, 데이터의 일관성을 유지하기 위한 동기화가 필요하다.
      • 서버는 연결된 클라이언트 소켓을 관리하고, 클라이언트가 연결을 끊을 때 자원을 적절히 해제해야 한다.
    4. 데이터 송수신:
      • 서버와 클라이언트는 서로 데이터를 주고받을 수 있어야 한다. 이를 위해 입력 스트림과 출력 스트림을 사용한다.

    1:N 소켓 양방향 통신의 개념을 시각적으로 표현

    +---------------------+          +---------------------+
    |      서버          |           |     클라이언트 1    |
    | +---------------+  |           | +---------------+  |
    | | ServerSocket  |  |           | |     Socket    |  |
    | +-------+-------+  |           | +-------+-------+  |
    |         |          |           |         |          |
    |         v          |           |         v          |
    | +-------+-------+  |           | +-------+-------+  |
    | |  Socket[1]    |  |<--------->| |  Network    |    |
    | +-------+-------+  |           | +---------------+  |
    | |  Socket[2]    |  |           +---------------------+
    | +-------+-------+  |
    | |  Socket[3]    |  |           +---------------------+
    | +-------+-------+  |           |     클라이언트 2    |
    |        ...         |           | +---------------+   |
    | +-------+-------+  |           | |     Socket    |   |
    | |  Socket[N]    |  |<--------->| +-------+-------+   |
    | +---------------+  |           |         |           |
    +--------------------+           |         v           |
                                     | +-------+-------+   |
                                     | |  Network      |   |
                                     | +---------------+   |
                                      +---------------------+

    2. MultiClient 처리 (server측 코드)

    package ch06;
    
    import java.io.BufferedReader;
    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) {
    			e.printStackTrace();
    		}
    
    	} // 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;
    		}
    		@Override
    		public void run() {
    			try {
    				in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    				out = new PrintWriter(socket.getOutputStream(), true);
    				
    				// 여기서 중요! - 서버가 관리하는 자료구조에 자원 저장(클라이언트와 연결된 소켓 -> outStream)
    				clientWriters.add(out);
    				String msg;
    				while ((msg = in.readLine()) != null) {
    					System.out.println("Recevied : " + msg);
    					// 받은 데이터를 서버측과 연결된 데이터를 전달하자.
    					broadcastMessage(msg);
    				}
    			} catch (Exception e) {
    				//e.printStackTrace();
    			} finally {
    				try {
    					socket.close();
    					System.out.println("연결 해제...");
    				} catch (Exception e2) {
    					//e2.printStackTrace();
    				}
    			}
    		}
    	} // end of ClientHandler
    	
    	// 모든 클라이언트에게 메시지 보내기 - 브로드캐스트
    	private static void broadcastMessage(String message) {
    		for (PrintWriter writer : clientWriters) {
    			writer.println(message);
    		}
    	}
    }

     

    Vector 클래스는 자바의 java.util 패키지에 포함된 동기화된 리스트 구현체이다.

    Vector는 동기화된 메서드를 제공하여 멀티스레드 환경에서 안전하게 사용할 수 있다. 그러나 이러한 동기화 메서드는 성능에 영향을 미칠 수 있다.

     

    ConcurrentHashMap vs HashMap vs Hashtable

    1. HashMap
      • 비동기화된 맵 구현으로, 단일 스레드 환경에서 사용된다.
      • 스레드 안전하지 않기 때문에 멀티스레드 환경에서는 사용하면 안된다.
    2. Hashtable
      • 동기화된 맵 구현으로, 모든 메서드가 동기화되어 있다.
      • 동기화 메서드 사용으로 인해 성능 저하가 발생할 수 있다.
    3. ConcurrentHashMap
      • 동시성 제어가 추가된 고성능 맵 구현이다.
      • 내부적으로 세분화된 잠금을 사용하여 높은 동시성을 제공한다.
      • 멀티스레드 환경에서 가장 적합한 선택이다.

    사용법 확인

    import java.util.concurrent.ConcurrentHashMap;
    
    public class ConcurrentHashMapExample {
        public static void main(String[] args) {
            // ConcurrentHashMap 생성
            ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
    
            // 키-값 쌍 추가
            map.put("one", 1);
            map.put("two", 2);
            map.put("three", 3);
    
            // 값 가져오기
            System.out.println("Value for 'one': " + map.get("one"));
            System.out.println("Value for 'two': " + map.get("two"));
    
            // 키 확인
            System.out.println("Map contains key 'three': " + map.containsKey("three"));
    
            // 값 제거
            map.remove("two");
            System.out.println("Map contains key 'two' after removal: " + map.containsKey("two"));
    
            // 모든 키 출력
            for (String key : map.keySet()) {
                System.out.println("Key: " + key + ", Value: " + map.get(key));
            }
        }
    }
    • ConcurrentHashMap.newKeySet()은 내부적으로 ConcurrentHashMap을 사용하여 스레드 안전한 Set 을 생성한다.
    • Set 은 여러 스레드가 동시에 접근하거나 수정할 수 있는 환경에서 안전하게 사용할 수 있다.
    • Set 의 모든 수정 연산은 내부적으로 ConcurrentHashMap의 동시성 제어 메커니즘을 사용하여 높은 성능과 스레드 안전성을 제공한다.
    Set<PrintWriter> clientWriters = ConcurrentHashMap.newKeySet();

    일반적인 Set 자료 구조를 사용하는 것이 아니라, ConcurrentHashMap 의 특성을 지닌 스레드 안전한 Set 을 만드는 개념이다. 이를 통해 ConcurrentHashMap의 내부 구조와 동시성 제어 메커니즘을 활용하여 높은 성능과 스레드 안전성을 갖춘 Set을 생성할 수 있다.


    3. 클라이언트 만들기

    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 (InterruptedException e) {
    		}
    	}
    	
    	private Thread createWriteThread() {
    		return new Thread(() -> {
    			try {
    				String msg;
    				while ((msg = keyboardReader.readLine()) != null) {
    					socketWriter.println("[" + name + "] : " + msg);
    				}
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		});
    	}
    	
    	private Thread createReadThread() {
    		return new Thread(() -> {
    			try {
    				String msg;
    				while ((msg = socketReader.readLine()) != null) {
    					System.out.println("방송 옴 : " + msg);
    				}
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		});
    	}
    	
    	private void cleanup() {
    		if(socket != null) {
    			try {
    				socket.close();
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    
    }
    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 --> 부모 클래스 --> 서버측과 연결된 소켓을 주입해주어야 한다.
    		super.setSocket(new Socket("localhost", 5000));
    		
    	}
    	
    	public static void main(String[] args) {
    		ChatClient chatClient = new ChatClient("홍길동");
    		chatClient.run();
    	}
    }

    Java 유용한 클래스 - 3 으로 돌아가기