Java 소켓 프로그래밍: 네트워크 통신을 위한 Java Socket

Java 소켓(Socket)은 네트워크 통신을 위한 API입니다. 소켓(Socket)은 TCP/IP 기반의 연결 지향형 통신을 제공하며, 소켓 프로그래밍을 통해 클라이언트와 서버 간의 데이터 통신을 구현할 수 있습니다.


소켓(Socket) 관련 용어

`클라이언트와 서버`

클라이언트는 서버에 연결을 요청하고 데이터를 보내고 받는 쪽이며, 서버는 클라이언트의 요청을 수락하고 처리하는 쪽입니다.

 

`TCP/IP`

소켓은 TCP/IP 프로토콜을 기반으로 동작합니다. TCP는 신뢰성 있는 연결 지향형 통신을 제공합니다. IP는 데이터를 패킷으로 분할하여 전송하는 역할을 합니다. Java는 TCP 네트워킹을 위해 `java.net.Socket``java.net.ServerSocket` 클래스를 제공합니다.


소켓 통신 흐름

`java.net.ServerSocket`: 연결 요청을 기다리면서 연결 수락을담당

`java.net.Socket`: 통신 담당

출처:자바 클라이언트 서버 모델(1) TCP 네트워.. : 네이버블로그 (naver.com)

 

  1. 클라이언트에서 통신할 서버의 IP주소와 포트 번호를 안 상태에서 Socket을 생성하여 서버에 연결 요청을 합니다.
  2. 서버는 고정된 포트 번호에 바인딩하기 때문에, 서버에서 ServerSocket을 생성할 때 포트 번호를 하나 지정해줍니다. 위 그림에서는 8888로 포트 번호를 지정하여 SeverSocket을 생성한 이미지입니다.
  3. 클라이언트에서 Socket을 생성하여 연결을 요청하고, 서버에 있는 ServerSocket에서 요청을 수락하면(accept()) 서버는 통신용 Socket을 생성합니다.
  4. 클라이언트와 서버는 각각의 Socket을 이용해서 데이터를 주고 받습니다.     

