Engineering Note

[Network] 간단한 채팅프로그램 직접 만들어보며 TCP/IP 소켓 통신 이해하기 본문

Computer Science/Network

[Network] 간단한 채팅프로그램 직접 만들어보며 TCP/IP 소켓 통신 이해하기

Software Engineer Kim 2025. 7. 20. 13:26

[1단계] TCP Server/Client 기본 연결(완료)

  • ServerSocket, Socket 사용
  • 클라이언트가 서버에 접속하면 “연결됨” 메시지 출력

[2단계] 텍스트 1:1 송수신(완료)

  • 서버: 클라이언트가 보낸 메시지 읽고 콘솔에 출력
  • 클라이언트: 키보드 입력을 서버에 전송, 서버 응답 받음

[3단계] 서버에서 여러 클라이언트 관리

  • 서버에서 연결되는 클라이언트마다 Thread로 관리
  • 서버가 모든 연결된 클라이언트에 메시지 브로드캐스팅

[4단계] 클라이언트-클라이언트 채팅

  • 한 클라이언트가 메시지 보내면, 서버가 전체 클라이언트에게 메시지 전달
  • 콘솔에 “닉네임: 메시지” 형태로 출력




Socker 통신 흐름

  • 클라이언트에서 Socket("127.0.0.1", 12345) 생성
  • TCP 연결 시도 (3-way handshake 등)
  • TCP 패킷 → IP 패킷에 캡슐화
  • OS 네트워크 스택에서 실제 데이터 전송

 

 

세분화한 소켓 통신 과정 (클라이언트 ↔ 서버)

1. 소켓 열기 및 연결

  • 클라이언트가 Socket 객체 생성(서버 IP/PORT로 연결 요청)
  • 서버는 ServerSocket으로 연결을 기다리다가
    클라이언트가 오면 Socket 객체를 반환

2. 데이터 송신 (보내는 쪽, 예: 클라이언트)

  • OutputStream 생성
  • OutputStream out = socket.getOutputStream(); // 바이트 단위 스트림
  • OutputStreamWriter문자열을 바이트로 변환
  • OutputStreamWriter osw = new OutputStreamWriter(out); // 문자→바이트 변환
  • BufferedWriter버퍼에 모아서 한 번에 출력
  • BufferedWriter bw = new BufferedWriter(osw);
  • 문자열을 write()로 쓰고 flush()로 내보냄
  • bw.write("hello\n"); bw.flush();

3. 데이터 수신 (받는 쪽, 예: 서버)

  • InputStream 생성
  • InputStream in = socket.getInputStream(); // 바이트 단위 스트림
  • InputStreamReader바이트를 문자로 변환
  • InputStreamReader isr = new InputStreamReader(in); // 바이트→문자 변환
  • BufferedReader버퍼에 모아서 한 번에 읽기
  • BufferedReader br = new BufferedReader(isr);
  • readLine() 등으로 한 줄씩 읽기
  • String line = br.readLine();

전체 그림 (양방향 통신일 때)

[클라이언트]
  (보내기)
  BufferedWriter
      |
  OutputStreamWriter
      |
  OutputStream
      |
    [Socket]   <------- 네트워크 ----->   [Socket]
      |
  InputStream
      |
  InputStreamReader
      |
  BufferedReader
  (받기)

[서버도 동일 구조, 반대 방향]

 

한 줄 요약

소켓 연결 후, 데이터를 보낼 때는 OutputStream(→문자변환→버퍼)에 담아 보내고, 받을 때는 InputStream(→문자변환→버퍼)에 담아 읽는다!

 

[실습]

[1 단계] Client, Server 소켓 생성 및 연결

 

1-1 ChatServer

package socket;

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

