본문 바로가기
Java/Java

소켓을 활용한 Echo Server 만들기

by oneny 2023. 9. 1.

소켓(Socket)

소켓은 네트워크에서 실행되는 두 프로그램 간의 양방향 통신 링크의 양 끝점을 말한다. 즉, 프로세스가 데이터를 보내거나 받기 위해서는 반드시 소켓을 열어서 소켓에 데이터를 써보내거나 소켓으로부터 데이터를 읽어들여야 한다.

다시 말해, 소켓은 떨어져 있는 두 호스트를 연결해주는 도구로써 인터페이스의 역할을 하는데 데이터를 주고 받을 수 있는 구조체로 소켓을 통해 데이터 통로가 만들어진다. 이러한 소켓은 역할에 따라 서버 소켓과 클라이언트 소켓으로 구분되는데 이에 대해서는 뒤에서 자세히 살펴보자.

 

소켓의 역할

클라이언트-서버 응용 프로그램에서 서버는 사용자가 원하는 정보를 전송하여 보여주는 서비스를 일부 제공한다. 이러한 클라이언트와 서버 간에 발생하는 통신은 신뢰할 수 있어야 한다. 즉, 데이터를 삭제할 수 없으며 서버가 데이터를 보낸 순서와 동일한 순서로 클라이언트 측에 도착해야 한다. 일반적으로 웹 서비스에서 사용하는 통신으로 HTTP 통신이 있다.

 

널리 알려진 OSI 7 Layer 중 Application Layer에서 가장 대표적인 HTTP(Hypertext Transfer Protocol), FTP(File Transfer Protocol), Telnet들은 모두 TCP(Transmission Control Protocol) 통신을 한다. TCP 통신은 인터넷 상의 클라이언트-서버 응용 프로그램이 서로 통신하는데 사용하는 안정적인 통신 채널을 제공하여 연결 기반 프로토콜이라고 불린다. TCP 통신하기 위해 클라이언트 프로그램과 서버 프로그램은 서로 연결을 설정하고, 소켓을 양 종단점 끝에 바인딩한다. 이후 클라이언트와 서버는 각각 연결에 바인딩된 소켓에서 읽고 쓴다. 여기서 알 수 있듯이 TCP 통신을 위해서는 소켓이 필요하다.

 

HTTP 통신도 마찬가지로 요청이 올 때만 서버가 응답하는 방식으로 일회성으로 커넥션을 맺었다가 끊기 때문에 단방향 통신이라고 말하지만 결국 HTTP 또한 소켓 통신을 활용한 통신 방식의 일종이다.

 

소켓 통신을 위해 필요한 개념

 

프로토콜

프로토콜은 원래 외교상의 언어로써 의례나 국가간에 약속을 의미하며, 통신에서는 어떤 시스템이 다른 시스템과 통신을 원활하게 수용하도록 해주는 통신규약, 약속을 말한다.

 

IP

전 세계 컴퓨터에 부여된 고유의 식별 주소

 

포트

포트(Port)는 네트워크 상에서 통신하기 위해서 호스트 내부적으로 프로세스가 할당받아야 하는 고유한 숫자이다. 한 호스트 내에서 네트워크 통신을 하고 있는 프로세스를 식별하기 위해 사용되는 값으로, 같은 호스트 내에서 서로 다른 프로세스가 같은 Port 넘버를 가질 수 없다. 즉, 같은 컴퓨터 내에서 프로그램을 식별하는 번호라고 할 수 있다.

Port 번호는 1 ~ 65535까지 사용 가능하며, 이중 1 ~ 1023까지는 시스템 서비스용으로 예약되어 있다. ServerSocket

소켓 통신을 위한 Socket 클래스

출처 - GeeksForGeeks(java.net.SocketException in Java with Examples)

Java에서 TCP 통신을 자바에서 수행하려면 Socket 클래스를 사용하면 된다. 이 Socket 클래스는 데이터를 보내는 쪽(보통 클라이언트)에서 객체를 생성하여 사용한다. 데이터를 받는 쪽(보통 서버)에서 클라이언트 요청을 받으면, 요청에 대한 Socket 객체를 생성하여 데이터를 처리한다. 즉, Socket 클래스는 서버 쪽이 되었든, 클라이언트 쪽이 되었든 원격에 있는 장비와의 연결 상태를 보관하고 있다고 생각하면 된다.

 