클라이언트 소켓(Socket) 생성

  • 클라이언트는 ``java.net.Socket` 클래스를 사용하여 서버에 연결할 소켓을 생성합니다.
  • 소켓 생성 시 서버의 IP 주소와 포트 번호를 지정해야 합니다.
  • 클라이언트 소켓은 `InputStream` `OutputStream`을 통해 서버와 데이터를 주고받을 수 있습니다.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class ClientExample {
    public static void main(String[] args) {
        String serverIP = "127.0.0.1";  // 서버의 IP 주소
        int serverPort = 8000;          // 서버의 포트 번호

        try {
            // 클라이언트 소켓 생성
            Socket socket = new Socket(serverIP, serverPort);

            // 서버와의 입출력 스트림 확보
            OutputStream outputStream = socket.getOutputStream();
            InputStream inputStream = socket.getInputStream();

            // 서버로 메시지 전송
            String message = "Hello Server!";
            outputStream.write(message.getBytes());

            // 서버로부터 메시지 수신
            byte[] buffer = new byte[1024];
            int bytesRead = inputStream.read(buffer);
            String receivedMessage = new String(buffer, 0, bytesRead);
            System.out.println("Message From Server: " + receivedMessage);
			
            outputStream.flush();
            
            // 입출력 스트림및 소켓 닫기 
            inputStream.close();
            outputStream.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

서버 소켓(Socket) 생성

  • 서버는 `java.net.ServerSocket` 클래스를 사용하여 클라이언트의 연결 요청을 수락할 소켓을 생성합니다.
  • 서버 소켓은 특정 포트 번호에서 클라이언트의 연결 요청을 대기합니다.
  • 클라이언트의 연결 요청이 있을 때마다 서버는 새로운 소켓을 생성하여 해당 클라이언트와 통신을 수행합니다.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerExample {
    public static void main(String[] args) {
        int serverPort = 8000;  // 서버의 포트 번호

        try {
            // 서버 소켓 생성
            ServerSocket serverSocket = new ServerSocket(serverPort);

            while (true) {
                // 클라이언트의 연결 요청 대기 및 수락
                Socket clientSocket = serverSocket.accept();

                // 클라이언트와의 입출력 스트림 확보
                OutputStream outputStream = clientSocket.getOutputStream();
                InputStream inputStream = clientSocket.getInputStream();

                // 클라이언트로부터 메시지 수신
                byte[] buffer = new byte[1024];
                int bytesRead = inputStream.read(buffer);
                String receivedMessage = new String(buffer, 0, bytesRead);
                System.out.println("Message From Client: " + receivedMessage);

                // 클라이언트로 메시지 전송
                String message = "Hello Client!";
                outputStream.write(message.getBytes());
				
                outputStream.flush();
                
                // 입출력 스트림및 소켓 닫기
                inputStream.close();
                outputStream.close();
                clientSocket.close();
            }
            
            // 서버 소켓 닫기
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • `accept()`: 클라이언트의 연결 요청을 받기를 대기하다 요청이 오면 요청을 수락하고, 클라이언트와의 통신을 위한 `Socket` 객체를 반환합니다. `accept()` 메서드는 블로킹 메서드로, 클라이언트의 연결 요청이 들어올 때까지 실행을 멈추고 대기하다 연결 요청이 들어오면 해당 클라이언트와 통신을 위한 `Socket` 객체를 반환합니다. 

 

ServerSocket 객체 생성시 바인딩 방법

1. 생성자에 바인딩할 포트 대입

ServerSocket serverSocket = new ServerSocket(port);

2. bind() 메서드를 이용하여 대입

ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(address, port));

`bind()` 메서드를 사용하여 IP 주소와 포트에 바인딩하고 서버 소켓을 생성할 수 있습니다. `bind()` 메서드를 사용할 때 IP 주소를 지정해줌으로, 서버는 지정된 주소의 지정된 포트로 들어오는 클라이언트의 연결 요청만을 수행합니다. 만약 IP주소를 지정해주지 않으면 서버는 모든 네트워크 인터페이스에 대해 포트를 바인딩합니다. 첫 번째 방법 역시 이와 마찬가지입니다.


다중 클라이언트 처리

위에서 보여준 서버의 코드는 단일 연결 처리만 할 수 있습니다. 다중 클라이언트에서 요청이 들어올 때 처리하기 위해서는 멀티스레드나 스레드 풀을 사용하여야 합니다. 이렇게 구현하면 각 클라이언트 연결은 별도의 스레드에서 처리되며, 서버는 클라이언트와의 통신을 병렬로 처리합니다.

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class MultiClientServer {
    public static void main(String[] args) {
        int serverPort = 8000;  // 서버의 포트 번호

        try {
            // 서버 소켓 생성
            ServerSocket serverSocket = new ServerSocket(serverPort);

            while (true) {
                // 클라이언트의 연결 요청 수락
                Socket clientSocket = serverSocket.accept();
                
                // 클라이언트와의 통신을 처리하는 스레드 생성 및 시작
                Thread clientThread = new ClientHandler(clientSocket);
                clientThread.start();
            }
            
            // 서버 소켓 닫기
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 클라이언트와의 통신을 처리하는 스레드 클래스
    private static class ClientHandler extends Thread {
        private Socket clientSocket;

        public ClientHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }

        @Override
        public void run() {
            try {
                // 클라이언트와의 입출력 스트림 확보
                InputStream inputStream = clientSocket.getInputStream();
                OutputStream outputStream = clientSocket.getOutputStream();

                // 클라이언트로부터 메시지 수신
                byte[] buffer = new byte[1024];
                int bytesRead = inputStream.read(buffer);
                String receivedMessage = new String(buffer, 0, bytesRead);
                System.out.println("Message From Client: " + receivedMessage);

                // 클라이언트로 메시지 전송
                String message = "Hello Client!!";
                outputStream.write(message.getBytes());

                // 입출력 스트림 및 소켓 닫기
                inputStream.close();
                outputStream.close();
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 소켓 예외 처리

소켓을 사용하다보면 네트워크 관련된 다양한 예외들이 있을 수 있습니다. 소켓과 관련된 주요 예외는 다음과 같습니다.

 

  • `IOException`: 입출력 작업 중에 발생하는 일반적인 예외입니다. 소켓 연결이 끊기거나 통신 도중에 오류가 발생하는 경우에 이 예외가 발생할 수 있습니다.
  • `SocketException`: 소켓 관련 예외입니다. 소켓 설정에 문제가 있거나 소켓이 닫혀 있는 경우에 이 예외가 발생할 수 있습니다.
  • `ConnectException`: 서버에 연결할 수 없는 경우에 발생하는 예외입니다. 서버가 다운되었거나 네트워크 연결에 문제가 있는 경우에 이 예외가 발생할 수 있습니다.
  • `UnknownHostException`: 주어진 호스트 이름이나 IP 주소를 해석할 수 없는 경우에 발생하는 예외입니다. DNS 조회에 실패하거나 잘못된 호스트 이름을 사용하는 경우에 이 예외가 발생할 수 있습니다.
  • `SocketTimeoutException`: 소켓 작업(연결, 읽기, 쓰기 등)이 지정된 시간 내에 완료되지 않을 때 발생하는 예외입니다. 소켓 작업이 시간 초과될 경우에 이 예외가 발생할 수 있습니다.

최종 범용성 코드

  • `try-with-resources`문을 사용하여 자원을 관리하도록 하였습니다.
  • `PrintWriter`를 생성하고 `true`를 인자로 주어 자동으로 flush 되도록 하였습니다.
  • `finally` 클라이언트 소켓을 닫도록 처리하였습니다.
  • `BufferedReader`를 사용하여 문자열 단위로 데이터를 읽어옴으로 입출력 속도를 향상시켰습니다.   
  • 로직 처리는 따로 메서드로 분리하였습니다.   
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class MultiClientServerExample {
    public static void main(String[] args) {
        int serverPort = 8000; // 서버의 포트 번호

        try (ServerSocket serverSocket = new ServerSocket(serverPort)) {

            while (true) {
                Socket clientSocket = serverSocket.accept();
                
                Thread clientThread = new Thread(new ClientHandler(clientSocket));
                clientThread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static class ClientHandler implements Runnable {
        private Socket clientSocket;

        public ClientHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }

        @Override
        public void run() {
            try (
                InputStream inputStream = clientSocket.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                OutputStream outputStream = clientSocket.getOutputStream();
                PrintWriter writer = new PrintWriter(outputStream, true)
            ) {
                String request = reader.readLine();

                // 요청 처리 로직
                String response = processRequest(request);
                
                writer.println(response);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        
        private String processRequest(String request);
        	// 로직 처리
            return "Hello Client!!";
        }
    }
}