1:1 채팅 (소켓-5):
클라이언트 A ────────── 서버
1:N 채팅 (소켓-6):
클라이언트 A ──┐
클라이언트 B ──┼── 서버 → 모든 클라이언트에게 전달
클라이언트 C ──┘
채팅방처럼 한 명이 말하면 모두에게 전해지는 방식을 브로드캐스트(Broadcast)라고 함.
1:1과 1:N의 핵심 차이
1:1 서버는 클라이언트 하나를 처리하고 끝남
1:N 서버는 클라이언트가 접속할 때마다 새 스레드를 만들어서 처리
1:1 서버:
accept() → Socket → 통신 → 종료
1:N 서버:
while(true) {
accept() → 새 클라이언트 접속
new ClientHandler(socket).start() → 전담 스레드 생성
(다시 accept() 대기) → 다음 클라이언트 기다림
}
브로드캐스트를 위해 모든 클라이언트의 출력 스트림을 한 곳에 모아둠 Vector는 여러 스레드가 동시에 접근해도 안전한 리스트.
일반 ArrayList는 동시에 여러 스레드가 접근하면 데이터가 손상될 수 있음
- 서버 코드
package server.ch04;
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 {
public static final int PORT = 5000;
// 연결된 모든 클라이언트의 출력 스트림을 보관할 자료구조
//Vector는 멀티 스레드 환경에서 안전한 동작을 한다.
private static Vector<PrintWriter> clientWriterList = new Vector<>();
public static void main(String[] args) {
System.out.println("서버 시작 ... ");
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
Socket clientSocket = serverSocket.accept();
//클라이언트 접속 --> 전담 스레드 생성 --> 메인 스레드는 다시 대기
new ClientHandler(clientSocket).start();
System.out.println("클라이언트 접속. 현재 접속자 " + clientWriterList.size() + " 명");
}
} catch (IOException e) {
throw new RuntimeException(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);
//추후 접속자 명수 확인 또는 방송(브로드 캐스트를) 하기 위해 백터 자료구조에 저장할 예정
clientWriterList.add(out);
String message;
while ((message = in.readLine()) != null) {
System.out.println("수신 : " + message);
//받은 메시지를 연결된 모든 클라이언트에게 전송 (브로드캐스트)
broadcast(message);
}
} catch (Exception e) {
System.out.println("누군가 채팅을 종료 했습니다");
//throw new RuntimeException(e);
}finally {
//클라이언트가 강제 종료 및 exit 요청을 했다면 서버에서 관리하고 있는
//백터 안에 나으
clientWriterList.remove(out);//출력 스트림 제거
try {
if(socket != null){
socket.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}//end of run
//방송하기
private void broadcast(String message) {
for (PrintWriter writer : clientWriterList) {
writer.println(message);
}
}
}//end of inner ClientHandler
}//end of class
-클라이언트 코드
package client.ch04;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class ChatClient {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("채팅 이름을 입력하세요 : ");
String name = sc.nextLine();
//강사 ip 주소 : 192.168.4.101
try (Socket socket = new Socket("localhost", 5000)) {
System.out.println(name + " 님, 채팅방 입장 했음 (종료 : exit)");
//소켓에서 연결 할 입력, 출력 스트림 2개가 필요하다.
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//클라이언트에서 키보드에서 값을 입력 받을 스트림이 필요하다
BufferedReader keyboardReader = new BufferedReader(new InputStreamReader(System.in));
//읽기 쓰레드 (서버 측에서 값을 계속 받을 수 있도록 처리)
Thread readThread = new Thread(new Runnable() {
@Override
public void run() {
try {
String serverMessage;
while ((serverMessage = reader.readLine()) != null) {
System.out.println("서버 : " + serverMessage);
}
} catch (IOException e) {
System.out.println("서버측과 연결이 끊어졌습니다");
}
}
});
//쓰기 스레드 생성(클라이언트 측 키보드에서 값을 받아서 서버측으로 전송)
Thread writeThread = new Thread(new Runnable() {
@Override
public void run() {
try {
String clientMessage;
while ((clientMessage = keyboardReader.readLine()) != null) {
if ("exit".equalsIgnoreCase(clientMessage)) {
System.out.println("채팅방 종료");
writer.println("[" + name + "] 님이 퇴장 했습니다");
break;
}
//서버에 메세지 전송
writer.println("[" + name + "]" + clientMessage);
}
} catch (IOException e) {
System.out.println("메세지 전송 중 오류가 발생했습니다");
}
}
});
readThread.start();
writeThread.start();
readThread.join();
writeThread.join();
} catch (UnknownHostException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
클라이언트 A: writerStream.println("[A]: 안녕!")
|
▼
서버 ClientHandler (A 담당 스레드)
in.readLine() → "[A]: 안녕!" 수신
broadcast("[A]: 안녕!")
|
┌─────────────┼────────────────┐
▼ ▼ ▼
A 화면 B 화면 C 화면
[A]: 안녕! [A]: 안녕! [A]: 안녕!
클라이언트 코드 리팩토링 - 추상 클래스 활용
-추상 클라이언트 코드
package client.ch05;
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 writerStream;
private BufferedReader readerStream;
private BufferedReader keyboardReader;
public AbstractClient(String name) {
this.name = name;
}
// 실행 흐름 정의 (자식 클래스에서 재정의 불가) final
public final void run() {
try {
connectToServer();
setupStreams();
startCommunication();
} catch (Exception e) {
e.printStackTrace(); // 스택구조 예외를 추척하게 하는 부분을 출력
} finally {
// 최종에는 socket.close() 처리
cleanUp();
}
}
// AbstractClient 상속 받은 자식 클래스는 무조건 connectToServer()메서드를 재 정의 해야 함.
protected abstract void connectToServer() throws IOException;
private void setupStreams() throws IOException {
writerStream = new PrintWriter(socket.getOutputStream(), true);
readerStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
keyboardReader = new BufferedReader(new InputStreamReader(System.in));
}
private void startCommunication() throws InterruptedException {
Thread readThread = new Thread(new Runnable() {
@Override
public void run() {
try {
String msg;
while ( (msg = readerStream.readLine()) != null ) {
System.out.println(msg);
}
} catch (Exception e) {
System.out.println("서버와의 연결이 끊겼습니다.");
}
}
});
// 키보드에서 값을 받아서 소켓으로 서버에 메세지 전송
Thread writeThread = new Thread(new Runnable() {
@Override
public void run() {
try {
String input;
while ( (input = keyboardReader.readLine()) != null ) {
writerStream.println("[" + name + "]" + input);
}
} catch (Exception e) {
System.out.println("전송 중 오류 발생");
}
}
});
readThread.start();
writeThread.start();
readThread.join();
writeThread.join();
} // end of startCommunication
// 외부에서 메서드를 통해서 주소값을 주입 받음
protected void setSocket(Socket socket) {
this.socket = socket;
}
private void cleanUp() {
try {
if( socket != null) {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
-클라이언트 코드
package client.ch05;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
public class ChatClient extends AbstractClient {
public ChatClient(String name) {
// 부모 클래스에 사용자 정의 생성자가 있다면 자식 클래스 생성자에서 가장 먼저
// 부모 생성자를 호출 해야 한다.
super(name);
}
// 서버에 연결 방법만 직접 구현
@Override
protected void connectToServer() throws IOException {
// Socket socket = new Socket("localhost", 5000);
// setSocket(socket);
// 부모 클래스 멤버 변수인 socket 에 객체의 주소값을 할당 하지 않으면
// nullPointerException 발생 함.
setSocket(new Socket("localhost", 5000));
}
// 실행 하기
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("이름을 입력하세요 : ");
String name = sc.nextLine();
// ChatClient chatClient = new ChatClient(name);
// chatClient.run();
new ChatClient(name).run();
}
}
리펙토링 전 후
리팩토링 전:
ChatClient 가 스트림 설정, 스레드 생성, 종료 처리를 모두 직접 관리
리팩토링 후:
AbstractClient 가 공통 로직 담당 (스트림, 스레드, 종료)
ChatClient 는 connectToServer() 구현만 담당
→ 나중에 다른 서버에 연결하는 클라이언트를 만들 때도 AbstractClient 재사용 가능
핵심 요약
1:N 서버 핵심 구조
while(true) { new ClientHandler(accept()).start() }
→ 클라이언트마다 전담 스레드 생성
브로드캐스트
clientWriters (Vector) 에 모든 출력 스트림 보관
메시지 수신 시 전체에게 전달
Vector
멀티스레드 환경에서 안전한 리스트
여러 스레드가 동시에 add/remove 해도 안전
AbstractClient (리팩토링)
OOP 추상 클래스로 공통 로직 분리
connectToServer() 만 구현하면 다른 서버에도 재사용 가능