[Java] TCP/IP Socket Server-Client 채팅 프로그램

devdo·2024년 12월 28일
0

Java

목록 보기
61/61
post-thumbnail

Java 소켓 프로그래밍 개발

1. 개요

Java의 Socket API를 사용한 네트워크 프로그래밍에 대한 학습하겠습니다. 기본적인 에코 서버/클라이언트 및 멀티스레드 에코 서버 구현을 통해 Java의 Thread, Socket, 네트워크 Stream의 개념과 사용법을 설명합니다.

2. Java 소켓 프로그래밍 기본 개념

2.1 소켓(Socket)

소켓은 네트워크 통신의 엔드포인트로, 두 프로그램이 네트워크를 통해 데이터를 주고받을 수 있게 합니다.

  • ServerSocket: 서버 측에서 클라이언트의 연결 요청을 기다리는 소켓
  • Socket: 실제 데이터 통신에 사용되는 소켓(클라이언트와 서버 양쪽에서 사용)

2.2 스트림(Stream)

스트림은 데이터의 입출력을 다루는 Java의 기본 메커니즘입니다.

  • InputStream: 데이터를 읽기 위한 스트림
  • OutputStream: 데이터를 쓰기 위한 스트림

2.3 스레드(Thread)

스레드는 프로그램 내에서 동시에 실행될 수 있는 작은 실행 단위입니다. 다중 클라이언트 처리가 필요한 서버에서 중요합니다.

3. 구현 예제 분석

3.1 기본 에코 서버 (EchoServer.java)

public class EchoServer {
    public static void main(String[] args) throws IOException {
        // 서버 소켓을 포트 9999에서 생성
        ServerSocket serverSocket = new ServerSocket(9999);
        
        // 무한 루프로 클라이언트 연결을 대기
        while (true) {
            // 클라이언트 연결 대기
            Socket socket = serverSocket.accept();
            
            // 클라이언트와의 입출력 스트림 생성
            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            
            byte[] buf = new byte[1024]; // 데이터 버퍼
            int count = 0;
            
            // 클라이언트로부터 데이터를 읽고 다시 전송
            while ((count = inputStream.read(buf)) != -1) {
                outputStream.write(buf, 0, count);
                System.out.write(buf, 0 , count);
            }
            
            // 연결 종료 및 소켓 닫기
            outputStream.close();
            socket.close();
        }
    }
}

핵심 개념:

  • ServerSocket(9999): 9999 포트에서 서버 소켓 생성
  • serverSocket.accept(): 클라이언트 연결을 기다리는 블로킹 메서드
  • socket.getInputStream()/getOutputStream(): 클라이언트와 데이터를 주고받을 스트림 생성
  • inputStream.read(buf): 클라이언트로부터 데이터 읽기
  • outputStream.write(buf, 0, count): 클라이언트에게 데이터 전송

💥한계점: 하나의 클라이언트만 처리 가능. 첫 클라이언트의 연결이, 처리가 끝날 때까지 다른 클라이언트는 대기해야 합니다.


3.2 에코 클라이언트 (EchoClient.java)

public class EchoClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket();
        // 서버에 연결
        socket.connect(new InetSocketAddress("localhost", 9999));
        
        // 서버와의 입출력 스트림 생성
        OutputStream outputStream = socket.getOutputStream();
        InputStream inputStream = socket.getInputStream();
        
        byte[] buf = new byte[1024]; // 데이터 버퍼
        int count = 0;
        
        // 표준 입력에서 데이터를 읽어 서버로 전송하고 응답 받기
        while ((count = System.in.read(buf)) != -1) {
            outputStream.write(buf, 0, count);
            count = inputStream.read(buf);
            System.out.write(buf, 0, count);
        }
        
        // 연결 종료 및 소켓 닫기
        outputStream.close();
        socket.close();
    }
}

핵심 개념:

  • new Socket(): 소켓 객체 생성
  • socket.connect(new InetSocketAddress("localhost", 9999)): 지정된 주소와 포트로 서버에 연결
  • System.in.read(buf): 표준 입력(키보드)에서 데이터 읽기
  • inputStream.read(buf): 서버로부터 응답 데이터 읽기

3.3 멀티스레드 에코 서버 (MultiThreadEchoServer.java)

