1:N 소켓 양방향 통신에서는 하나의 서버가 여러 클라이언트와 동시에 양방향 통신이 가능하다.
이는 채팅 애플리케이션, 멀티플레이어 게임 서버 등에서 흔히 사용된다.
개념
1. 서버와 클라이언트 소켓 :
- 서버는 하나의 ServerSocket을 통해 여러 클라이언트의 연결 요청을 기다린다.
- 클라이언트는 각각의 Socket을 통해 서버에 연결을 요청하고, 연결된 후 서버와 통신한다.
2. 멀티스레딩 :
- 서버는 각 클라이언트와의 통신을 별도의 스레드에서 처리한다. 이를 통해 여러 클라이언트와 동시에 통신할 수 있다.
- 각 클라이언트는 서버와의 통신을 처리하는 자체 스레드를 가진다.
3. 데이터 송수신 :
- 서버와 클라이언트는 서로 데이터를 주고받을 수 있어야 한다. 이를 위해 입력 스트림과 출력 스트림을 사용한다.
1:N 소켓 양방향 통신의 시각적 개념 표현
+---------------------+ +---------------------+
| 서버 | | 클라이언트 1 |
| +---------------+ | | +---------------+ |
| | ServerSocket | | | | Socket | |
| +-------+-------+ | | +-------+-------+ |
| | | | | |
| v | | v |
| +-------+-------+ | | +-------+-------+ |
| | Socket[1] | |<--------->| | Network | |
| +-------+-------+ | | +---------------+ |
| | Socket[2] | | +---------------------+
| +-------+-------+ |
| | Socket[3] | | +---------------------+
| +-------+-------+ | | 클라이언트 2 |
| ... | | +---------------+ |
| +-------+-------+ | | | Socket | |
| | Socket[N] | |<--------->| +-------+-------+ |
| +---------------+ | | | |
+--------------------+ | v |
| +-------+-------+ |
| | Network | |
| +---------------+ |
+---------------------+
1단계 - MultiClient 처리 - 단, 브로드캐싱은 되지 않음 (서버 측 코드)
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<>();
//private static Set<PrintWriter> clientWriters = ConcurrentHashMap.newKeySet(); // 스레드 안전한 클라이언트 출력 스트림 집합
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);
}
}
}
Vector 클래스는 자바의 java.util 패키지에 포함된 동기화된 리스트 구현체이다. Vector는 동기화된 메서드를 제공하여 멀티스레드 환경에서 안전하게 사용할 수 있다. 그러나 이러한 동기화 메서드는 성능에 영향을 미칠 수 있다.
ConcurrentHashMap vs HashMap vs Hashtable
1. HashMap
- 비동기화 된 맵 구현으로, 단일 스레드 환경에서 사용된다.
- 스레드가 안전하지 않기 때문에 멀티스레드 환경에서는 사용하지 않아야 한다.
2. Hashtable
- 동기화 된 맵 구현으로, 모든 메서드가 동기화 되어 있다.
- 동기화 메서드 사용으로 인해 성능의 저하가 발생할 수 있다.
3. CurrentHashMap
- 동시성 제어가 추가된 고성능 맵 구현이다.
- 내부적으로 세분화된 잠금을 사용해 높은 동시성을 제공한다.
- 멀티스레드 환경에서 가장 적합한 선택이다.
사용법 확인
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을 생성할 수 있다.
클라이언트 만들기
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;
}
public final void fun() {
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();
}
}
}
}
'Java' 카테고리의 다른 글
| JSP와 MVC 패턴 Todo 프로젝트 (0) | 2024.07.12 |
|---|---|
| 네트워크 프로토콜 (0) | 2024.07.12 |
| 1:1 양방향 통신(채팅 기본 기능 구현) (0) | 2024.07.12 |
| 간단한 게시판 만들기 (0) | 2024.07.10 |
| 서블릿과 JSP의 개념과 차이점 (0) | 2024.07.10 |