public class ChatServer {
    public static void main(String[] args) {
        final int PORT = 12345;
        System.out.println("서버 시작. 클라이언트 연결 대기 중...");

        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            Socket clientSocket = serverSocket.accept();
            System.out.println("클라이언트 연결됨: " + clientSocket.getInetAddress());

            while (true){
                // 클라이언트로부터 메시지 읽기
                BufferedReader networkInput = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                String message = networkInput.readLine();
                System.out.println("클라이언트로부터 메시지: " + message);
                if(message.equals("bye")) {
                    System.out.println("서버를 종료합니다.");
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

 

1-1 ChatClient

package socket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class ChatClient {
    public static void main(String[] args) {
        final String SERVER_IP = "127.0.0.1";
        final int SERVER_PORT = 12345;

        try (Socket socket = new Socket(SERVER_IP, SERVER_PORT);
             BufferedWriter networkOut = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
             BufferedReader keyboardInput = new BufferedReader(new InputStreamReader(System.in))) {

            while (true) {
                System.out.print("[클라이언트:] 서버로 보낼 메시지 입력: ");
                String msg = keyboardInput.readLine();

                networkOut.write(msg + "\n");
                networkOut.flush();
                System.out.println("서버로 메시지를 전송했습니다. ");
                if (msg == null || msg.equals("bye")) {
                    System.out.println("채팅 프로그램을 종료 합니다. ");
                    break;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

[1 단계]에서는 간단하게 Server의 Socket을 열어주고, Client에서 연결하는 프로그램을 작성했다.

Server 프로그램은 인텔리제이에서 실행해주고, Client는 터미널에서 직접 실행해주었다. 어디서 실행해도 상관없으나, 실제처럼 테스트를 위해 일부러 다른 환경에서 실행해주었다.

 

- 서버 프로그램 실행

 

- 클라이언트 프로그램 실행, 실행중인 서버에 연결 

 

 

 

[2 단계] 연결된 소켓을 통해 단방향 메세지 데이터 전송

- 클라이언트에서 서버에 메세지 전송, 서버가 콘솔에 출력

- TCP/IP 네트워크 계층 동작 원리 실습 및 이해(트랜스포트 계층 TCP, 네트워크 계층 IP)

 

package socket;

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

public class ChatServer {
    public static void main(String[] args) {
        final int PORT = 12345;
        System.out.println("서버 시작. 클라이언트 연결 대기 중...");

        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            Socket clientSocket = serverSocket.accept();
            System.out.println("클라이언트 연결됨: " + clientSocket.getInetAddress());

            // 클라이언트로부터 메시지 읽기
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            String message = in.readLine();
            System.out.println("클라이언트로부터 메시지: " + message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

 

package socket;

import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;

public class ChatClient {
    public static void main(String[] args) {
        final String SERVER_IP = "127.0.0.1";
        final int SERVER_PORT = 12345;

        try (Socket socket = new Socket(SERVER_IP, SERVER_PORT);
             BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
             Scanner scanner = new Scanner(System.in)) {

            System.out.print("서버로 보낼 메시지 입력: ");
            String msg = scanner.nextLine();
            out.write(msg + "\n");
            out.flush();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

 

 

[3단계]: 양방향 채팅 구조

 

- 배구나 탁구 처럼, 한 번씩 메세지를 주고 받을 수 있는 채팅 서비스

 

package socket;

import java.io.*;
import java.net.*;
import java.util.Scanner;

public class ChatServer {
    public static void main(String[] args) {
        final int PORT = 12345;
        System.out.println("서버 시작. 클라이언트 연결 대기 중...");

        try (
                ServerSocket serverSocket = new ServerSocket(PORT);
                Socket clientSocket = serverSocket.accept();
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
                Scanner scanner = new Scanner(System.in)
        ) {
            System.out.println("클라이언트 연결됨: " + clientSocket.getInetAddress());

            while (true) {
                // 1. 클라이언트 메시지 읽기
                String clientMsg = in.readLine();
                if (clientMsg == null || clientMsg.equalsIgnoreCase("bye")) {
                    System.out.println("클라이언트 연결 종료");
                    break;
                }
                System.out.println("[클라이언트] : " + clientMsg);

                // 2. 서버 메시지 입력 및 전송
                System.out.print("[서버] 메시지 입력: ");
                String serverMsg = scanner.nextLine();
                out.write(serverMsg + "\n");
                out.flush();
                System.out.println("[서버의 메시지 전송이 완료되었습니다.]\n");

                if (serverMsg.equalsIgnoreCase("bye")) {
                    System.out.println("서버 종료");
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

package socket;

import java.io.*;
import java.net.*;
import java.util.Scanner;

public class ChatClient {
    public static void main(String[] args) {
        final String SERVER_IP = "127.0.0.1";
        final int SERVER_PORT = 12345;

        try (
                Socket socket = new Socket(SERVER_IP, SERVER_PORT);
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                Scanner scanner = new Scanner(System.in)
        ) {
            while (true) {
                // 1. 클라이언트 → 서버로 메시지 전송
                System.out.print("[클라이언트] 메시지 입력: ");
                String clientMsg = scanner.nextLine();
                out.write(clientMsg + "\n");
                out.flush();
                System.out.println("[클라이언트 메시지 전송이 완료되었습니다.\n");

                if (clientMsg.equalsIgnoreCase("bye")) {
                    System.out.println("채팅 종료");
                    break;
                }

                // 2. 서버 메시지 수신
                String serverMsg = in.readLine();
                if (serverMsg == null || serverMsg.equalsIgnoreCase("bye")) {
                    System.out.println("서버에서 채팅 종료");
                    break;
                }
                System.out.println("[서버] " + serverMsg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

서버 시작

 

 

클라이언트 소켓 연결

 

 

클라이언트가 메세지를 전송하면 서버가 받고, 다시 서버가 메세지를 전송하는 구조

 

클라이언트 화면

 

서버 화면

 

 

 

[3단계] 멀티쓰레딩 방식의 다수 사용자 접속 채팅 프로그램

다수사용자 접속 -> 서버에서 접속한 모든 클라이언트에 브로드캐스트 메세지 전달

 

 

 



사용 객체 정리


1. Runnable
- Java에서 Thread로 실행가능한 작업을 의미하는 인터페이스

- Runnable에는 run메서드가 있는데 Runnable을 구현하면 run메서드를 정의해서 이 코드가 별도의 Thread에서 실행될 수 있다.

사용 이유

- 동시에 여러 작업을 처리할 때 사용(멀티 쓰레드)

2.BufferedWriter

- 문자 텍스트를 버퍼에 모았다가 어떤 출력 스트림(파일, 네트워크 등)에 쓰는 클래스

- buffer에 모았다가 데이터를 한 번에 내보내서 성능 향상

- Buffer를 사용하지 않고도 네트워크 I/O, 파일 I/O를 할 수 있으나, I/O작업은 context switch, kernel mode 진입, interrupt 등 하드웨어와 OS레벨에서 소모하는 비용이 많은 작업이므로 문자 하나마다 I/O작업을 하면 비용이 많이 들어가므로, buffer에 모아서 문자열 스트림 단위로 I/O작업을 하도록 도와주는 클래스다. Java 언어차원에서 개발자가 Buffer클래스를 직접 구현하지 않아도 되도록 지원하고 있다.

사용 예시

BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("Hello\n");
writer.flush(); // 버퍼에 남은 데이터 즉시 전송

 

 

3. BufferedReader

- 한 번에 한 문자씩이 아니라 '여러문자'로 읽어서 효율을 높인 문자 입력 스트림

- 주로 .readLine()메서드로 한 주 단위로 텍스트를 읽을 때 많이 사용.

- 내부적으로 버퍼(임시저장공간)을 사용해서 입출력 성능이 더 좋음.

- 주요 메서드

  - readLine() : 한 줄(개행까지) 읽어옴

  - read() : 한 문자 읽음

  - close() : 스트림 닫기

사용 이유

- 네트워크(소켓), 파일, 입력 콘솔 등에서 텍스트를 빠르고 편하게 읽기 위해서 사용.

- BufferedWriter와 마찬가지로 Buffer를 사용하지 않고도  I/O 작업을를 할 수 있으나, I/O작업은 context switch, kernel mode 진입, interrupt 등 하드웨어와 OS레벨에서 비용이 큰 작업이므로, 입출력 데이터를 buffer에 모아서 문자열 스트림 단위로 I/O작업을 하도록 도와주는 클래스다. Java 언어차원에서 개발자가 Buffer클래스를 직접 구현하지 않아도 되도록 지원하고 있다.

 

 

4. InputStreamReader

- 바이트 입력 스트림을 문자열로 변환하는 클래스

- "0101010101(byte)"를 글자(String)로 바꿔줌


사용 예시

InputStreamReader isr = new InputStreamReader(socket.getInputStream());
BufferedReader br = new BufferedReader(isr);

 

사용 이유

- 네트워크, 파일 등에서 텍스트 데이터를 읽어올 때 사용. 바이트 -> 문자열로 변환해야 할 때

 

 

5.OutputStreamReader

- 바이트 출력스트림에 문자열을 쓸 수 있게 해주는 클래스

- "글자(String)"를 "010101010"으로 바꿔서 내보냄.

사용 예시

OutputStreamWriter osw = new OutputStreamWriter(socket.getOutputStream());
BufferedWriter bw = new BufferedWriter(osw);

 

사용 이유

- 네트워크나 파일에 문자 데이터를 저장하거나 전송하기 위해 사용.

 

 

6. Set

- 중복을 허용하지 않는 데이터 집합(컬렉션) 인터페이스

- add로 값 추가 있으면 이미 데이터 존재하면 추가 안됨.

 

사용 이유

- 중복없이 여러 데이터를 보관할 때 사용

 

사용 예시

Set<String> set = new HashSet<>();
set.add("A");
set.add("A"); // 중복이라 한 번만 저장됨

 

 

7. ConcurrentHashMap

- 여러 Thread가 동시에 접근해도 안전하게 동작하는 HashMap(키-값 저장소)

- Thread safe

 

사용 이유

- 여러 쓰레드가 동시에 존재하는 환경에서 HashMap 대신 사용

 

사용 예시

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);

 

 

8. ServerSocket

- Server에서 'Client의 TCP 연결 요청'을 기다릴 때 쓰는 소켓.

- .accept()로 접속을 기다리고, 누가 접속하면 socket을 반환. 반환된 socket으로 데이터 송수신

 

사용 예시

ServerSocket server = new ServerSocket(12345);//파라미터: 포트 번호
Socket client = server.accept(); // 클라이언트가 접속하면 리턴됨

 

사용 이유

- Server 측에서 네트워크로 Client와 통신할 때 반드시 필요

 



9. Socket

- Operating System(OS, 운영체제)에서 제공하는 transport 계층(TCP/UDP)의 소켓을 Java 코드에서 객체로 다룰 수 있게 만들어 놓은 클래스

- 실제 데이터 송수신에 사용되는 소켓(client/server 공통 사용)객체가 사용됨

- 연결이 성사된 후에 데이터를 주고 받는 역할

 

주요 메서드

- getInputStream(), getOutputStream()

 

사용 이유

- transport 계층(TCP/UDP 등)에서 데이터를 전송하기 위해 사용 

 

사용 예시

// 서버에서는
Socket socket = serverSocket.accept();

// 클라이언트에서는
Socket socket = new Socket("서버IP", 12345); // 서버에 바로 연결 시도

 

 

 

cf)

- 소켓(Socket)이란 OS가 네트워크 통신을 위해 제공하는 transport 계층의 end-point이자 API

- 포트(Port)는 한 컴퓨터(=IP 주소)에서 여러 네트워크 애플리케이션(프로세스/서버/서비스)을 서로 구분해주는 논리적인 "경로이자 식별자"입니다.

Comments