이번에 코드스쿼드 미션으로 웹서버를 구현해 보면서 공부한 내용을 정리해 보았다.

 

[기본으로 주어진 예제 코드]

try (ServerSocket listenSocket = new ServerSocket(port)) {
    Socket connection;
    
    while ((connection = listenSocket.accept()) != null) {
        Thread thread = new Thread(new RequestHandler(connection));
        thread.start();
    }
}

 

1. 소켓(Socket)과 포트(Port)


유튜브 널널한 개발자님

위 그림에서 TCP와 Process 사이 노란 원이 소켓을 표현한 건데 소켓은 유저 모드 애플리케이션에서 커널 영역에 접근하기 위한 통로 역할을 한다.

소켓의 본질은 파일(File)이라고 하는데 여기서 파일은 운영체제 커널 영역에 구현되어 있는 요소들에 대한 추상화된 인터페이스로 이때 프로토콜 요소(여기서는 TCP/IP 프로토콜)에 대해서 추상화를 한 경우 소켓이라고 부른다.

OS에서는 읽고 쓰는 것을 파일로 추상화해서 범용적인 의미로 사용하기 때문에 일반적으로 아는 파일, 디렉토리, 심볼릭 링크(바로가기) 소켓 등 많이 것들이 파일에 포함된다. 처음에는 소켓이 왜 파일인지 의아했는데 결국 소켓도 스트림으로 읽고 쓰는 것이라고 생각하니 조금 이해가 되었다.

 

김영한님 HTTP 강의

소켓의 스트림을 통해 데이터 <-> TCP(세그먼트) <-> IP(패킷) <-> Frame으로 전달이 되는데 여기서 TCP의 주요 특징인 포트가 나온다. 기존에 IP 패킷만 전달할 때는 신뢰성, 순서 보장 등의 문제도 있었지만 한 IP 내에서 여러 애플리케이션이 실행중일 때 어디로 전달이 되어야 하는지를 모르는 문제가 있었다.

 

 

그래서 프로세스를 식별하기 위한 포트 정보를 같이 전달하게 되는데 포트 번호는 16bit(2^16) 0 ~ 65535의 범위를 가지며 일반적으로 1~1023는 용도가 정해진 포트 번호들이 있고, 서버 소켓의 포트로는 1024 ~ 49151 범위로 사용하는 것 같다. 동적 포트는 보통 브라우저에서 클라이언트 쪽에 할당하는 포트 번호이다.

 

- Well Known 포트 : 0부터 1023 (root 권한으로 바인드)

- Registered 포트 : 1024부터 49151 (일반유저 권한으로 바인드)

- Dynamic/Private 포트 : 49152부터 65535 (클라이언트와 서버 간 통신 시 클라이언트가 사용하는 포트)

 

 

 

2. ServerSocket, Socket


 이제 예제 코드에서 사용하는 소켓을 보면 ServerSocket, Socket이 있다.

 

먼저 ServerSocket을 보면 생성자 파라미터로 포트 번호를 전달하고 있는데 해당 포트 번호로 바인딩을 시도하고 정상적으로 바인딩이 되면 서버 소켓은 LISTEN 상태가 된다.

sudo lsof -PiTCP -sTCP:LISTEN

LISTEN 상태에서 서버 소켓은 클라이언트의 연결 요청을 수신하며 TCP/IP 연결을 하고 연결이 되면 연결 정보를 내부적으로 큐에 관리한다. accept()를 호출하면 큐에 있는 연결 정보를 꺼내거나 없을 경우 현재 스레드에서 block 상태로 대기한다. (non-blocking 방식 socket도 있다고 한다.)

 

ServerSocket의 생성자를 보면 backlog로 큐의 사이즈를 설정할 수 있는데 기본 50으로 설정이 된다.

연결 요청을 관리하는 큐는 2개가 있는데 3 Way-Handshake 연결이 된 후 커넥션을 Accept Queue로 이동시키고 서버 소켓에서 accept()로 꺼낼 수 있다. 서버 소켓에서 설정한 backlog가 Accept Queue의 사이즈가 된다. (스프링 부트에서 톰켓의 backlog 값도 설정할 수 있다고 한다..!)

 

다시 예제 코드를 보면 accept()를 통해 Socket 인스턴스가 반환이 되는데 이 소켓은 클라이언트와 연결된 소켓으로 클라이언트쪽 소켓의 포트 정보를 가지고 있으며 Stream을 통해 데이터를 주고받을 수 있다. 즉, 서버쪽 포트를 바인딩하고 클라이언트 연결을 수신하는 ServerSocket클라이언트 연결이 accept() 될 때마다 생성되는 Socket이 있는 것이다.

 

accept()로 생성된 소켓에서 양쪽의 포트 정보를 확인할 수 있다.

 

예제에서는 8080번 포트를 사용했는데 "http://localhost:8080"처럼 뒤에 포트 번호를 붙여주고 전송을 하면 된다. RFC에 기본 포트로 지정이 되어 있는 80(HTTP) 443(HTTPS)로 포트 바인딩을 하면 scheme(HTTP, HTTPS)에 따라 포트 번호 생략이 가능하다.

 

 

