본문 바로가기
Java

[Java] Socket에 관하여

by soro.k 2023. 2. 8.

 

들어가기 전에

자바의 Socket에 대해 설명하기 전에 먼저 네트워크 관점에서 소켓을 알아보고자 한다. 기본적인 Socket의 개념에 대해 공부한 것을 정리하고자 하는 글이므로 자세한 송수신 과정이나 소켓 말소에 대해서는 다루지 않으며 모두 TCP 통신을 기반으로 작성되었다.

 

클라이언트가 서버와 데이터를 송수신하기 위해서는 OS의 내부에 있는 네트워크 제어용 소프트웨어인 프로토콜 스택을 이용해야 한다. 

출처 : 성공과 실패를 결정하는 1%의 원리

 

<성공과 실패를 결정하는 1%의 네트워크 원리> 에서 소켓은 개념적인 것이어서 실체가 없다고 설명하면서 소켓 자체를 프로토콜 스택 내부에 데이터를 송수신 하기 위한 제어 정보를 저장하는 메모리 영역이라고 생각하면 된다고 이야기한다. 그러니까 데이터 송수신을 위한 연결의 양 끝점에 소켓이 위치하는 것이다. 실제로 아래의 데이터 송수신 의뢰 과정을 보면 이해하기가 쉽다.

 

출처 : 성공과 실패를 결정하는 1%의 원리

 

그렇다면 이 Sokcet은 어떻게 만들어지는 걸까? 처음에 socket을 호출해서 소켓 작성을 의뢰받으면 프로토콜 스택은 우선 소켓 한 개 분량의 메모리 영역을 확보한다. 그리고 송수신을 위한 제어 정보를 이 메모리 영역에 기록하는데 이 과정을 통해서 소켓이 만들어진다고 이해하면 된다. 결론적으로 프로토콜 스택은 소켓에 기록된 정보들을 참조해서 데이터를 송수신 하는데, 그러기 위해서 서버 측과 클라이언트 측에서 만든 각각의 소켓을 연결해야 한다. 

 

자바에서는 어떻게 소켓을 구현하고 데이터를 송수신할까?

 

 

자바의 Socket

A socket is an endpoint for communication between two machines.

자바에서 이야기하는 Socket도 마찬가지로 두 시스템 간의 통신을 위해 사용되는 엔드포인트를 의미한다. java.net 패키지에는 클라이언트 측의 소켓을 구현하는 Socket 클래스서버 측의 소켓을 구현하는 ServerSocket 클래스가 있어서 각각의 소켓을 만들 수 있다. 그리고 클라이언트 측과 서버 측의 소켓이 연결되면 아래 그림처럼 각각 OutputStream과 InputStream을 통해 값을 내보내고 읽는다.

 

출처 :&nbsp;https://www.geeksforgeeks.org/java-net-socketexception-in-java-with-examples/

 

 

Socket 클래스와 ServerSocket 클래스를 보면 생성자에서 setImpl()이란 메서드를 호출한다.

private void setImpl() {
    if (factory != null) {
        impl = factory.createSocketImpl();
        checkOldImpl();
    } else {
        impl = new SocksSocketImpl();
    }
    if (impl != null)
        impl.setServerSocket(this);
}

 

SocketImplFactory 인터페이스에 있는 createSocketImpl() 메서드가 실제 소켓을 구현하는 SocketImple을 생성하면서 소켓이 만들어지는 것이다.

public
interface SocketImplFactory {
    /**
     * Creates a new {@code SocketImpl} instance.
     *
     * @return  a new instance of {@code SocketImpl}.
     * @see     java.net.SocketImpl
     */
    SocketImpl createSocketImpl();
}

 

 

Server

Server 클래스의 주요 생성자는 아래의 두 생성자인데 소켓을 생성해서 지정된 host과 포트 번호에 연결하거나 지정된 IP 주소와 포트 번호에 연결할 수 있다. 

public Socket(String host, int port)
    throws UnknownHostException, IOException
{
    this(host != null ? new InetSocketAddress(host, port) :
         new InetSocketAddress(InetAddress.getByName(null), port),
         (SocketAddress) null, true);
}

public Socket(InetAddress address, int port) throws IOException {
    this(address != null ? new InetSocketAddress(address, port) : null,
         (SocketAddress) null, true);
}

 

 

주요 메서드

메서드 설명
void close() 소켓을 닫는다.
void connect(SocketAddress endpoint) 소켓을 서버에 연결한다.
InetAddress getInetAddress() 소켓이 연결한 서버의 주소를 반환한다.
InputStream getInputStream() 소켓에 대한 입력 스트림을 반환한다.
OutputStream getOuputStream() 소켓에 대한 출력 스트림을 반환한다.
InetAddress getLocalAddress() 소켓이 연결된 로컬 주소를 반환한다.
int getLocalPort() 소켓이 연결된 로컬 포트 번호를 반환한다.
int getPort() 소켓이 연결한 서버의 포트 번호를 반환한다.
boolean isBound() 소켓이 로컬 주소에 연결되어있으면 True를 반환한다.
boolean isConnected() 소켓이 서버에 연결되어있으면 True를 반환한다.
boolean isClosed() 소켓이 닫혀있으면 True를 반환한다.
void setSoTimeout(int timeout) 데이터 읽을 수 있는 타임아웃 시간을 지정, 0이면 해제한다.

 

 

