Java로 웹 서버를 직접 구현해보는 프로젝트를 진행하고 있다.
책, 구글링을 통해 어찌어찌 요청을 받는 부분을 ServerSocket을 통해 멀티 스레딩 + blocking 방식으로 구현했다.
하지만 나는 socket이라는 개념에 대해 모호하게 알고 있었고, 확실하게 이해하고자 TCP에 대한 공부를 진행했다.
그 내용을 간략하게 설명하면 아래와 같다.
1. TCP: IP의 Unreliable(패킷 유실 가능, 순서 보장X)한 특징을 보완하고자, Connection을 통해 신뢰성을 보장하는 프로토콜
2. connection: 프로세스 간의 안정적인 통신을 보장하는 논리적인 통로
3. socket: 시스템에서 애플리케이션이 네트워크 관련 기능을 사용할 수 있게 제공해주는 API이다. 애플리케이션은 socket을 통해 시스템과 데이터를 주고받는다.
4. socket 생성 과정: connection이 맺어지지 않은 요청이 오면, listening socket을 통해 connection을 수립하고, 데이터 송수신용 socket을 생성한다. 그리고 데이터 송수신은 이 socket을 통해 이루어진다.
이 개념들을 Java와 연결해봤을 때, "ServerSocket(Java)이 listening socket을, Socket(Java)이 데이터 송수신용 socket을 추상화한 것 아닐까?" 추측을 하게 되었다. 그리고 자세히 알아보기로 했다.
ServerSocket server = new ServerSocket(8080);
이처럼 ServerSocket 객체를 생성하게 되면 내부적으로 시스템 콜을 사용해서 8080 포트에 대한 리스닝 소켓을 생성할 것이다.
정말 그렇게 하고 있는지 코드를 통해 알아보자.


위 메서드 내부를 보면 getImpl()을 통해 bind()와 listen() 메서드를 호출한다.
getImpl() 메서드 내부에서 socket0(), bind() 내부에서 bind0(), listen()에서 listen0()라는 네이티브 코드를 최종적으로 호출하는 것을 PlainSocketImpl 클래스를 통해 알 수 있다. 아래는 그 메서드가 PlainSocketImpl에 구현되어 있는 부분이다.

각각의 네이티브 코드가 어떤 방식으로 동작하는지는 PlainSocketImpl.c의 소스 코드 여기에서 확인할 수 있다.
이렇게 내부적으로 네이티브 코드를 통해 socket() → bind() → listen() 세 가지 시스템 콜이 순차적으로 실행하면서 OS 커널에 리스닝 소켓을 만든다.
또한 socket(), bind(), listen() 등의 시스템 콜이 어떤 역할을 하는지는 리눅스 시스템 콜 설명 여기에서 간단하게 확인할 수 있다.
결론적으로 ServerSocket 생성자를 호출하면 내부적으로 시스템 콜을 통해 커널에 listening socket을 생성한다.
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
accept() 메서드를 사용하면 OS에서 3-way handshake가 완료된 요청을 가져올 수 있다.
ServerSocket의 소스 코드를 살펴보면


....
등등 여러 메서드를 호출하는 과정을 거치면서 아래의 메서드에 도착하면

impl.accept(si)를 통해 OS에서 연결을 받아 오는 것으로 보인다.
impl의 구현체 중 하나인 PlainSocketImpl의 내부 코드를 보면 아래와 같다.


여기에서도 이렇게 네이티브 코드를 호출하고 있고, 그 네이티브 코드가 어떻게 동자가하는지 찾아보면
PlainSocketImpl.c의 소스 코드에서 Java_java_net_PlainSocketImpl_socketAccept라는 함수에서 시스템 콜 accept()을 통해 TCP accept queue에서 커넥션을 꺼내서 반환하는 것으로 보인다.
ServerSocket의 accept()를 호출하면 이렇게 내부적으로 시스템 콜 accept()를 호출하여 OS의 accept queue에서 연결된 소켓 fd를 꺼내서 Socket 객체를 완성한다. 그리고 이 Socket을 통해 실질적으로 데이터 송/수신을 한다.
Java의 ServerSocket은 내부적으로 socket(), bind(), listen() 등의 시스템 콜을 호출해서 커널에 리스닝 소켓을 생성하고,
클라이언트의 연결 요청이 오면 커널이 3-way handshake를 통해 accept queue에 연결 소켓을 저장한 뒤,
accept() 호출 시 이를 꺼내와 Socket 객체로 wrapping하여 반환한다.
즉, Java의 ServerSocket과 Socket은 TCP의 연결 수립과 데이터 송수신을 커널 수준에서 처리하고 있다.
이번 분석을 통해 얻은 핵심 포인트는 다음과 같다.
1. ServerSocket은 커널에 리스닝 소켓을 생성하는 API이다.
2. 커널은 SYN queue, accept queue를 따로 관리하며, 3-way handshake 이후에만 accept()가 가능하다.
3. accept()는 소켓 fd를 받아 Java Socket 객체로 생성한다.
4. Java 네트워크 프로그래밍의 추상화 뒤에는 철저한 시스템 콜 기반의 네트워크 흐름이 존재한다.
+) socket에 대해서도 명쾌하게 이해할 수 있었다. socket이 시스템에서 애플리케이션이 네트워크 기능을 사용할 수 있게 제공하는 API라고 했는데, Java의 Socket에서도 다양한 메서드를 통해 네트워크 기능을 사용하기 때문이다.
+) tomcat에서는 NIO 방식으로 요청을 수신한다고 알고 있다. 이때 사용하는 api는 다를 수 있어도 결국 시스템 콜을 통해 요청을 수신할 것이다.
Java ServerSocket 소스 코드 (OpenJDK)