ved_Rony
article thumbnail

네트워크에서 데이터를 주고 받을 때 소켓을 사용한다. 게임 프로그래밍에서 데이터를 주고 받는 방식의 차이가 있다.

  • TCP로 통신하는 경우 클라이언트 개수만큼 소켓이 있어야 하므로 게임 서버에서 다루어야 하는 소켓 개수가 많다.
  • 파일 핸들을 하는 동안 스레드가 대기하는 일이 없어야 한다.

이러한 특징으로 네트워크 프로그래밍에서 소켓은 보통 비동기 입출력 상태로 다룬다. 비동기 입출력 방식에는 크게논 블로킹 소켓 방식과Overlapped I/O 방식이 있다. 그리고 이 방식을 진보시킨 epoll과 IOCP(I/O Completion Port) 방식이 많이 활용된다

 

블로킹 소켓

디바이스에 처리 요청을 걸어 놓고 응답을 대기하는 함수를 호출할 때 스레드에서 발생하는 대기 현상을 블로킹이라 한다. 소켓뿐만 아니라 파일 핸들에 대한 함수를 호출했을 때도 이러한 대기 현상이 발생하는 것을 모두 블로킹이라 한다.

 

블로킹이 발생한 스레드에서는 CPU 연산을 하지 않는다. 즉, 스레드는 waitable state인 상태이다. 이 상태일 때 파일이나 소켓의 실제 처리는 디바이스에서 한다. 작업이 끝나면 상태는 다시 running 상태로 바뀐다.

 

블로킹과 소켓 버퍼

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

 

송신 버퍼는

  • 바이트 배열이며 크기는 고정되어 있으나 크기를 마음대로 변경할 수 있다.
  • FIFO 형태로 작동한다.
  • send(data)를 호출하면 data는 송신 버퍼에 채워지고 잠시 후 통신 선로를 통해 점차적으로 빠져나간다.
  • 송신 버퍼가 가득 찬다면 블로킹이 발생하고 빈 공간이 생기면 블로킹이 해제되고 send() 함수를 리턴한다.

수신 버퍼는

  • 송신 버퍼와 많은 부분이 비슷하지만 작동 순서가 반대이다. 송신 버퍼에는 사용자가 push()하고 운영체제가 pop()을 하지만 수신 버퍼에는 운영체제가 push()하고 사용자가 pop()을 한다.
  • 데이터가 수신되는 것이 있을 때마다 계속해서 수신 버퍼에 채워주는데, 꽉 차면 더 이상 데이터를 받지 않는다.
  • 수신 버퍼가 완전히 비어 있으면 블로킹이 발생한다.

만약 수신 함수가 수신 버퍼에서 데이터를 꺼내는 속도가

운영체제가 수신 버퍼의 데이터를 채우는 속도보다 느리면 => 함수 <  OS

 

TCP 수신 함수인 recv()는 1바이트라도 수신할 수 있으면 즉시 리턴

  • 수신 버퍼에는 남은 공간이 하나도 없을 때까지 완전히 채워지게 되고 데이터를 보내는 쪽에서는 송신 함수 send()가 블로킹된다. 이 상태에서는 TCP 통신은 전혀 없고 TCP 연결만 살아 있는 것이다.

 

UDP 소켓에서는 데이터그램이 최소 1개 도착해 있으면 즉시 리턴

  • 수신 버퍼가 담을 여유 공간이 없으면 데이터그램은 그냥 버려진다. 이때 송신 함수 sendTo()의 블로킹은 발생하지 않는다. 즉, 데이터그램 유실이 발생한다.

이제 블라킹이 뭔지 소켓 버퍼가 어떻게 작동을 하는 지 보았다.

 

논블록 소켓 & Overlapped I/O 혹은 비동기 I/O

대부분 운영체제에서는 소켓 함수가 블로킹되지 않게 하는 API를 추가로 제공한다. 이를 논블록 소켓이라 한다.

논블록 소켓을 사용하는 방법은 아래와 같다.

 

void NonBlockSocketOperation()
{
    result = s.connect();
    if (result = = EWOULDBLOCK)
    {
        while (true)
        {
            byte emptyData[0]; // 길이 0인 배열
            result = s.send(emptyData);
            if (result == OK)
            {
                // 연결 성공 처리
            }
            else if (result == ENOTCONN)
            {
                // 연결이 아직 진행 중이다.
            }
            else
            {
                // 연결 실패 처리
            }
        }
    }
}

 연결 함수에서 리턴된다면 문제가 발생한다. 잘 연결된 것인지 확인 할때, 다시 connect()를 다시 시도하는 것보단 0바이트 송신 방식을 통해 확인하는 것이 좋다.

 

논블록 소켓의 단점

  • 소켓 I/O 함수가 리턴한 코드가 would block인 경우 재시도 호출 낭비가 발생한다.
  • 소켓 I/O 함수를 호출할 때 입력하는 데이터 블록에 대한 복사 연산이 발생한다.

이러한 단점을 해결해주는 것이 Overlapped 또는 Asynchronous(비동기) I/O이다.

  • 소켓에 대해 Overlapped 액세스를 걸고 성공했는지 확인 후 성공했으면 결괏값을 얻어 와서 나머지를 처리한다.
  • 완료되기 전까지 Overlapped status 객체가 데이터 블록을 중간에 훼손하지 말아야 한다.

논블록 소켓과 Overlapped I/O은 소켓 개수가 많을 때 루프를 돌며 처리하기 때문에 성능 문제가 생긴다.

 

그래서 등장한 것이 IOCP와 epoll이다.

 

epoll

위의 그림에서 I/O가 가능한 상태의 소켓2를 큐에 넣어주고, 사용자가 이벤트 정보를 pop할수 있다. 따라서, 소켓이 아무리 많아도 I/O 가능 상태인 소켓만 가져와 사용할수 있다.

 

IOCP

epoll과 비슷하다. 차이점이라고 한다면, I/O 가능 상태가 아니라 완료 상태인 소켓을 알려준다이다.

 

epoll과 IOCP 비교

profile

ved_Rony

@Rony_chan

검색 태그