ServerSocket

아래는 ServerSocket에 포트 번호 혹은 포트 번호와 backlog를 매개변수로 넘겨주는 주요 생성자이다.

public ServerSocket(int port) throws IOException {
    this(port, 50, null);
}

public ServerSocket(int port, int backlog) throws IOException {
	// backlog : 대기 중인 최대 연결 수를 지정할 수 있는 변수
    this(port, backlog, null);
}

 

기본적으로 ServerSocket 클래스를 이용해서 Socket을 만들 때 특정 로컬 포트를 매개변수로 전달해 객체를 생성하는데, 이때부터 클라이언트 측의 소켓이 연결하기를 기다리는 상태가 된다. 클라이언트가 연결을 할 때마다 Request Queue에 요청이 쌓이게 되는데 하나 하나의 요청에 대해 accept()를 해서 받아들인다. 이때 Request Queue에서 요청을 꺼내고 Socket 객체를 리턴하면 이 객체를 통해 클라이언트와 데이터를 송수신할 수 있는 것이다. 

 

 

아래 코드를 보면 위에 설명한 것과 같이 특정한 포트번호를 매개변수로 넘겨 ServerSocket을 만들었다. 그리고 Socket을 선언했는데 while문의 조건을 보면 accept() 되면서 Socket 객체가 실제로 반환됐는가를 확인해서 원하는 작업을 할 수 있도록 구현한 것을 알 수 있다. 

// 출처 : fastcampus
public void start() throws IOException {
    try (ServerSocket serverSocket = new ServerSocket(port)) {

        Socket clientSocket;

        while ((clientSocket = serverSocket.accept()) != null) {
            // Do something
        }
    }
}

 

 

그외 추가적으로 알 수 있는 내용은 다음과 같다.

  • ServerSocket 생성 시 포트 번호를 0으로 지정하면 시스템에서 포트 번호를 임의로 할당해준다.
  • OS는 ServerSocket 객체가 생성되는 즉시 클라이언트로부터 연결을 받을 수 있도록 준비한다.

 

 

주요 메서드

메서드 설명
Socket accept() 연결 요청을 기다리다가 요청이 들어오면 수락하고 새로운 Socket 객체를 반환한다.
void close() 서버 소켓을 닫는다.
InetAddress getInetAddress() 서버 소켓에 연결된 로컬 주소를 반환한다.
int getLocalPort() 서버 소켓이 연결 요청을 모니터링하는 포트 번호를 반환한다.
boolean isBound() 서버 소켓이 로컬 주소에 연결되어있으면 True를 반환한다.
boolean isClosed() 서버 소켓이 닫혀있으면 True를 반환한다.
void setSoTimeout(int timeout) accept()에 대한 타임 아웃 시간 지정, 0이면 해제한다.

 

 

 

Request Message 확인 실습

Socket끼리 연결을 완료한 이후에 clientSocket이 생성되었기 때문에 이제는 데이터를 주고받을 수 있다.

 

바이트 단위가 아닌 문자열을 처리하기 위해서 BufferedReader를 사용해서 InputStream을 감싸줬다.

// 출처 : fastcampus
public void start() throws IOException {
    try (ServerSocket serverSocket = new ServerSocket(port)) {

        Socket clientSocket;

        while ((clientSocket = serverSocket.accept()) != null) {

            try (InputStream in = clientSocket.getInputStream(); 
            OutputStream out = clientSocket.getOutputStream();) {
            
                BufferedReader br = 
                	new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));

                String line;
                while ((line = br.readLine()) != "") {
                    System.out.println(line);
                }
            }
        }
    }
}

 

Request Message가 제대로 수신되고 읽히는지 테스트 하기 위해 IntelliJ의 http 파일을 이용해 보겠다.

 

출력 결과

 

 

Request Message의 모든 항목이 제대로 잘 읽히는 것을 확인할 수 있다.

 

 

 

정리

  • Socket은 클라이언트와 서버 간의 통신을 위해 사용되는 엔드포인트이다.
  • java.net 패키지의 Socket 클래스로 클라이언트 측의 소켓을 생성하고, ServerSocket 클래스로 서버 측의 소켓을 생성할 수 있다.
  • 서버 측의 Socket이 만들어지면 클라이언트 측의 연결을 기다리는 대기 상태가 되고, 연결 요청이 들어오면 그 요청을 받아들임으로써 데이터 송수신 과정이 시작된다.

 

 

참고

  • <성공과 실패를 결정하는 1%의 네트워크 원리>
  • https://www.youtube.com/watch?v=BSdMtcWrHuE
  • 소켓 프로그래밍