
본격적인 IOCP 구현을 하기 전에 window에서 소켓과 관련된 여러 라이브러리가 존재한다. 이러한 헤더파일의 타입 객체가 무엇을 뜻하고 어떻게 활용되는지 알아야 수준 높은 서버를 구축할 수 있다.
Windows.h 라이브러리는 윈도우의 많은 기능을 수행할 수 있다. 파일을 만들고 추가하는 작업등이 대표적인데 이러한 기능 말고 우리는 Windows의 HANDLE에 대해서 알아야 한다.
핸들은 커널 오브젝트를 사용하기 위해 필요한 장치라고 생각하면 된다. 우리가 자동차 핸들을 돌리면 실제 바퀴를 우리가 돌리는 것이 아닌 것 처럼. 우리가 커널 공간의 메모리를 직접 참조하지 않고 핸들을 이용해 접근해 처리하는 개념이다.
HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
참고로 HANDLE을 의미하는 자료형은 모두 앞에 H를 붙여서 표현하게 된다.
HCURSOR h_my_cursor = LoadCursor(NULL, IDC_ARROW);
HICON h_my_icon = LoadIcon(NULL, IDI_APPLICATION);
이 함수는 새 IOCP 객체를 생성하거나 기존 IOCP에 핸들을 연결하는 함수다.
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle, // [in] 1번: 파일/소켓
_In_opt_ HANDLE ExistingCompletionPort, // [in] 2번: 기존 IOCP
_In_ ULONG_PTR CompletionKey, // [in] 3번: 완료키 (사용자 데이터)
_In_ DWORD NumberOfConcurrentThreads // [in] 4번: 동시 스레드 수
);
INVALID_HANDLE_VALUE를 받게 되면 내부에서 이걸 확인한 다음 새로운 IOCP 커널 오브젝트를 생성하게 된다.(HANDLE)socket이런 형태가 될 것이다.ULONG_PTR은 운영체제의 따라 크기가 가변적으로 바뀌는 포인터이다. 클래스, 구조체, 등등을 다 가르킬 수 있다.DWORD는 32비트로 고정된 Unsigned int이다. IOCP 큐에서 완료된 IO작업을 꺼내오는 함수다.
BOOL GetQueuedCompletionStatus(
[in] HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
[out] PULONG_PTR lpCompletionKey,
[out] LPOVERLAPPED *lpOverlapped,
[in] DWORD dwMilliseconds
);
CompletionPort: CreateioCompletionPort로 만들어진 핸들을 연결한다.lpNumberOfBytesTransferrd: 는 이번 I/O 작업에서 실제 전송된 바이트 수를 저장하는 출력 매개변수이다. ipCompletionKey: CreateioCompetionPort에서 등록한 키를 연결한다.lpOverlapped: Overlapped작업이 시작될때 지정한 구조체의 주소의 포인터다.dwMilliseconds: 대기시간. 이 대기시간동안 완료 패킷이 완료 포트에 나타나지 않으면 시간초과로 FALSE처리한다. 보통 INFINITY를 넣는다.기존의 함수에서 확장이 된 Ex버전의 함수도 존재한다. 주로 고성능 서버에서 사용된다.
BOOL WINAPI GetQueuedCompletionStatusEx(
_In_ HANDLE CompletionPort,
_Out_ LPOVERLAPPED_ENTRY lpCompletionPortEntries,
_In_ ULONG ulCount,
_Out_ PULONG ulNumEntriesRemoved,
_In_ DWORD dwMilliseconds,
_In_ BOOL fAlertable
);
winsock2.h는 window에서 제공하는 소켓 관련 라이브러리 인데 사실상 IOCP의 핵심이라고 해도 과언이 아니다. 이 라이브러리를 좀 알고있어야 게임서버를 구현할 수 있다.
typedef UINT_PTR SOCKET;
SOCKET WSAAPI socket(
[in] int af,
[in] int type,
[in] int protocol
);
#pragma comment(lib, "ws2_32.lib")로 자동 해결할 수 있다.[in] af같은 경우는 주소 패밀리의 사양이다. 이는 어떤 주소 체계를 사용할건지 명시하는 것이다. (ex ipv4, ipv6)| AF | 의미 |
|---|---|
| AF_INET (2) | IPv4 주소 패밀리. |
| AF_INET6 (23) | IPv6 주소 패밀리. |
| AF_BTH (32) | 블루투스 주소 패밀리 |
[in] type같은 경우는 소켓의 형식 타입을 의미한다. | type | 의미 |
|---|---|
| SOCK_STREAM (1) | 양방향 연결 기반 바이트 스트림, IPv4혹은 IPv6에서 TCP를 사용한다. |
| SOCK_DGRAM (2) | 고정된 길이, 데이터그램 지원, IPv4혹은 IPv6에서 UDP를 사용한다. |
[in] protocol은 사용할 프로토콜을 정의한다. | protocol | 의미 |
|---|---|
| BTHPROTO_RFCOMM (3) | 블루투스 프로토콜 |
| IPPROTO_TCP (6) | TCP |
| IPPROTO_UDP (17) | UDP |
이렇게 socket함수를 이용해 원하는 통신 방법에 대해 소켓을 만들면 된다.
앞으로 WSA라는 이름이 붙어있는 함수나 변수를 많이 보게 될텐데 여기서 WSA는 Window Socket Api라는 뜻이다.
앞으로 LP라고 붙은 타입들도 보게 될것이다. LP는 Long Pointer의 약자로 이게 붙어있으면 주소를 가르키는 타입이라고 생각하면 된다.
ex) LPWSADATA: 라고 되어있으면 WSA의 정보를 가지고 있는 상자의 주소를 가르키는 구나 라고 생각할 수 있다.
typedef struct _WSAOVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
WSAOVERLAPPED는 비동기 IO작업의 식별자로, WSARecv 호출 시 완료 알림을 받기 위한 구조체이다.int WSAAPI WSAStartup(
[in] WORD wVersionRequested,
[out] LPWSADATA lpWSAData
);
typedef unsigned short WORD;를 뜻한다.LPWSADATA는 다음을 의미한다.typedef struct _WSADATA {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR* lpVendorInfo;
} WSADATA, *LPWSADATA; // LPWSADATA = WSADATA*
mVersionRequested는 호출자가 사용할 수 있는 가장 높은 버전의 Windows 소켓 사양이다. 현재는 2.2버전이 가장 최신이기 때문에 맞춰서 WSADATA wsaData; // 빈 데이터 상자
WORD ver = MAKEWORD(2,2);
int ref = WSAStartup(ver, &wsaData);
lpWSAData는 소켓구현의 세부 정보를 받는 포인터이다.WSAStartup()함수는 성공하면 0을 반환하고 아니면 여러 오류중 하나를 반환한다. (공식문서 확인)int WSAAPI WSACleanup();
WSASocket은 socket()함수의 업그레이드 버전으로 IOCP나 고성능 서버를 이용할때 사용한다.
- WSASocket은 매크로로 WSASocketA/ WSASocketW의 별칭이다. 이렇게 2개로 나눠져있는 이유는 문자 인코딩 방식에있는데 그냥 WSASocket으로 사용하면 될 듯 하다.
SOCKET WSAAPI WSASocket(
[in] int af,
[in] int type,
[in] int protocol,
[in] LPWSAPROTOCOL_INFOA lpProtocolInfo,
[in] GROUP g,
[in] DWORD dwFlags
);
af는 앞서 설명한거처럼 주소 패밀리 사양이다.type도 앞서 설명한 소켓의 형식 타입이다.protocol도 앞서 socket에서 설명한것과 같다.[in] lpProtocolInfo: 이거는 그냥 Null로 하면 된다. 찾아보니까 null로 설정하면 앞의 3개 인자로 자동 결정이 된다. 대부분의 상황에서는 Null이니까 그냥 넘어가도록 하자.Group g: 이는 같은 그룹의 소켓들끼리 "동일한 설정을 강제"하는 것인데 0으로 하면 개별 소켓으로 처리하는 것이다. 그리고 이것도 대부분의 상황에서는 0이니까 그냥 0이라고 생각하고 넘어가자.dwFlags: 추가 소켓 특성을 지정하는데 사용되는 플레그이다. 이것도 여러가지가 있는데 WSA_FLAG_OVERLAPPED를 사용하면 IOCP를 구현할 수 있고 이것이 IOCP의 핵심이라고 볼 수 있다.int WSAAPI bind(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen
);
sockaddr로 캐스팅, 그리고 그 주소의 길이를 인자로 넣으면 된다.int WSAAPI listen(
[in] SOCKET s,
[in] int backlog
);
s는 bind할때 사용한 서버의 소켓을 넣는다.backlog는 보류중인 연결 큐의 최대 길이를 의미한다. SOMAXCONN으로 하면 백로그를 적절한 최대값으로 설정한다.SOCKET_ERROR를 반환한다. SOCKET WSAAPI accept(
[in] SOCKET s,
[out] sockaddr *addr,
[in, out] int *addrlen
);
SOCKET을 반환하게 된다.int WSAAPI recv(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags
);
s: 연결된 소켓을 식별한다. -> 여기서는 클라이언트 소켓을 사용한다.buf: 들어오는 데이터를 수신할 버퍼에 대한 포인터이다. 즉 어디에 받을지.len: 그 버퍼의 크기flags: 함수의 동작에 영향을 준다. 보통 0으로 한다.| flags 값 | 의미 |
|---|---|
| MSG_PEEK | 버퍼에 데이터를 복사하지만 큐에서 제거하지는 않는다.(패킷 검사에 사용) |
| MSG_OOB | Out Of Band의 뜻으로 긴급 메시지를 뜻함. recv큐에 안쌓이고 바로 처리 |
| MSG_WAITALL | recv가 요청한 전체 바이트 수 만큼 데이터를 받을 때 까지 블로킹(UDP에서는 무시됨.) |
int WSAAPI recvfrom(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags,
[out] sockaddr *from,
[in, out, optional] int *fromlen
);
from: 반환 시 원본 소켓 주소를 의미함. (누구한테 왔는지를 반환)fromlen: from의 크기.int WSAAPI WSARecv(
[in] SOCKET s,
[in, out] LPWSABUF lpBuffers,
[in] DWORD dwBufferCount,
[out] LPDWORD lpNumberOfBytesRecvd,
[in, out] LPDWORD lpFlags,
[in] LPWSAOVERLAPPED lpOverlapped,
[in] LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
s: 마찬가지로 연결된 소켓 (클라이언트)typedef struct _WSABUF {
ULONG len;
CHAR *buf;
} WSABUF, *LPWSABUF;
LPWSABUF타입은 _WSABUF구조체이다.[in, out] lpBuffers: 여러 버퍼들을 받는다. + 버퍼의 길이까지. 참고로 이건 배열이고 주소를 가지고 있어서 0번째 인덱스를 가르키고 있는 것임.dwBufferCount: lpBuffers 배열에 들어있는 WSABUF 구조체의 개수.lpNumberOfButesRecvd: 이번 WSARecv호출로 실제 받은 전체 바이트 수를 반환한다. (총 수신량)lpFlags: recv와 마찬가지 플레그들이 있다. 보통 0으로 설정lpOverlapped: WSAOVERLAPPED 구조체에 대한 포인터이다.lpCompletionRpoutine: 수신작업이 완료되면 호출되는 완료 루틴에 대한 포인터. 중요한거는 IOCP와 같이 사용 불가능하고 Event 모델에서만 사용한다.int WSAAPI WSARecvFrom(
[in] SOCKET s,
[in, out] LPWSABUF lpBuffers,
[in] DWORD dwBufferCount,
[out] LPDWORD lpNumberOfBytesRecvd,
[in, out] LPDWORD lpFlags,
[out] sockaddr *lpFrom,
[in, out] LPINT lpFromlen,
[in] LPWSAOVERLAPPED lpOverlapped,
[in] LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
lpFrom: 발신자 주소lpFromlen: 그 주소 구조체의 크기int WSAAPI send(
[in] SOCKET s,
[in] const char *buf,
[in] int len,
[in] int flags
);
s: 연결된 소켓을 식별한다. 즉, 서버는 accept()로 얻은 클라이언트 소켓을 의미하고, 클라이언트에서는 connect()로 서버에 연결된 자신의 소켓을 말함.buf: 전송할 데이터를 포함한 버퍼에 대한 포인터len: 그 버퍼의 사이즈.flags: 보통 0으로 플레그 안씀. (귀찮아서 정리 안하는거 아님.)int WSAAPI sendto(
[in] SOCKET s,
[in] const char *buf,
[in] int len,
[in] int flags,
[in] const sockaddr *to,
[in] int tolen
);
to: 대상 소켓의 주소를 포함하는 sockaddr에 대한 포인터다. (대상 주소)tolen: 그거의 길이.int WSAAPI WSASend(
[in] SOCKET s,
[in] LPWSABUF lpBuffers,
[in] DWORD dwBufferCount,
[out] LPDWORD lpNumberOfBytesSent,
[in] DWORD dwFlags,
[in] LPWSAOVERLAPPED lpOverlapped,
[in] LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
lpOverlapped: 이게 추가가 되어서 IOCP 큐에 들어갈 수 있음. (백그라운드 비동기 처리를 함.)lpCompletionRoutine: 콜백 함수 포인터로 작업 완료시 자동으로 호출되는 완료 루틴이다. IOCP와는 같이 사용할 수 없어서 Null로 둔다.int WSAAPI WSASendTo(
[in] SOCKET s,
[in] LPWSABUF lpBuffers,
[in] DWORD dwBufferCount,
[out] LPDWORD lpNumberOfBytesSent,
[in] DWORD dwFlags,
[in] const sockaddr *lpTo,
[in] int iTolen,
[in] LPWSAOVERLAPPED lpOverlapped,
[in] LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
lpTo: SOCKADDR 구조체의 대상 소켓 주소에 대한 선택적 포인터이다. (누구한테 보낼지)iTolen: 그 포인터 의 크기int WSAAPI closesocket(
[in] SOCKET s
);
SOCKET_ERROR를 반환한다.추가로 이런 라이브러리도 있다. 주로 Winsock2.h의 확장된 기능들을 사용하고 있다. 그래서 IOCP를 실질적으로 구현한다고 하면 winsock 으로만 하는게 아니라 Mswsock를 같이 사용해 확장된 기능을 이용한다.
기존의 accept는 블로킹을 한다. 그래서 연결 수락을 대기하게 되는데 AcceptEx같은 경우는 비동기로 처리가 된다. 그래서 미리 accept소켓을 준비해두고, overlapped IO로 호출을 한다. IOCP에서 완료 호출을 받으면 그 때 나머지 작업을 처리하면 되는 것이다.
BOOL AcceptEx(
[in] SOCKET sListenSocket,
[in] SOCKET sAcceptSocket,
[in] PVOID lpOutputBuffer,
[in] DWORD dwReceiveDataLength,
[in] DWORD dwLocalAddressLength,
[in] DWORD dwRemoteAddressLength,
[out] LPDWORD lpdwBytesReceived,
[in] LPOVERLAPPED lpOverlapped
);
sListenSocket: 여기서는 리슨 소켓을 넣는다. 즉 서버의 소켓을 하면 된다.sAcceptSocket: 미리 만들어진 소켓으로 클라이언트의 소켓과 연결될 소켓을 의미한다. lpOutputBuffer: 기존 accept같은 경우는 SOCKADDR_IN 구조체를 만들어서 이거의 포인터를 통해 대입을 했지만 AcceptEx에서는 char[buffer_size]형태로 대입한다.dwReceivedataLength: 이거는 이제 입력을 받을 데이터의 사이즈를 구하는 건데. 보통 [로컬 주소][원격 주소][데이터] 이런 형태로 만들어지게 된다. 그래서 데이터 부분의 길이는 전체에서 - 로컬 주소 - 원격 주소 가 되는 것이 일반적이다.sizeof(recvBuffer) - (2 * (sizeof(SOCKADDR_IN) + 16))이렇게 계산된 값이 실제 데이터의 영역이라 이걸 넣는다.dwLocalAddressLength, dwRemoteAddressLength: 이거는 위에서 했던 sizeof(SOCKADDR_IN) + 16을 넣으면 된다.lpdwBytesReceived: 이거는 초기값을 말하는데 그냥 0으로 하면 됨.lpOverlapped: 요청을 처리하는 Overlapped 구조체를 의미한다. 필수로 지정해줘야 한다.(null안됨)이렇게 하면 이제 C++ 로 IOCP 서버를 만들 준비가 끝났다. IOCP의 기본 개념도 알았고 C++의 windows.h와 winsock2.h의 주요 함수에 대해서도 알아봤다. 다음시간에는 유니티와 연동하는 서버를 실제로 만들어 보도록 하겠다.
window 공식문서 - winsock2.h
window 공식문서 - windows.h
Handle에 대하여 - 김성엽