public class MultiThreadEchoServer extends Thread {
    private Socket socket = null;
    
    // 생성자: 클라이언트 소켓을 받아 초기화
    public MultiThreadEchoServer(Socket socket) {
        this.socket = socket;
    }
    
    // 스레드가 실행되는 메서드
    public void run() {
        try {
            InputStream fromClient = socket.getInputStream();
            OutputStream toClient = socket.getOutputStream();
            
            byte[] buf = new byte[1024];
            int count = 0;
            while ((count = fromClient.read(buf)) != -1) {
                toClient.write(buf, 0, count);
                System.out.write(buf, 0, count);
            }
        } catch (IOException e) {
            System.out.println(socket + ": 연결종료 (" + e + ")");
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                    socket = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public static void main(String[] args) throws NumberFormatException, IOException {
        ServerSocket serverSocket = new ServerSocket(5001);
        while (true) {
            Socket client = serverSocket.accept();
            MultiThreadEchoServer myServer = new MultiThreadEchoServer(client);
            myServer.start();
        }
    }
}

핵심 개념:

  • extends Thread: Thread 클래스 상속으로 멀티스레딩 구현
  • 클라이언트마다 새로운 스레드 생성
  • myServer.start(): 스레드 시작, run() 메서드 실행
  • 다중 클라이언트 동시 처리 가능

4. 주요 기술 상세 설명

4.1 Java Socket API

ServerSocket 클래스:

  • 생성: ServerSocket serverSocket = new ServerSocket(포트번호)
  • 클라이언트 연결 대기: Socket clientSocket = serverSocket.accept()
  • 주요 메서드:
    • accept(): 클라이언트 연결 수락(블로킹)
    • close(): 서버 소켓 닫기

Socket 클래스:

  • 생성: Socket socket = new Socket() 또는 new Socket(호스트, 포트)
  • 연결: socket.connect(new InetSocketAddress(호스트, 포트))
  • 주요 메서드:
    • getInputStream(): 소켓에서 데이터를 읽기 위한 스트림
    • getOutputStream(): 소켓으로 데이터를 쓰기 위한 스트림
    • close(): 소켓 닫기

4.2 Java Stream API

InputStream:

  • 주요 메서드:
    • read(byte[] b): 바이트 배열로 데이터 읽기
    • read(byte[] b, int off, int len): 지정된 오프셋과 길이로 데이터 읽기
    • 반환값 -1은 더 이상 읽을 데이터가 없음을 의미

OutputStream:

  • 주요 메서드:
    • write(byte[] b): 바이트 배열 전체 쓰기
    • write(byte[] b, int off, int len): 지정된 오프셋과 길이로 데이터 쓰기
    • flush(): 버퍼링된 출력 바이트 강제 쓰기

4.3 Java Thread API

Thread 클래스:

  • 생성 방법:
    1. Thread 클래스 상속: class MyThread extends Thread
    2. Runnable 인터페이스 구현: class MyRunnable implements Runnable
  • 주요 메서드:
    • start(): 스레드 시작
    • run(): 스레드 실행 시 실행될 코드 (오버라이드 필요)
    • join(): 스레드가 종료될 때까지 대기

멀티스레드 서버의 이점:

  • 동시에 여러 클라이언트 처리 가능
  • 클라이언트 처리 시간이 길어도 다른 클라이언트 연결 차단되지 않음
  • 서버 리소스를 효율적으로 사용 가능

5. 구현 시 주의사항

5.1 리소스 관리

예제 코드에서는 다음과 같은 리소스 관리 패턴을 사용했습니다:

  • 소켓과 스트림 사용 후 명시적 close()
  • finally 블록에서 리소스 해제 (MultiThreadEchoServer)

개선점: 현대 Java에서는 try-with-resources 구문을 사용하여 더 안전하게 리소스를 관리할 수 있습니다.

5.2 예외 처리

예제 코드에서는 다음과 같은 예외 처리 패턴을 보여줍니다:

  • IOException을 메인 메서드에서 throw
  • MultiThreadEchoServer에서는 catch 블록으로 예외 처리

개선점: 더 상세한 예외 처리와 로깅이 필요합니다.

5.3 스레드 관리

현재 구현에서는 클라이언트마다 새 스레드를 생성합니다. 이는 간단하지만 클라이언트가 많아지면 비효율적일 수 있습니다.

개선점: ThreadPool(ExecutorService)을 사용하여 스레드 수를 제한하고 효율적으로 관리할 수 있습니다.

6. 확장 및 개선 방향

6.1 채팅 기능 확장

현재 1:1 에코 서버에서 다음과 같이 확장할 수 있습니다:

  • 클라이언트 간 메시지 브로드캐스팅
  • 사용자 이름 등록 및 관리
  • 대화방(채팅룸) 개념 도입

6.2 성능 개선

  • NIO(Non-blocking I/O) 사용: java.nio 패키지의 Channel과 Selector 활용
  • 스레드 풀 도입: java.util.concurrent.ExecutorService 사용

6.3 보안 강화

  • SSL/TLS를 통한 암호화 통신: javax.net.ssl 패키지 활용
  • 사용자 인증 및 권한 관리 추가

7. 결론

Java 소켓 프로그래밍의 기본 개념인 Socket, Stream, Thread를 소개하고, 실제 구현 예제를 통해 그 사용법을 보여주었습니다.

제공된 예제 코드는 기본적인 에코 서버/클라이언트와 멀티스레드 서버를 구현하며, 실제 채팅 애플리케이션으로 확장하기 위한 기반을 제공합니다.

Java의 네트워크 프로그래밍은 강력하고 유연하며, 이러한 기본 개념을 이해함으로써 더 복잡한 네트워크 애플리케이션을 개발할 수 있는 토대를 마련할 수 있습니다.

8. 개선 ⭐

멀티스레드 에코 서버의 데이터 송수신과 예외 처리를 개선하도록 하겠습니다. 주요 개선사항은 다음과 같습니다:

  1. 버퍼 크기를 상수로 정의
  2. try-with-resources 사용하여 자원 관리 개선
  3. 데이터 송수신 시 버퍼 플러시 추가
  4. 더 자세한 예외 처리와 로깅
  5. 스레드 이름 지정으로 디버깅 용이성 향상
public class MultiThreadEchoServer extends Thread {
    private static final int BUFFER_SIZE = 1024;
    private final Socket socket;

    public MultiThreadEchoServer(Socket socket) {
        this.socket = socket;
        setName("Client-" + socket.getInetAddress().getHostAddress()); // 스레드 이름 지정
    }

    public void run() {
        System.out.println("[" + getName() + "] 클라이언트 연결됨: " + socket);
        
        try (InputStream fromClient = socket.getInputStream();
             OutputStream toClient = socket.getOutputStream()) {
            
            byte[] buf = new byte[BUFFER_SIZE];
            int count;
            
            while ((count = fromClient.read(buf)) != -1) {
                toClient.write(buf, 0, count);
                toClient.flush(); // 버퍼 즉시 전송
                
                // 서버 로그에 수신된 데이터 출력
                System.out.printf("[%s] 수신된 데이터 크기: %d bytes%n", getName(), count);
                System.out.write(buf, 0, count);
                System.out.flush();
            }

        } catch (IOException e) {
            System.out.printf("[%s] 연결 오류: %s%n", getName(), e.getMessage());
        } finally {
            closeSocket();
        }
    }

    private void closeSocket() {
        if (socket != null && !socket.isClosed()) {
            try {
                socket.close();
                System.out.printf("[%s] 소켓 정상 종료%n", getName());
            } catch (IOException e) {
                System.out.printf("[%s] 소켓 종료 중 오류 발생: %s%n", getName(), e.getMessage());
            }
        }
    }

    // ... existing code ...
}

주요 개선사항 설명

  1. BUFFER_SIZE를 상수로 정의하여 버퍼 크기를 쉽게 관리할 수 있게 함
  2. try-with-resources를 사용하여 스트림 자원을 자동으로 해제
  3. flush()를 추가하여 데이터가 즉시 전송되도록 보장
  4. 스레드 이름을 지정하여 로깅 시 어떤 클라이언트의 메시지인지 식별 가능
  5. 소켓 종료 로직을 별도 메서드로 분리하여 코드 가독성 향상
  6. 더 자세한 로그 메시지 추가로 디버깅 용이성 향상

이러한 개선사항들은 서버의 안정성과 유지보수성을 높여줄 것입니다.


참고

profile
배운 것을 기록합니다.

0개의 댓글