TCP - Multicasting

heeezni·2025년 6월 17일

Java GUI 프로젝트

목록 보기
20/20
post-thumbnail

Mutlicasting

  • TCP 기반으로 멀티캐스팅을 흉내내는 구조

TCP (Transmission Control Protocol)

  • 컴퓨터들끼리 신뢰성 있게 데이터를 주고받기 위한 통신 규칙(프로토콜)
  • 유니캐스트(1:1 전송)에 특화된 프로토콜
    ➡ 서버가 접속한 모든 클라이언트 소켓을 기억(Vector) 해두고, 받은 메시지를 반복문으로 모든 클라이언트에게 보내면 (send())
    마치 "멀티캐스팅처럼 보이는 효과"를 만들 수 있음

Server

public class GUIServer extends JFrame {

	...
	Thread thread; // 서버 가동용 스레드 (메인스레드가 accept() 대기상태에 빠지지 않도록 분리 실행)
	Vector<ServerChatThread> vec = new Vector<>(); // ✅ 접속 중인 클라이언트들의 스레드를 저장 (현재는 0)

	...
	public void startServer() {
		int port = Integer.parseInt(t_port.getText());
		try {
			ServerSocket server = new ServerSocket(port);
			area.append("서버 생성 및 접속자 감지 시작\n");

			while (true) {
				Socket socket = server.accept(); // 클라이언트가 접속할 때까지 블로킹 대기
				String ip = socket.getInetAddress().getHostAddress();
				area.append(ip + "님 접속 감지\n");

				// 클라이언트와의 통신을 전담할 스레드 생성
				ServerChatThread chatThread = new ServerChatThread(this, socket);
				chatThread.start(); // 별도 스레드로 실행

				// ✅ 현재 접속한 클라이언트 스레드를 목록(Vector)에 추가
				vec.add(chatThread);
				area.append("현재 " + vec.size() + "명 접속\n");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

ArrayList vs Vector

  • ArrayList도 가능은 하지만, 다중 스레드 환경에서는 동기화를 지원하지 않기 때문에 여러 스레드가 동시에 같은 인덱스를 조작하면 충돌 위험이 있음
    ➡ 개발자가 직접 synchronized {} 블럭으로 감싸서 동기화 코드를 작성해야 함

  • Vector는 내부적으로 동기화(synchronized)가 적용된 클래스이므로
    별도 동기화 없이 멀티스레드 환경에서도 안전하게 사용할 수 있음

ServerChatThread

// 서버에 접속한 모든 클라이언트에게 메시지 전달 (브로드캐스트)
for (int i = 0; i < guiserver.vec.size(); i++) {
	ServerChatThread st = guiserver.vec.get(i);
	st.send(msg); // 각 클라이언트에게 전송
}

서버에 접속한 모든 유저와 1:1 대응하는 ServerChatThread수 만큼 반복하면서 메시지를 보내자!


ClientChatThread

클라이언트 측에서 서버 메시지를 실시간으로 수신하기 위한 스레드 클래스

  • listen : 사용자가 메시지를 보내지 않아도 서버의 메시지를 계속 청취해야 하므로 무한루프 필요
  • send() : 사용자가 직접 입력할 때만 호출됨
public class ClientChatThread extends Thread {
	Client client; // GUI 요소를 제어하기 위한 참조 (ex: JTextArea 등)
	Socket socket;
	BufferedReader buffr; // 서버로부터 수신
	BufferedWriter buffw; // 서버로 메시지 전송

	public ClientChatThread(Client client, Socket socket) {
		this.client = client;
		this.socket = socket;

		try {
			// 소켓에서 입력/출력 스트림을 뽑아서 버퍼 연결
			buffr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			buffw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// 사용자가 입력한 메시지를 서버로 전송 (사용자가 보내기 원할 때만)
	public void send(String msg) {
		try {
			buffw.write(msg + "\n");
			buffw.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// 서버에서 보낸 메시지를 실시간으로 수신하여 GUI에 출력 
	public void listen() {
		String msg = null;
		try {
			msg = buffr.readLine(); // 서버가 보낸 메시지 수신
			client.area.append(msg + "\n"); // GUI에 출력
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// ✅ 스레드가 시작되면 무한 루프로 listen() 실행
	@Override
	public void run() {
		while (true) {
			listen();
		}
	}
}

Client

// 서버에 접속하는 메서드: IP와 포트번호로 소켓을 생성한다
public void connectServer() {
	String ip = (String) cb_ip.getSelectedItem(); // 콤보박스에서 선택된 서버 IP
	int port = Integer.parseInt(t_port.getText()); // 입력된 포트 번호

	try {
		// 서버에 연결 시도 → 성공하면 서버와의 통신을 위한 소켓 생성됨
		Socket socket = new Socket(ip, port);

		// ✅ 접속 이후부터는 메시지 수신을 별도 스레드가 담당해야 하므로,
		// 소켓을 ClientChatThread에 넘겨주고 스레드 실행
		clientThread = new ClientChatThread(this, socket);
		clientThread.start(); // 내부 run()에서 listen() 무한 실행됨

	} catch (UnknownHostException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

접속 이후부터 채팅은 스레드가 담당하므로, 소켓을 스레드에 전달해주자!

// 텍스트 입력창(t_input)에 엔터 키 이벤트 등록
t_input.addKeyListener(new KeyAdapter() {
	@Override
	public void keyReleased(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_ENTER) {
			// 사용자가 엔터를 누르면 현재 입력값을 서버로 전송
			clientThread.send(t_input.getText());
			t_input.setText("");
		}
	}
});
  • clientThread.start()listen()을 무한 루프로 돌려서 서버 메시지를 계속 수신
  • send()는 엔터를 눌렀을 때만 1회 호출되어 입력값을 서버로 전송

채팅방 나가기 처리: readLine() 예외 + 스레드 종료

누가 채팅방을 나가면
ServerChatThread의 readLine()부분이 에러가 남
➡ flag방식으로 해결하기

// 클래스 멤버에 추가
private boolean isRunning = true;

@Override
public void run() {
	while (isRunning) { // 내부에서 isRunning=false가 되면 루프 종료됨
		listen();
	}
}

// 수신
public void listen() {
	String msg = null;
	try {
		msg = buffr.readLine(); // 클라이언트가 보낸 메시지 수신
		if (msg == null) throw new IOException("클라이언트 연결 끊김");

		guiserver.area.append(msg + "\n");

		// 전체 클라이언트에게 메시지 전송
		for (int i = 0; i < guiserver.vec.size(); i++) {
			ServerChatThread st = guiserver.vec.get(i);
			st.send(msg);
		}
	} catch (IOException e) {
		// ⛔ 예외 발생 시 실행
		isRunning = false; // ✅ 루프 종료 유도
		guiserver.vec.remove(this); // ✅ 나(서버 스레드)는 해당 클라이언트 전용이므로, 
        // 연결 종료 시 접속자 목록에서 제거
		guiserver.area.append("현재 접속자 " + guiserver.vec.size() + " 명\n");
		System.out.println("어! 누구 나갔다");
	}
}
profile
아이들의 가능성을 믿었던 마음 그대로, 이제는 나의 가능성을 믿고 나아가는 중입니다.🌱

0개의 댓글