[TIL] 소켓 프로그래밍 - 블로킹 소켓

KYJ의 Tech Velog·2024년 2월 27일
0

'게임 서버 프로그래밍 교과서'의 3장에 해당하는 내용입니다.

앞선 1,2장의 내용은 멀티스레딩과 컴퓨터 네트워크의 내용인데 운영체제와 네트워크 부문에서 많이 공부하였기 때문에 포스팅은 건너뛰도록 하겠습니다.


소켓 프로그래밍

네트워크 프로그래밍에서 가장 많이 사용하는 것이 소켓이라고 합니다.

소켓은 기본적으로 파일 핸들과 비슷하다고 하는데요. 디스크에 데이터를 기록하거나 책을 읽어 들일 때 파일 핸들을 사용합니다. 이와 비슷하게 네트워크로 데이터를 주고받을 때 소켓을 사용하죠.

하지만 온라인 게임 프로그래밍에서의 소켓은 파일 핸들과 약간 다릅니다.

  1. 게임 서버에서는 다루어야 하는 소켓의 개수가 많습니다. 만약 TCP 통신이라면 클라이언트 수만큼 소켓이 있어야 하죠.
  2. 소켓을 이용해서 데이터를 읽고 쓰고자 할 때 스레드가 대기하는 일이 없어야 합니다. 소켓을 이용해 읽기/쓰기 함수를 호출하였는데 바로 리턴하지 않는다면 이들을 호출한 메인 스레드는 사용자 입장에서 멈춰 있는 것처럼 보일 것입니다.

따라서, 소켓은 보통 비동기 입출력 상태로 다루어집니다. 이 때 두 가지 방식이 있는데요.

  • 논블로킹 소켓 방식
  • Overlapped I/O 방식

그리고 이 방식들을 발전시킨 epoll과 IOCP(I/O Completion Port) 방식을 많이 활용한다고 합니다.

먼저 동기 입출력 방식인 블로킹 소켓부터 알아보도록 하겠습니다.

블로킹 소켓

💡블로킹이란?
디바이스에 처리 요청을 걸어 놓고 응답을 대기하는 함수를 호출할 때 스레드에서 발생하는 대기 현상

블로킹이 발생한 스레드에서는 CPU 연산을 수행하지 않습니다. CPU 사용량이 0%가 된다는 거죠. 즉, 스레드는 waitable 상태인 것입니다.

스레드가 이 상태일 동안 파일이나 소켓의 실제 처리(어떤 함수의 실행)는 디바이스에서 합니다. 실제 처리가 디바이스에서 완전히 끝날 때까지 스레드는 waitable 상태를 유지합니다.

💡여기서 잠깐...
파일에 기록을 하는 함수를 호출했다면, 정확히는 데이터가 디스크에 기록이 된 시점이 아니라 RAM에 기록을 마친 후 즉시 블로킹이 끝나게 됩니다. 추후에 디스크로 옮겨지는 것이죠. 지금은 크게 중요한 내용은 아닙니다만 혹시 모르니...

실제 처리가 끝나게 되면 스레드는 running 상태로 바뀌고 함수는 리턴하며 다음 명령어를 실행하게 됩니다.

소켓은 다음과 같이 처리됩니다. 스레드에서 네트워크 수신을 하는 함수를 호출하면, 수신할 수 있는 데이터가 생길 때까지 스레드는 waitable 상태, 즉 블로킹이 발생합니다. 만약 통신하고 있는 상대방이 데이터를 보내지 않는다면 영원이 블로킹이 발생하게 되겠죠.

소켓 버퍼

TCP 소켓을 활용하여 통신하는 프로그램이 있다고 해봅시다. 데이터를 송신하는 함수를 호출하게 되면 상대방이 데이터를 수신했는지와 상관없이 즉시 리턴합니다. 상대방이 수신이 완료될때까지 블로킹이 걸리지 않는다는 이야기입니다. 심지어 데이터를 수신하지 못했을 수도 있죠.

소켓은 각각 송신 버퍼와 수신 버퍼를 하나씩 가지고 있습니다.

송신 버퍼는 일련의 바이트 배열로 기본 크기는 고정되어 있으나 원하는 대로 변경할 수 있습니다. 큐와 같은 방식인 FIFO 방식으로 작동됩니다. 사용자가 데이터 송신 함수를 호출하면 이 버퍼에 데이터가 채워지게 되고 잠시 후 통신 선로를 통해 빠져나가게 됩니다. 결국엔 빈 상태가 된다는 말이죠. (빠져나가게 되는 행위는 운영체제가 수행합니다.)

이 때, 만약 아직 송신 버퍼가 비워지지 않은 상태에서 데이터 송신 함수가 호출되면 이 함수는 블로킹이 걸리게 됩니다. 송신 버퍼에 이 데이터가 들어갈 공간이 생길 때까지 블로킹은 지속되게 됩니다.

송신 버퍼가 가득 채워지지 않은 정도의 데이터라면 데이터 송신 함수는 항상 즉시 리턴될 것입니다.

수신 버퍼는 송신 버퍼와 거의 비슷합니다. 눈에 띄는 차이는 작동 순서가 반대라는 것입니다. 송선 버퍼는 사용자가 데이터를 넣고 운영체제가 데이터를 빼냅니다. 수신 버퍼는 운영체제가 데이터를 넣고 사용자가 데이터를 빼내죠.

사용자는 소켓에서 데이터를 수신하는 함수를 통해 수신 버퍼에서 데이터를 꺼낼 수 있습니다. 만약 수신 버퍼가 비어 있다면 이 함수는 블로킹이 발생하게 되겠죠. 블로킹이 발생하는 상황도 완전히 반대입니다.

수신 버퍼가 가득 차게 되면??

TCP 수신 함수는 1바이트라도 수신할 데이터가 버퍼에 있으면 즉시 리턴합니다. 그 전까지는 계속 블로킹이 발생하게 되죠. 만약 수신 함수가 버퍼에서 데이터를 꺼내는 속도가 운영체제가 버퍼에 데이터를 채우는 속보다 느리면 어떻게 될까요?

일단 수신 버퍼에 남은 공간이 없을 때까지 데이터가 채워집니다. 그리고 TCP 송신 측에서 송신 함수가 블로킹됩니다. 만약 이 때 수신 측에서 수신 함수를 호출하지 않으면 TCP 통신은 전혀 없고 연결만 살아있는 상태가 됩니다.

UDP 소켓은 데이터그램이 최소 1개 도착해 있으면 즉시 리턴합니다. 데이터그램이 1개 도착할 때까지 블로킹되게 되죠. 이 때, 앞선 가정의 상황을 생각해봅시다.

UDP 데이터그램 A가 도착했을 때, 수신 버퍼의 A를 담을 공간이 없다면 A는 버려지게 됩니다. 이 때, 송신 함수에 블로킹이 걸리지 않는다는 것이 TCP와의 차이점입니다.

0개의 댓글