네트워킹 라이브러리를 제작하면서 이상한 오류를 발견했다. 대기 소켓에서 Accept
를 처리하려는데 클라이언트의 주소를 얻어올 수가 없었다. 이를 해결하기 위해 임시로 서버를 만든 후 테스트를 해봤다.
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 1. IOCP 생성
HANDLE iocpHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, NULL);
// 2. 대기 소켓을 만들고 IOCP에 등록
SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
CreateIoCompletionPort((HANDLE)listener, iocpHandle, 0, 0);
// 3. 주소 바인딩 후 연결 대기
sockaddr_in addr{ };
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = AF_INET;
addr.sin_port = htons(7777);
::bind(listener, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
::listen(listener, SOMAXCONN);
// 4. Accept 요청
SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
DWORD bytes;
OVERLAPPED overlapped{ };
char outputBuffer[1024]{ };
int outputBufferLen = 1024;
AcceptEx(listener, client, outputBuffer, 1024, sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, &bytes, &overlapped);
// 5. IOCP로부터 이벤트 올 때까지 대기
LPOVERLAPPED o_p = nullptr;
ULONG_PTR key = 0;
DWORD bytesTransferred = 0;
if (GetQueuedCompletionStatus(iocpHandle, &bytesTransferred, &key, &o_p, INFINITE))
{
// 6. 클라이언트의 주소를 얻어온다.
sockaddr_in clientAddress{ };
int addrLen = sizeof(clientAddress);
if (SOCKET_ERROR == getpeername(client, (sockaddr*)&clientAddress, &addrLen))
{
// ERROR!
}
cout << "Client Connected\n";
}
while (true)
{
// 서버가 종료되지 않게 대기
}
}
문제가 되는 부분은 클라이언트의 주소를 얻는 아래의 코드였다. getpeername()
에서 자꾸 SOCKET_ERROR
를 반환하고 있었다.
// 6. 클라이언트의 주소를 얻어온다.
sockaddr_in clientAddress{ };
int addrLen = sizeof(clientAddress);
if (SOCKET_ERROR == getpeername(client, (sockaddr*)&clientAddress, &addrLen))
{
// ERROR!
}
WSAGetLastError()
로 오류 코드를 얻으니 WSAENOTCONN
코드가 나왔다. 이상했다. 이 코드는 소켓이 연결되지 않을 때 나오는 코드다. 하지만, 내 프로그램에서는 분명 연결이 성립됐고 서로 메시지까지 주고받고 있었다.
해결의 실마리는 AcceptEx()
의 레퍼런스 문서에 있었다.
On Windows XP and later, once the AcceptEx function completes and the SO_UPDATE_ACCEPT_CONTEXT option is set on the accepted socket, the local address associated with the accepted socket can also be retrieved using the getsockname function. Likewise, the remote address associated with the accepted socket can be retrieved using the getpeername function.
Windows XP 이상에서는 AcceptEx 함수가 완료되고 수락된 소켓에 SO_UPDATE_ACCEPT_CONTEXT 옵션이 설정되면, 수락된 소켓과 연결된 로컬 주소도 getsockname 함수를 사용하여 검색할 수 있습니다. 마찬가지로, 수락된 소켓과 연결된 원격 주소는 getpeername 함수를 사용하여 검색할 수 있습니다.
나는 좀 뜨악했다. 왜냐하면 대부분의 소켓 옵션은 지원하지 않는다고 MSDN에 적혀있기 때문이었다. SOL_SOCKET
소켓 옵션 문서의 아래 쪽에는 Windows에서 지원하는 소켓 옵션이 나와있다.
Option | Windows 10 | Windows 7 | Windows Server 2008 | Windows Vista | Windows Server 2003 | Windows XP | Windows 2000 | Windows NT4 | Windows 9x/ME |
---|---|---|---|---|---|---|---|---|---|
PVD_CONFIG | |||||||||
SO_ACCEPTCONN | x | x | x | x | x | x | x | x | x |
SO_BROADCAST | x | x | x | x | x | x | x | x | x |
SO_BSP_STATE | x | x | x | x | |||||
SO_CONDITIONAL_ACCEPT | x | x | x | x | x | x | x | ||
SO_CONNDATA | x | x | x | x | x | x | x | x | |
SO_CONNDATALEN | x | x | x | x | x | x | x | x | |
SO_CONNECT_TIME | x | x | x | x | x | x | x | x | x |
SO_CONNOPT | x | x | x | x | x | x | x | x | |
SO_CONNOPTLEN | x | x | x | x | x | x | x | x | |
SO_DISCDATA | x | x | x | x | x | x | x | x | |
SO_DISCDATALEN | x | x | x | x | x | x | x | x | |
SO_DISCOPT | x | x | x | x | x | x | x | x | |
SO_DISCOPTLEN | x | x | x | x | x | x | x | x | |
SO_DEBUG | x | x | x | x | x | x | x | x | x |
SO_DONTLINGER | x | x | x | x | x | x | x | x | x |
SO_DONTROUTE | x | x | x | x | x | x | x | x | x |
SO_ERROR | x | x | x | x | x | x | x | x | x |
SO_EXCLUSIVEADDRUSE | x | x | x | x | x | x | x | x SP4+ | |
SO_GROUP_ID | x | x | x | x | |||||
SO_GROUP_PRIORITY | x | x | x | x | |||||
SO_KEEPALIVE | x | x | x | x | x | x | x | x | x |
SO_LINGER | x | x | x | x | x | x | x | x | x |
SO_MAX_MSG_SIZE | x | x | x | x | x | x | x | x | x |
SO_MAXDG | x | x | x | x | x | x | x | ||
SO_MAXPATHDG | x | x | x | x | x | x | x | ||
SO_OOBINLINE | x | x | x | x | x | x | x | x | x |
SO_OPENTYPE | x | x | x | x | x | x | x | x | x |
SO_PORT_SCALABILITY | x | x | x | ||||||
SO_PROTECT | x | ||||||||
SO_PROTOCOL_INFO | x | x | x | x | x | x | x | x | x |
SO_PROTOCOL_INFOA | x | x | x | x | x | x | x | x | x |
SO_PROTOCOL_INFOW | x | x | x | x | x | x | x | x | x |
SO_RCVBUF | x | x | x | x | x | x | x | x | x |
SO_RCVLOWAT | |||||||||
SO_RCVTIMEO | x | x | x | x | x | x | x | x | x |
SO_RANDOMIZE_PORT | x | x | x | x | |||||
SO_REUSEADDR | x | x | x | x | x | x | x | x | x |
SO_REUSE_UNICASTPORT | x | ||||||||
SO_REUSE_MULTICASTPORT | x | ||||||||
SO_SNDBUF | x | x | x | x | x | x | x | x | x |
SO_SNDLOWAT | |||||||||
SO_SNDTIMEO | x | x | x | x | x | x | x | x | x |
SO_TYPE | x | x | x | x | x | x | x | x | x |
SO_UPDATE_ACCEPT_CONTEXT | x | x | x | x | x | x | x | x | |
SO_UPDATE_CONNECT_CONTEXT | x | x | x | x | x | x | |||
SO_USELOOPBACK |
필자가 사용하는 운영체제는 Windows 11이니까 설정을 하는 게 의미없다고 생각했는데, 그게 아니었다. 아래와 같이 코드를 수정하면 정상적으로 동작한다.
setsockopt(client, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (const char*)&listener , sizeof(listener));
// 6. 클라이언트의 주소를 얻어온다.
sockaddr_in clientAddress{ };
int addrLen = sizeof(clientAddress);
if (SOCKET_ERROR == getpeername(client, (sockaddr*)&clientAddress, &addrLen))
{
// ERROR!
}
// 이제는 주소를 잘 얻어온다.
게임 서버 제작 시 자주 사용되는 SO_REUSEADDR
, SO_LINGER
, TCP_NODELAY
옵션에 대해서도 확인이 필요할 것 같다. 🥹 내가 표를 잘못 보는 것인지 MSDN 문서가 잘못된 것인지...😮💨
여담으로 아래와 같이 템플릿 함수를 만들어 놓는다면 소켓 옵션을 편리하게 사용할 수 있다.
/// <summary>
/// 소켓 옵션을 설정한다.
/// </summary>
/// <returns>옵션 설정에 성공했다면 true, 아니면 false다.</returns>
template <typename T>
bool SetSockOpt(SOCKET s, int level, int optname, T optval)
{
return SOCKET_ERROR != setsockopt(s, level, optname, reinterpret_cast<const char*>(&optval), sizeof(T));
}
/// <summary>
/// 소켓 옵션을 가져온다.
/// </summary>
/// <returns>옵션을 가져오는 데 성공했다면 true, 아니면 false다.</returns>
template <typename T>
bool GetSockOpt(SOCKET s, int level, int optname, T& optval)
{
return SOCKET_ERROR != getsockopt(s, level, optname, reinterpret_cast<const char*>(&optval), sizeof(T));
}
참고자료
'Study > Game Server' 카테고리의 다른 글
게임 서버 라이브러리 제작하기 - 교착 상태 탐지 모듈 (1) | 2024.09.23 |
---|