728x90
3장 소켓 프로그래밍
- 온라인 게임 프로그래밍에서 소켓은 파일 핸들 방식과는 약간 다르다.
- 게임 서버에서는 다루어야 하는 소켓 개수가 많다. TCP를 이용해서 통신하는 경우 클라이언트 개수만큼 소켓이 있어야 한다.
- 파일 핸들을 하는 동안 스레드가 대기하는 일이 없어야 한다. 디스크를 읽거나 쓸 때 사용하는 read(), write() 함수는 호출 후 실행이 완료될 때 까지 리턴하지 않는다. 소켓을 이용해서 읽기/쓰기를 하는 함수를 호출 했는데 즉시, 리턴하지 않는다면 이들을 호출한 메인 스레드는 사용자 입장에서 일시정지 하는 것 처럼 보인다.
- 이러한 이유 때문에 네트워크 프로그래밍에서 소켓은 보통 비동기 입출력(Asynchronous I/O) 상태로 다룬다. 소켓을 비동기 입출력으로 다루는 방식에는 크게는 블로킹 소켓(non-blocking socket) 방식과 overlapped I/O 방식이 있다. 그리고 이 방식을 진보시킨 epoll 과 I/O Completion Port(IOCP) 방식이 많이 활용된다.
3.1 블로킹 소켓
- 디바이스에 처리요청을 걸어놓고 응답을 대기하는 함수를 호출할 때 스레드에서 발생하는 대기 현상을 블로킹이라고 한다. 소켓뿐만 아니라 파일 핸들에 대한 함수를 호출했을 때도 이러한 대기 현상이 발생하는 것을 모두 블로킹이라고 한다.
- 스레드에서 네트워크 수신을 하는 함수를 호출하면, 수신할 수 있는 데이터가 생길 때까지 스레드는 블로킹이 발생한다.
3.2 네트워크 연결 및 송신
- TCP는 연결 지향형 프로토콜이다. 그리고 일대일 통신만 허락한다. 따라서 TCP 소켓 1개는 오직 끝점 1개하고만 통신할 수 있다.
3.3 블로킹과 소켓 버퍼
- 소켓 각각은 송신 버퍼와 수신 버퍼를 하나씩 가지고 있다. 송신 버퍼는 일련의 바이트 배열이라고 보면 된다. 송신 버퍼의 크기는 고정되어 있으나, 마음대로 크기를 변경할 수 있다.
- 송신 버퍼는 큐와 마찬가지로 FIFO 형태로 작동한다. send(data)를 호출하면 data는 일단 송신 버퍼에 채워지고, 채워진 data는 잠시 후 통신선로를 통해 점차적으로 빠져나간다. 따라서 송신 버퍼는 뭔가가 채워지더라도 곧 빈 상태가 된다.
3.4 네트워크 연결 받기 및 수신
- 수신 버퍼 안에는 데이터가 수신되는 것이 있을 때 마다 계속해서 채워준다. 즉, 수신 버퍼를 방치하면 격국 꽉 차게 된다. 꽉 차면 더 이상 데이터를 받지 않는다.
- 사용자는 소켓에서 데이터를 수신하는 함수를 호출하여 수신 버퍼에서 이미 수신된 데이터를 꺼낼 수 있다. 수신 버퍼가 완전히 비어 있으면 데이터를 수신하는 함수는 블로킹이 일어난다.
3.5 수신 버퍼가 가득 차면 발생하는 현상
- 수신 버퍼가 꽉 차면 송신 함수인 send()가 블로킹된다. 극단적으로 이 상태에서 TCP recv()를 전혀 하지 않으며 send()도 계속 블로킹 상태를 유지한다. 이 상태에서는 TCP 통신은 전혀 없고, TCP연결만 살아있다.
- TCP 송신 함수로 송신 버퍼에 데이터를 쌓는 속도보다 수신 함수로 수신 버퍼에서 데이터를 꺼내는 속도가 느리면, 데이터그램 유실이 발생한다.
- 라우터는 여러 곳에서 도착하는 패킷을 처리한다. 라우터가 초당 처리할 수 있는 패킷양을 넘어서는 패킷이 라우터에 도착할 때 라우터는 패킷을 늦게 처리하거나 버린다.
- TCP는 송신자가 초당 보내는 데이터양이 수신자가 초당 수신할 수 있는 데이터 양보다 많을 때, 송신자 측 운영체제가 알아서 초당 송신량을 줄인다. 따라서 송신자와 수신자 사이의 다른 네트워킹이 경쟁에서 밀리지 않는다. UDP에는 이러한 제어 기능이 없다. 따라서 UDP를 속도 제한 없이 마구 송신하면 주변의 네트워킹이 경쟁에서 밀린다. 이 때문에 주변의 네트워킹이 두절되기도 하는데, 이러한 현상을 혼잡현상이라고 한다.
3.6 논블록 소켓
- 운영체제에서 소켓함수가 블로킹되지 않게하는 API를 추가로 제공한다. 이를 논블록 소켓이라고 한다.
논블록 소켓을 사용하는 방법
- 소켓을 논블록 소켓 모드로 전환합니다.
- 논블록 소켓에 대해 평소처럼 송신, 수신, 연결과 관련된 함수를 호출한다.
- 논블록 소켓은 무조건 이 함수의 호출에 대해 즉시 리턴한다.
- 논 블로킹 연결 함수가 would block을 리턴한 후에는 would block이 끝났는지 알고자 다른 방법을 사용하는게 좋다. 여러 방법이 있지만 ‘0바이트 송신’이라는 요령이 있다. TCP는 스트림 기반 프로토콜이기 때문에 0바이트를 보내는 것은 사실상 아무것도 하지않는 셈이다. 그리고 0바이트를 보내려는 시도를 하면 TCP 소켓이 현재 어떤 상태인지 알 수 있다.
- 0바이트 송신하는 함수가 ‘성공’을 리턴하면 TCP 연결되어 있다는 의미다.
- ENOTCONN(소켓이 연결되어 있지 않음)을 리턴하면 TCP 연결이 진행 중이다.
- 기타 오류 코드가 나오면 TCP 연결 시도가 실패한 것이다.
3.7 Overlapped I/O 혹은 비동기 I/O
논블록 소켓의 장점
- 스레드 블로킹이 없으므로, 중도 취소 같은 통제가 가능하다.
- 스레드 개수가 1개이거나 적어도 소켓을 여러 개 다룰 수 있다.
- 스레드 개수가 적거나 1개 이므로 연산량이 낭비되지 않는다. 그리고 호출스택 메모리도 낭비되지 않는다.
논블록 소켓의 단점
- 소켓 I/O 함수가 리턴한 코드가 would block인 경우 재시도 호출 낭비가 발생한다.
- 소켓 I/O 함수를 호출할 때 입력하는 데이터 블록에 대한 복사 연산이 발생한다.
- connect() 함수는 재시도 호출 하지 않지만, send() 함수나 receive() 함수는 재시도 호출해야 하는 API가 일관되지 않는다는 문제가 있다.
- 논블록 소켓에서 재시도용 호출의 낭비가 어떤 경우에 있는지 알아보자
- TCP 소켓의 send() 처리의 경우 송신 버퍼에 1바이트라도 비어있으면 I/O 가능이 된다. 이 상태에서 TCP 소켓에 대해 send 함수를 호출하면 would block은 절대 발생하지 않는다. 보내려는 데이터가 5바이트인데 송신 버퍼에 빈공간이 1바이트라면, 1바이트만 소켓 송신 버퍼에 채워진다. 그리고 성공적으로 리턴한다.
- TCP 소켓의 receive() 처리 부분은 수신 버퍼에 1바이트라도 들어 있으면 I/O 가능이 된다. 이 상태에서 TCP 수신, 즉 recv() 함수를 호출하면 수신 버퍼에 있을것을 꺼내온다. 따라서 would block이 발생하지 않으며 UDP 소켓의 receive 처리 역시 문제가 없다. 하지만 UDP 소켓의 send() 처리에서는 문제가 있다.
- UDP 소켓의 송신 버퍼에 1바이트라도 비어 있다면 I/O 가능이다. 그런데 보내려는 데이터가 5바이트인데 송신 버퍼 빈 공간이 1바이트라면, 넣을 수 있는 크기를 넘어선다. TCP와 달리 UDP는 일부만 보낼 수 없으므로 would block이 발생한다. I/O 가능이라 재시도를 하는데, 여전히 would block이다. 이는 CPU 낭비로 이어진다.
- overlapped 또는 Asynchronous I/O는 재사용 호출 낭비 문제와 소켓 함수에 데이터 블록 복사 문제를 모두 해결해준다.
Overlapped I/O
- 소켓에 대해 overlapped 액세스를 건다
- overlapped 액세스가 성공했는지 확인한 후 성공했으면 결과 값을 얻어와서 나머지를 처리한다.
Overlapped I/O를 다루는 법
- overlapped I/O를 걸 때 진행중인 상태현황을 보관하는 구조체를 먼저 준비한다.
- 블로킹 소켓을 그대로 사용한다.
- 소켓에 대한 overlapped I/O 전용 함수를 호출한다. 전용 함수는 항상 즉시 리턴한다.
- 즉시 성공 했다면 OK가 리턴되고, 그렇지 않으면 완료를 기다리는 중, 즉 I/O Pending이라는 값이 즉시 반환된다.
- overlapped I/O 완료 여부는 확인하는 함수를 호출해서 확인한다.
- 5에서 완료라는 결과가 나오면 그냥 나머지 처리를 한다.
- overlapped I/O를 수신했다면 data 객체에 수신된 데이터가 자동으로 채워져 있을 것이다. 이를 액세스 한다.
- overlapped I/O 함수는 즉시 리턴되지만, 운영체제로 해당 I/O 실행이 별도로 동시간대에 진행되는 상태이다. 놀랍게도 운영체제는 소켓 함수에 인자로 들어갔던 데이터 블록을 백그라운드에서 액세스 한다.
- 따라서 호출한 overlapped I/O 전용 함수가 비동기로 하는 일이 완료될 때 까지는 소켓 API에 인자로 넘긴 데이터 블록을 제거하거나 내용을 변경해서는 안된다. 그뿐만 아니라 overlapped I/O 전용함수의 인자로 overlapped status 구조체가 같이 들어가는데, 완료 여부는 이 구조체로 알 수 있다.
- overlapped status 구조체 또한 운영체제에서 백그라운드로 액세스 중이다. 따라서 중간에 없애거나 내용을 변경해서도 안된다.
- 소켓은 내부에 송수신 버퍼를 가지고 있다. 소켓 안 송수신 버퍼의 크기는 마음대로 변경할 수 있으며, 심지어 0도 가능하다. 0으로 설정하면 overlapped I/O는 다르게 작동한다.
- overlapped I/O 전용 송수신 함수를 호출하면 운영체제는 송신할 데이터가 저장되어 있는 메모리 블록 자체를 송신 버퍼로 사용해 버린다. 수신할 때도 수신 데이터가 있으면 수신 버퍼에 일단 온 후에는 수신 데이터 블록에 복사되지 않는다. 즉시 수신 데이터 블록에 수신 데이터가 쌓이게 된다.
Overlapped I/O 장점
- 장점
- 소켓 I/O 함수 호출 후 would block 값인 경우 재시도 호출 낭비가 없다.
- 소켓 I/O 함수를 호출할 때 입력하는 데이터 블록에 대한 복사 연산을 없앨 수 있다.
- send, receive, connect, accept 함수를 한 번 호출하면 이에 대한 완료 신호는 딱 한 번만 오기 때문에 프로그래밍 결과물이 깔끔하다.
- 뒤에서 설명할 I/O Completion Port와 조합하면 최고 성능의 서버를 개발할 수 있다.
- 단점
- 완료되기 전까지 overlapped status 객체가 데이터 블록을 중간에 훼손하지 말아야 한다.
- 윈도 플랫폼에서만 제공하는 기능이다.
- accept, connect 함수 계열의 초기화가 복잡하다.
논블록 소켓에서 I/O Operation 상태
- 송신 버퍼에 1바이트라도 여유 공간이 있으면 송신 가능, 즉 send available이라고 한다.
- 수신 버퍼에 1바이트라도 여유 공간이 있으면 수신 가능, 즉 receive available이라고 한다.
- 이를 통칭 I/O 가능이라고 한다.
- Overlapped I/O에서 I/O 실행(Operation) 상태는 다음과 같이 설명할 수 있다.
- overlapped 송신이 진행중이고 완료가 아직 안되었으면 overlapped 송신이 아직 완료 대기중(pending)이라고 한다.
- overlapped 수신이 진행중이고 완료가 아직 안되었으면 overlapped 수신이 완료 대기 중 이라고 한다.
- 이를 I/O 완료 대기 중 혹은 I/O 실행 중 이라고 한다.
- 논블록 소켓에서는 상태 확인 후 뭔가를 하고, overlapped I/O에서는 일단 저지른 후 결과를 확인한다. 그래서 논블록 소켓을 ‘뒤늦게’ 라는 의미의 리액터 패턴(reactor pattern)이라고 한다. overlapped I/O는 ‘미리’ 라는 의미의 프로액터 패턴(Proactor pattern) 이라고 한다.
리액터 - 논블록 소켓
- I/O를 시도한다(성공할 수도 있고, 실패할 수도 있다.)
- 실패할 때는 I/O 기능을 기다린 후 I/O를 재시도 한다.
- 성공할 때는 상황을 종료한다.
프로액터 - Overlapped I
- I/O를 시행(무조건 성공) 한다.
- I/O 완료를 기다린다.
- 상황을 종료한다.
3.8 epoll
- epoll은 소켓이 I/O 가능 상태가 되면 이를 감지해서 사용자에게 알림을 해주는 역할을 한다. 이때 어떤 소켓이 I/O 가능 상태인지 알려준다.
- 소켓이 I/O 가능이 되는 순간 epoll은 이 상황을 epoll 안에 내장된 큐에 푸시한다. epoll에서 이러한 이벤트(사건) 정보를 팝(pop)할 수 있다. 이렇게 해서 어떤 소켓이 I/O 가능인지 알 수가 있다.
- epoll은 리눅스와 안드로이드에서만 사용가능하다. ios,macos,freebsd에서는 epoll과 매우 유사한 kqueue를 사용하면 된다.
epoll 사용법
- 먼저 epoll 객체를 만든다.
- 여러 소켓을 epoll에 추가한다. 추가된 소켓의 I/O가능 이벤트는 epoll로 감지할 수 있다. 소켓을 추가할 때는 정의한 소켓 소유자 객체등 원하는 아무 값을 같이 넣을 수 있다.
- 모든 소켓에 대한 select() 대신 epoll에서 이벤트를 꺼내오는 함수를 호출한다. 이 함수는 사용자가 원하는 시간까지만 블로킹 되며, 그전에 이벤트가 생기는 순간 즉시 리턴한다. 출력 값에는 꺼내온 이벤트들이 있다.
- 각 이벤트에 대해 루프를 돌며 이벤트가 가리키는 소켓 객체와 사용자 정의 값을 꺼내온다.
- 원하는 처리를 한다.
- 이렇게 select()를 쓰면 모든 소켓에서 루프를 돌아야 하는것을 epoll을 쓰면 I/O 가능인 상태의 소켓에서만 루프를 돌면된다. 하지만 현실에서는 소켓의 송신 버퍼가 빈 공간이 없는 순간을 유지하는 시간이 상대적으로 짧다. 거의 대부분은 송신 가능이다.
- 이러한 특징 때문에 지금까지 설명한 방식으로 만들면 필요 이상의 루프를 돌아야 해서 불필요한 CPU 연산 낭비가 일어난다. 이 문제를 해결하려면 레벨 트리거 대신 에지 트리거를 써야한다. epoll에서 말하는 레벨 트리거는 소켓이 I/O 가능하다를 의미한다. 에지 트리거는 소켓이 I/O 가능이 아니었는데, 이제 가능이 되었다를 의미한다. 따라서 레빌 트리거는 I/O 가능인 이상 epoll에서 항상 꺼내지지만, 에지트리거는 I/O 가능이 아니었다가 가능으로 변하는 순간에만 꺼내어진다.
에지트리거 주의사항
- I/O 호출을 한 번만 하지 말고 would block이 발생할 때까지 반복해야 한다
- 소켓은 논블록으로 미리 설정되어 있어야 한다.
3.9 IOCP
- epoll은 논블록 소켓을 대략으로 갖고 있을 때 효율적으로 처리해주는 API이다. 이에 질세라 Overlapped I/O를 다루는 운영체제에서 대응한 것이 I/O Completion Port이다.
- IOCP는 소켓의 overlapped I/O가 완료되면 이를 사용자에게 알려주는 역할을 한다. 소켓이 완료되는 순간 IOCP는 이 상황을 내장된 큐에 푸시한다. 그리고 사용자는 IOCP에서 I/O가 완료되었음을 알려주는 완료신호(Completion event)를 꺼낼 수(pop)있다.
IOCP 사용법
- IOCP 객체를 만든다.
- 여러 소켓과 소켓에 대응하는 원하는 정수값(epoll의 userptr과 동일)을 IOCP에 추가한다.
- 추가된 소켓의 I/O 이벤트는 IOCP를 이용하여 감지할 수 있다. I/O를 하고 싶다면 overlapped I/O를 건다.
- 모든 overlapped status 객체에 대한 GetOverlappedResult 대신 IOCP에서 완료 신호를 꺼내오는 함수를 호출한다. 이 함수는 사용자가 원하는 시간까지만 블로킹되며, 그 전에 이베트가 생기면 즉시 리턴한다. 출력 값에는 꺼내온 이벤트들이 있다.
- 각 이벤트에 대해 루프를 돌면서, 이벤트가 가리키는 overlapped status 객체와 대응하는 userptr 값을 꺼내 온 후 원하는 처리를 하면된다. 추가로 I/O를 계속하고 싶다면 overlapped I/O를 또 건다.
- epoll은 I/O 가능인 것을 알려주지만, IOCP는 I/O 완료인 것을 알려준다.
- IOCP는 epoll보다 사용법에 복잡한 기능이 몇 가지 있는데 그 중 하나가 Accecpt 기능이다.
- IOCP에 listen socket L을 추가했다면 L에서 TCP 연결을 받을 경우 이에 대한 완료 신호가 IOCP에 추가된다.
- 단 사전에 이미 AcceptEx로 Overlapped I/O를 건 상태여야 한다.
- IOCP로 L에 대한 이벤트를 얻어왔지만, 앞서 Overlapped accept 처럼 SO_UPDATE_ACCEPT_CONTEXT와 관련된 처리를 해주어야 새 TCP 소켓 핸들을 얻어올 수 있다.
- 반대로 IOCP는 epoll에서는 할 수 없는 성능상 유리한 기능도 있다. 앞서 스레드 풀링을 활용하면 여러가지 일을 스레드 몇 개가 골고루 분담해서 처리할 수 있다는 것을 배웠다 IOCP는 이러한 스레드 풀을 쉽게 구현할 수 있게하는 반면 epoll은 그렇지 않다.
IOCP
- overlapped I/O를 통해 블로킹을 없앤다.
- overlapped I/O를 건다
- 완료신호를 꺼낸다
- 완료신호에 대한 나머지 처리를 한다
- 끝나고나서 다시 overlapped I/O를 건다
epoll
- 논블롯 소켓을 통해 블로킹을 없앤다.
- I/O 이벤트를 꺼낸다.
- 꺼낸 이벤트에 대응하는 소켓에 대한 논블록 I/O를 실행한다.
- IOCP는 epoll보다 사용법이 복잡하다 대신에 성능상 유리한 기능이 몇 있다.
- epoll을 쓰는 리눅스에서는 TCP 소켓으로 수신을 한 후에 데이터 수신을 하려면 소켓 수신 함수(recv)를 이어서 호출해주어야 한다. 그러나 IOCP를 쓰는 윈도 서버에서는 연결받기와 수신을 소켓 함수 호출 한 번으로 끝낼 수 있다. 부가적으로 연결된 소켓의 끝점 정보를 얻는것도 같이 끝내버릴 수 있어 프로그램 최적화에 유리하다.
728x90
'책 > 게임 서버 프로그래밍 교과서' 카테고리의 다른 글
게임 서버 프로그래밍 교과서 7장. 데이터베이스 기초 (0) | 2024.04.12 |
---|---|
게임 서버 프로그래밍 교과서 5장. 게임 네트워킹 (0) | 2024.04.10 |
게임 서버 프로그래밍 교과서 4장. 게임 서버와 클라이언트 (0) | 2024.04.10 |
게임 서버 프로그래밍 2장. 컴퓨터 네트워크 (0) | 2024.04.06 |
게임 서버 프로그래밍 1장. 멀티스레딩 (1) | 2024.04.06 |