위에서 소켓은 역할에 따라 서버 소켓과 클라이언트 소켓으로 구분한다고 했는데 java.net 패키지는 이에 맞게 각각 두 개의 클래스(Socket 및 ServerSocket)를 구현하여 제공한다.  위 그림을 보면 클라이언트에서 서버에 데이터를 전송하기 위해서는 OutputStream을, 서버로부터 데이터를 응답받기 위해서는 InputStream을 사용한다. 반대로 서버에서 ServerSocket의 accept() 메서드를 통해 생성된 socket 객체는 클라이언트로부터 요청받은 데이터를 InputStream으로, 클라이언트에게 결과를 전송하기 위해서는 OutputStream을 사용하는 것을 알 수 있다.

 

클라이언트 Socket 클래스

생성자 설명
Socket() 소켓 객체만 생성
Socket(InetAddress address, int port) 소켓 객체 생성 후 address와 port를 사용하는 서버에 연결(접속)
Socket(String host, int port) 소켓 객체 생성 후 host와 port를 사용하는 서버에 연결(접속)

데이터를 보내는 클라이언트에서는 Socket 객체를 직접 생성해야 하고, java.net 패키지에 있는 Socket 클래스의 생성자는 위와 같다. 소켓을 생성하면 연결은 자동으로 이루어지며, 연결 시 오류가 발생하면 IOException이 발생한다. TCP 연결에는 로컬IP 주소와 원격IP 주소, 로컬포트, 원격 포트 등이 사용된다. 소켓이 생성되어 원격호스트에 접속할 때 로컬의 포트는 대개 사용하지 않는 로컬 포트가 사용된다.

 

ServerSocket 클래스

그러면 서버에서는 어떻게 데이터를 받을까? 서버에서는 ServerSocket이라는 클래스를 사용하여 데이터를 받는다. 방금 서버에서 요청에 대한 Socket 객체를 만든다고 했는데, 이 객체를 ServerSocket 클래스에서 제공하는 appect() 메서드에서 클라이언트 요청이 생기면 Socket 객체를 생성해서 전달해 준다.

 

ServerSocket 생성자

생성자 설명
ServerSocket() 서버 소켓 객체만 생성한다.
ServerSocket(int port) 지정된 포트를 사용하는 서버 소켓을 생성한다.
ServerSocket(int port, int backlog) 지정된 포트와 backlog 개수를 가지는 소켓을 생성한다.
ServerSocket(int port, int backlog, InetAddress bindAddr) 지정된 포트와 backlog 개수를 가지는 소켓을 생성하면, bindAddr에 있는 주소에서의 접근만을 허용한다.

backlog라는 값은 쉽게 생각하면 큐의 개수라고 보면 된다. ServerSocket 객체가 바빠서 연결 요청을 처리 못하고 대기시킬 때가 있는데, 그 때의 최대 대기 개수라고 보면 된다. 지정하지 않을 경우에는 backlog의 개수는 50개가 되며, 애플리케이션에 따라 적절하게 개수를 조절하는 것이 좋다.

ServerSocket 생성 시 자기 자신의 컴퓨터에 소켓을 생성하기 때문에 IP 주소를 필요없다. 그리고 포트 번호를 0으로 주는 것은 시스템에서 알아서 포트를 할당하라는 의미이고, 매개 변수가 없는 ServerSocket 클래스를 제외한 나머지 클래스들은 객체가 생성되자마자 연결을 대기할 수 있는 상태가 된다(매개변수가 없는 ServerSocket()은 연결작업을 해야만 한다).

 

리턴 타입 메서드 설명
Socket accept() 새로운 소켓 연결을 기다리고, 연결이 되면 Socket 객체 리턴
void close() 소켓 연결을 종료

객체 생성 후 사용자의 요청을 대기하는 메서드는 accept() 메서드다. 소켓 연결이 끝난 이후에 소켓을 닫는 메서드는 close() 메서드이다. close() 메서드를 처리하지 않고, JVM이 계속 동작중이라면, 해당 포트는 동작하는 서버나 PC에서 다른 프로그램이 사용할 수 없다.

