최원종의 개발 블로그

(소켓 - 5) 1:1 실시간 채팅(멀티스레드) 본문

Java/JAVA 유용한 클래스

(소켓 - 5) 1:1 실시간 채팅(멀티스레드)

chl6698 2026. 3. 27. 13:46

양방향 관계 소켓 - 4 의 양방향 통신은 메시지를 한 번만 주고 끝남.

While문의 문제점

while (true) {
    String msg = reader.readLine();  // 상대방 메시지 읽기 → 여기서 멈춤
    writer.println("내 답장");       // 내 메시지 보내기
}

//문제점
읽는 동안에는 쓸 수 없고
쓰는 동안에는 읽을 수 없다.

상대방이 말해야만 내가 말할 수 있는 구조
→ 진짜 채팅처럼 동시에 주고받는 것이 불가능

//해결책
해결책은 읽기와 쓰기를 별도 스레드로 분리하는 것

-서버 측 코드

package server.ch03;

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

public class WhileServer {

    public static void main(String[] args) {

        try (ServerSocket serverSocket = new ServerSocket(5000)) {
            System.out.println("클라이언트에 연결 요청을 기다립니다...");
            Socket clientSocket = serverSocket.accept();
            System.out.println("==========서버 실행===========");

            //소켓과 연결 된 스트림
            BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(),true);

            //키보드와 연결할 스트림
            BufferedReader keyboardReader = new BufferedReader(new InputStreamReader(System.in));

            String line;
            while ((line = reader.readLine()) != null) {

                //클 --->exit <--- EXIT
                //equalsIgnoreCase  대소문자 구분 없이 값 확인
                if ("exit".equalsIgnoreCase(line)) {
                    break; // while 종료
                }
                System.out.println("클라이언트 > " + line);

                //서버 측 컴퓨터의 키보드에서 값을 받아서 클라이언트 측으로 전송
                System.out.print("서버입력 > ");
                //**서버에서 응답을 받아야만 메세지를 보낼 수 있음 **
                String severMsg = keyboardReader.readLine(); //블로킹 상태(콘솔 창에 글 입력해야 함)
                //클라이언트와 연결 된 소켓 출력 스트림을 활용해서 내용을 전달한다.
                writer.println(severMsg);
                //서버측에서도 더이상 글 입력 받기 싫다면 exit입력으로 while문 종료 처리
                if ("exit".equalsIgnoreCase(severMsg)) {
                    break;
                }
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

-클라이언트 측 코드

package client.ch03;

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

public class WhileClient {

    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 5000)) {

            //소켓에서 연결 할 입력, 출력 스트림 2개가 필요하다.

            //클라이언트에서 키보드에서 값을 입력 받을 스트림이 필요하다.
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            //클라이언트에서 키보드에서 값을 입력 받을 스트림이 필요하다
            BufferedReader keyboardReader = new BufferedReader(new InputStreamReader(System.in));

            String line;
            while (true) {
                System.out.print("클라이언트 입력 > ");
                String input = keyboardReader.readLine(); //블로킹 상태
                writer.println(input);
                if ("exit".equalsIgnoreCase(input)) {
                    break;
                }

                //서버측에서 보낸 메세지를 받아서 클라이언트 콘솔창에 출력
                String response = reader.readLine();
                if ("exit".equalsIgnoreCase(response)) {
                    System.out.println("서버측에서 대화 종료 요청");
                    break;
                }
                System.out.println("서버 응답 : " + response);
            }

        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

-채팅 결과


읽기와 쓰기를 별도 스레드로 분리한 코드

메인 스레드
   ├── 읽기 스레드: 상대방 메시지를 계속 기다리며 읽기
   └── 쓰기 스레드: 내 키보드 입력을 계속 기다리며 전송

두 스레드가 동시에 실행 → 읽으면서 동시에 쓸 수 있음

 


-서버 측 코드

package server.ch03;

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

public class MultiThreadedServer {

    public static void main(String[] args) {

        try (ServerSocket serverSocket = new ServerSocket(5000)) {
            System.out.println("클라이언트에 연결 요청을 기다립니다...");
            Socket clientSocket = serverSocket.accept();
            System.out.println("==========서버 실행===========");

            //소켓과 연결 된 스트림
            BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);

            //키보드와 연결할 스트림
            BufferedReader keyboardReader = new BufferedReader(new InputStreamReader(System.in));

            //읽기 스레드 : 클라이언트 메세지를 계속 수신
            Thread readThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        String clientMessage;
                        while ((clientMessage = reader.readLine()) != null) {
                            if ("exit".equalsIgnoreCase(clientMessage)) {
                                System.out.println("클라이언트 종료했습니다");
                                break;
                            }
                            System.out.println("클라이언트 메세지 : " + clientMessage);
                        }
                    } catch (IOException e) {
                        System.err.println("클라이언트와 연결이 끊겼습니다");
                    }

                }
            });

            //쓰기 스레드 : 키보드 입력을 받아 클라이언트에게 전송
            Thread writeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        String serverMessage;
                        while ((serverMessage = keyboardReader.readLine()) != null) {
                            writer.println(">>> " + serverMessage);//"\n" 포함
                            if ("exit".equalsIgnoreCase(serverMessage)) {
                                System.out.println("서버가 종료되었습니다");
                                break;
                            }

                        }
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }


                }
            });
            readThread.start(); //읽기 스레드 시작
            writeThread.start();// 쓰기 스레드 시작

            readThread.join(); // 읽기 스레드가 종료까지 대기
            writeThread.join(); // 쓰기 스레드가 종료까지 대기

            /**
             * join() = 이 스레드가 끝날 때 까지 기다려 줘 라는 의미
             * Thread.sleep() 이 "N초동안 잠깐 잠들어 멈춰" 라면
             * join()은 저 스레드가 끝날 때 까지 멈춰이다
             *
             * join() 이 없으면
             * main 스레드 바로 try 블록을 벗어남
             * 소켓 자동 close()
             * 아직 실행 중인readThread, writeThread 가 닫힌 소캣으로 통신시도 ....오류발생
             */

        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

 

 

-클라이언트 측 코드

package client.ch03;

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

public class MultiThreadedClient {

    public static void main(String[] args) {
    //다른 사람 컴퓨터 IP넣으면 서로 응답 가능
        try (Socket socket = new Socket("localhost", 5000)) {

            //소켓에서 연결 할 입력, 출력 스트림 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) {
                            if ("exit".equalsIgnoreCase(serverMessage)) {
                                System.out.println("서버가 종료했습니다");
                            }
                            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) {
                            writer.println(">>> " + clientMessage);
                            if ("exit".equalsIgnoreCase(clientMessage)) {
                                System.out.println("클라이언트가 종료했습니다");
                                break;
                            }
                        }
                    } 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);
        }
    }
}

 

-실행 결과

더보기

클라이언트

 

서버

 클라이언트