3. Thread


예제 코드를 보면 Thread 인스턴스를 생성하고 thread.run() 또는 thread.start()를 호출해서 Runnable 타겟을 실행할 수 있는데 run()의 경우는 스레드가 생성되지 않고 현재 스레드에서 실행이 된다. 반면 start()는 스레드를 생성하고 해당 스레드에서 작업을 실행한다.

무엇을 사용하든 스레드 자체는 new Thread()를 할 때 생성이 되는 줄 알았는데 수업을 듣고 잘못 알고 있는 것 같아서 테스트를 해봤다.

Thread thread = new Thread(Runnable target);

// start()? run()?
thread.start();
thread.run();

 

 

먼저 thread.run()의 경우 blocking이 되고 Runnable의 동작이 시작해서 끝난 뒤에 CALL 메세지가 출력이 된다.

 

thread.start()로 변경한 결과 Runnable의 동작을 기다리지 않고 CALL 메시지가 먼저 출력이 된다.

사실 출력 내용만 봐도 run()은 main 스레드에서 실행을 하고 있고 start()는 Thread-n에서 실행을 하고 있다.

 

main 스레드를 sleep 시키고 thread.start()를 했을 때 스레드의 상태를 확인해 보았다.

반면 run()으로 테스트를 했을 때는 메인 스레드가 sleep이 풀릴 때까지 기다렸다가 실행이 된다.

 

new Thread()를 하면 힙 영역에 인스턴스가 생성되는 것뿐이고(이때 thread 상태는 NEW) 실제 스레드 공간이 할당되는 것은 start()를 호출할 때이다.

 

 

추가로 스레드 덤프에 대해 알게 되었는데 멀티스레드 환경에서 발생할 수 있는 스레드 간 경합, 데드락 등 여러 문제를 분석할 수 있다. 스레드가 제대로 반납이 되지 않을 때 스레드 덤프를 확인해 보면 좋을 것 같다.

현재 내 코드에서는 스레드풀을 사용하기 때문에 WAITING 상태로 대기하는 것을 볼 수 있었다.

 

 

[참고]

 

널널한 개발자님 유튜브, 김영한님 HTTP 강의

 

 

Java client socket returned by ServerSocket.accept()

This is more of a general socket question. In Java, if I have a ServerSocket bound to a specific port, say 4444, I understand that it's listening for connection requests. The accept() method blocks

stackoverflow.com

 

Linux man : listen - 소켓의 연결을 위한 대기열을 만든다.

서버측 프로그램은 socket(2)함수를 이용해서 클라이언트(:12)의 연결을 받아들일 듣기소켓을 만들게 된다. 클라이언트의 연결은 듣기소켓을 통해서 이루어지는데 클라이언트는 connect(2)를 호출해

www.joinc.co.kr

 

스프링 부트에서 socket backlog 모니터링과 튜닝법

 

www.manty.co.kr

 

http의 기본 포트가 80, https의 기본 포트가 443인 이유는 무엇일까?

80은 처음부터 지정, 443은 나중에 요청을 받아 빈 공간으로 순서대로 배정

johngrib.github.io

 

리눅스보안기본#1 : : 포트(port)의 개념과 포트 제어 > 강좌 | 클라우드포털

포트(port)의 개념과 포트 제어 클라이언트/서버 환경에서 시스템의 접속은 포트와 포트간의 통신이라고 할 정도로 포트의 의미는 매우 중요하다. 당연히 시스템 내에서 작동하고 있는 서비스를

www.linux.co.kr

 

개발자를 위한 레시피

Recipes for Developer.

recipes4dev.tistory.com

 

Java client socket returned by ServerSocket.accept()

This is more of a general socket question. In Java, if I have a ServerSocket bound to a specific port, say 4444, I understand that it's listening for connection requests. The accept() method blocks

stackoverflow.com

 

Thread Dump 분석

Thread Dump 생성 방법Unix : kill -3 [PID]window : Ctrl + Break공통 : jstack [PID]반드시 생성시 3~5회 연속으로 생성하여 문제 상황에 대한 변화 과정을 확인 Thread Dump 정보tid (Java-Level Thread ID )를 이용하여 정보

ijbgo.tistory.com

 

[JAVA] - Thread.start()와 Thread.run()의 차이

JAVA로 Thread 관련 프로그래밍을 학습하다보면 start() 메서드와 run() 메서드를 보게되는데 두 메서드를 실행하게되면 Thread의 run() 메서드를 실행하게 된다. 다만 이 두 메서드의 동작방식을 제대로

kim-jong-hyun.tistory.com

 

[Java] JVM Thread Dump 분석하기

java-study에서 스터디를 진행하고 있습니다. JVM Thread Dump 분석하기 스레드 덤프가 필요한 이유 웹 서버에서는 많은 수의 동시 사용자를 처리하기 위해 수십 ~ 수백 개정도의 스레드를 사용한다. 두

steady-coding.tistory.com

https://d2.naver.com/helloworld/10963

 

+ Recent posts