ServerSocket이 accept()할 때 리턴된 서버용 Socket은 해당 클라이언트와 통신할 수 있는 유일한 수단 즉, 클라이언트 상대용 소켓이라고 생각하면 된다. 클라이언트로부터 요청이 와 accept()할 때 요청큐에서 꺼내져 리턴된 서버용 Socket은 자동으로 포트(Port)를 할당받고, 멀티스레드 환경에서 이 Socket을 생성한 스레드 등에 줘서 클라이언트를 상대하게 한다.

 

에코 서버 만들기

 

서버 소켓 만들기

 

public class EchoServer {

  public static void main(String[] args) throws IOException {
    ServerSocket serverSocket = new ServerSocket(9999); // 현재 컴퓨터에서 생성되기 때문에 address가 필요없다.
    System.out.println(9999 + " 포트 Echo Server Running...");

    while (true) {
      Socket socket = serverSocket.accept(); // 클라이언트로부터 요청이 들어와 연결됨
      System.out.println(LocalDateTime.now() + " : " + socket.toString());

      InputStream inputStream = socket.getInputStream();
      OutputStream outputStream = socket.getOutputStream();
      byte[] buf = new byte[1024]; // 1024 크기로 나눠서 읽으려고 함
      int count;

      while ((count = inputStream.read(buf)) != -1) { // 읽을 것이 있을때까지
        outputStream.write(buf, 0, count); // 클라이언트가 전송한 데이터를 outputStream에 입력
        System.out.write(buf, 0, count); // 콘솔 화면에 출력
      }

      outputStream.close();
      System.out.println("연결 종료 : " + socket);

      socket.close();
    }
  }
}

먼저, 소켓 통신용 에코 서버를 만들어보자. 위에서 말한 것처럼 ServerSocket 객체를 어느 포트 번호에서 사용할지와 함께 생성한 것을 확인할 수 있다. 그리고 루프를 돌다 클라이언트로부터 요청이 들어오면 accept() 메서드를 통해 연결된 소켓이 반환된다. 즉, 클라이언트와의 통신 채널이 생겼다고 생각하면 되고, 서로 데이터를 주고 받기 위해서는 InputStream과 OutputStream을 사용한다.

에코 서버 말그대로 메아리처럼 클라이언트가 요청한 데이터를 그대로 반환하기 위해 그 다음 로직을 짰고, 연결이 끊기면 콘솔에 종료했다는 출력과 함께 종료된다.

 

클라이언트 소켓 만들기

public class EchoClient {

  public static void main(String[] args) throws IOException {

    Socket socket = new Socket("127.0.0.1", 9999);
    System.out.println("소켓 연결 : " + socket);

    // 서버와 데이터를 주고 받기 위해 스트림을 열어서 스트림을 통해 대화를 주고 받을 수 있다.
    OutputStream outputStream = socket.getOutputStream();
    InputStream inputStream = socket.getInputStream();
    // 기본적으로 OutputStream, InputStream은 한 바이트 단위로 값을 주거나 받는다.
    byte[] buf = new byte[1024]; // 버퍼
    int count;

    while ((count = System.in.read(buf)) != -1) { // 클라이언트가 입력한 것이 더이상 없으면 -1을 반환
      outputStream.write(buf, 0, count); // buf[0]부터 count 개의 바이트를 출력스트림으로 보낸다.

      count = inputStream.read(buf); // 입력스트림으로부터 읽은 바이트들을 매개값으로 주어진 buf에 저장, 실제 읽은 바이트 수 리턴
      System.out.write(buf, 0, count);
    }

    outputStream.close();
    System.out.println(socket + ": 연결 종료");
    socket.close();
  }
}

OutputStream을 통해서 클라이언트가 원하는 데이터를 쓰고, 에코 서버를 통해 자신이 보냈던 데이터를 그대로 받는다. 그리고 해당 데이터를 출력한다.

 

결과

클라이언트와 에코 서버가 소켓 통신을 통해 콘솔에서 주고받은 데이터를 출력한 것을 확인할 수 있다.

 

출처

자바 입문 마스터하기 - 소켓을 활용한 Echo Server 만들기

(자바네트워크)자바 소켓(Socket, ServerSocket)이란, EchoClient, EchoServer,자바소켓프로그래밍

[기본] 소켓(SOCKET)통신 이란?

자바의 신2 - 28장. 다른 서버로 데이터를 보내려면 어떻게 하면 되나요?