최근에는 네트워크 소켓 프로그래밍을 공부하고 있습니다.
소켓 프로그래밍을 하다보니 백로그 큐에 대해서 알게 되었고 이 부분에 대해 기록하고자 합니다.
소켓 프로그래밍 지식이 어느정도 있어야 이해하실 수 있습니다.
backlog queue는 listen 상태의 서버 소켓에 연결하고자 하는 클라이언트의 요청들이 저장되는 곳입니다.
조금 더 구체적으로 보면 클라이언트 애플리케이션에서 connect() 함수를 호출하면 여러 계층의 활약으로 무사히 서버 호스트로 접속 요청이 도착하게 되고 TCP 계층에서 이 요청을 받아서 backlog queue에 넣어주게 됩니다.
이제 서버 측 애플리케이션에서 accept() 함수가 호출 되면 이 backlog queue에서 접속 요청을 하나씩 꺼내와서 새로운 소켓에 그 요청 내용을 참조하여 소켓 내부에 필요한 내용을 저장하게 되고 그 소켓을 반환해서 반환된 소켓으로 클라이언트와 통신할 수 있게 되는 것입니다.
TCP 연결은 언제 이루어지는지 궁금해지더라구요.
connect() 함수를 호출하면 TCP 연결이 일단은 이루어지는 것일까?
아니면 accept() 함수가 호출이 되고 새로운 소켓이 반환이 되어야 연결이 이루어지는 것일까? 하는 의문이 생겼습니다.
그 의문을 해소하려면 3 way handshaking이 언제 발생하는지를 알아야 합니다. 3 way handshaking은 TCP의 최초 연결 동작을 말합니다. 이에 대한 자세한 내용은 구글에 검색하면 많이 나오니 모르시는 분은 이해하고 넘어와 주세요.
아무튼 3 way handshaking은 3번의 패킷 교환이 이루어지므로 이 패킷을 캡처하면 언제 발생하는지 알 수 있습니다.
그래서 패킷 캡처 툴인 와이어 샤크를 이용해 보았습니다.
우선 클라이언트 측에서 connect()를 호출하고 서버 측에서는 accept()를 호출하지 않고 패킷을 캡처해보았습니다.
그랬을 때 3 way handshaking 패킷을 확인 할 수 있었습니다.
그러므로 accept() 함수의 호출과 TCP 연결은 무관하다는 사실을 알 수 있습니다. accept() 함수는 그저 backlog queue에 있는 연결되어 있는 클라이언트 요청 데이터를 꺼내서 연결된 소켓을 반환해주는 역할을 하는 것입니다.
listen() 함수를 호출할 때 매개변수로 개발자가 직접 backlog queue의 크기를 설정할 수 있습니다.
listen(listenSocket, SOMAXCONN);
listen() 함수의 두 번째 매개변수가 바로 backlog queue의 크기입니다.
SOMAXCONN은 무슨 값일까요? MSDN의 listen() 함수 설명을 보면 다음과 같이 설명합니다.
SOMAXCONN으로 설정하면 소켓을 담당하는 기본 서비스 공급자가 백로그를 적절한 최대 값으로 설정합니다.
https://learn.microsoft.com/ko-kr/windows/win32/api/winsock2/nf-winsock2-listen
적절한 최대 값이라... 한 번 알아봐야겠습니다.
알아보는 과정은 다음과 같습니다.
- 서버 프로그램에서는 listen() 과정까지만 해서 특정 포트를 LISTENING 상태로 만들어 놓고 accept()는 하지 않습니다. 이 때 backlog 매개 변수로 SOMAXCONN을 전달합니다.
- 클라이언트 프로그램에서는 많은 수의 connect() 함수를 호출하면 계속해서 backlog queue에 연결 요청이 쌓이게 되고 가득 차게 되면 SOCKET_ERROR가 발생할 것입니다.
- 언제 SOCKET_ERROR가 발생하는지 보면 기본 서비스 공급자가 설정한 적절한 최대 값을 알 수 있을 것입니다.
클라이언트 측 소켓을 1024개 만들어서 connect()를 호출해 보았더니
200에서 에러가 터졌습니다.
그러므로 적절한 backlog queue의 크기는 200이었던 것입니다.
더 많은 연결 요청이 짧은 시간에 들어온다고 하면 이 backlog queue의 크기를 더 늘리는 법도 알아야 할 것 같습니다.
MSDN 문서를 보면 이 크기를 늘리는 방법이 나와있습니다.
SOMAXCONN_HINT(N)(여기서 N은 숫자)로 설정하면 백로그 값은 N이 되고 범위 내에서 조정됩니다(200, 65535). SOMAXCONN_HINT 사용하여 SOMAXCONN에서 백로그를 가능한 것보다 큰 값으로 설정할 수 있습니다.
문서를 보면 65535개가 최대치인 것 같습니다. (포트의 한계 때문인 것 같습니다.)
65535개의 연결요청이 잘 들어가는지 확인해 보도록 하겠습니다.
65540개 정도의 클라이언트 소켓을 만들어서 connect를 해본 결과 16333에서 오류가 발생했습니다. 그런데 에러코드를 보니 기존에 backlog queue가 가득 차서 발생한 오류와는 다른 WSAENOBUF 에러가 발생했습니다.
처음에는 아마 내부 송 수신 버퍼 공간이 없어서 그런 것 같다고 생각했습니다.
그런데 x64의 메모리는 부족하지 않을 것 같다는 생각이 들었습니다.
계속 어떻게 하면 65535개의 연결 요청을 backlog queue에 넣을까 고민을 하다가... 65535개... 최대 포트 수... 혹시 동적 포트를 다 써서 그런 것일까?라는 실마리를 찾게 되었습니다.
그래서 구글에 동적 포트의 범위를 검색했더니
49152 - 65535였습니다.
두 수의 차는 16383... 어딘가 16333과 굉장히 유사한 숫자...!!
(50개는 다른 프로세스에서 쓰고 있는 듯 합니다.)
결론을 내리자면 내부 송,수신버퍼의 공간이 부족한 것이 아닌
65535개의 클라이언트 소켓에 할당할 포트가 부족했던 것이었습니다.
제 목표는 backlog queue에 65535개의 연결 요청을 넣어보는 것입니다.
어떻게 부족한 포트 문제를 해결할까 고민했습니다.
고민을 계속 하다가 모르겠어서 그냥 침대에 누워서 자려고 하는데... 정말 스치듯이 connect()한 클라이언트 소켓을 바로 closesocket()해버리면 동적 포트를 재사용할 수 있지 않을까? 하는 아이디어가 떠올랐습니다.
저는 바로 책상 앞에 앉아 노트북을 켜고 실험을 해보았습니다.
우선 클라 소켓을 closesocket()을 했을 때 서버 측의 backlog queue에 들어가 있던 연결 요청이 사라지지는 않을까? 하는 의문을 해결해야 했습니다.
(사실 연결 요청이 사라지지 않을 것이라고 예측을 했습니다.
연결 요청을 그냥 큐에 넣은 건데 close 했다고 실시간으로 큐의 내용을 지우진 않을 것 같았거든요.)
실험으로 2개의 클라이언트 소켓으로 connect() 후 바로 closesocket()을 했습니다. 서버측에서는 Sleep(5000)으로 5초 정도 지연시켜주고 accept()를 호출해서 요청을 2번 받는지 확인 했더니.
두 요청을 다 잘 받았습니다.
그러므로 closesocket()으로 포트를 재활용할 수 있겠습니다.
희망을 얻은 것도 잠시 TCP 연결을 해제 할 때는 closesocket()을 하면 소켓은 바로 사라지지 않고 Time_wait 상태가 됩니다. 즉 바로 사라지지 않고 60초 정도 기다렸다가 사라집니다. 그 기다리는 동안에는 해당 포트를 사용할 수 없는 것이죠.
하지만 구글링한 결과 소켓에 Linger 옵션을 설정하면 time_wait을 상태를 없앨 수 있다는 사실을 알게 되었습니다.
https://sunyzero.tistory.com/198
그렇게 time_wait을 없애고 포트를 재사용하니
드디어
backlog queue에 65535개의 연결 요청을 넣을 수 있었습니다!!
backlog queue에 최대로 넣을 수 있는 요청을 알아보고 실제로 넣어보는 과정이 끝났습니다.
네트워크 이론을 배우고 그 이론을 토대로 실험하고 검증하는 과정이 괴로우면서도 즐거웠습니다.
앞으로 게임 서버 공부를 하면서 backlog queue에 대해서는 잊지 않을 것 같습니다.
감사합니다.