아모레님,
알려주신 https://devblogs.microsoft.com/oldnewthing/20221102-00/?p=107343 링크는 fd_set, FD_SETSIZE와 이것이 WinSock과 어떻게 관련되어 있는지에 대한 역사를 설명하고 있었습니다. 정의된 문제를 해결하기 위한 샘플 코드를 남겨보았습니다.
* Unix와 WinSock의 차이
Unix에서는 파일 디스크립터의 수가 증가함에 따라 fd_set의 비트 배열 크기를 늘리는 대신, poll이라는 새로운 방법을 도입했습니다. poll은 파일 디스크립터 세트를 비트 배열이 아닌 pollfd 구조체의 배열로 표현합니다.
WinSock은 기존의 Unix 기반 네트워킹 코드와 소스 호환성을 유지하면서도 Windows의 핸들 시스템에 맞게 fd_set을 재정의했습니다. WinSock의 fd_set은 소켓 핸들의 배열로 구성되어 있습니다.
* Windows에서의 fd_set
Windows에서는 fd_set의 크기를 FD_SETSIZE로 정의할 수 있으며, 이는 fd_set을 조작하는 매크로의 동작에 영향을 줍니다.
fd_set의 크기가 다르게 정의된 경우, fd_set 객체를 주소 또는 참조로만 전달해야 합니다. 그렇지 않으면 One Definition Rule을 위반하게 됩니다.
* 해결 방법
1. Windows에서는 WSASelectEvent를 사용하여 각 소켓에 이벤트를 연결하거나 I/O 완료 포트를 사용하여 각 소켓의 준비 상태를 처리하는 것이 좋습니다.
2. FD_ISSET가 Windows에서 신뢰할 수 없다고 보고된 문제는, Windows 소켓의 숫자 값이 FD_SETSIZE보다 크기 때문에 발생합니다. 이 문제를 해결하려면 fd_set에 넣은 핸들을 추적하고 FD_ISSET을 호출할 때 해당 핸들만 반복해야 합니다.
#include <iostream> #include <vector> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib")
int main() { WSADATA wsaData; SOCKET clientSocket; sockaddr_in serverAddr; char buffer[1024]; WSAEVENT hEvent; WSANETWORKEVENTS networkEvents;
// WSAStartup 함수를 호출하여 Winsock 라이브러리를 초기화합니다. if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { std::cerr << "WSAStartup failed!" << std::endl; return -1; }
// 소켓을 생성합니다. clientSocket = socket(AF_INET, SOCK_STREAM, 0); if (clientSocket == INVALID_SOCKET) { std::cerr << "Socket creation failed!" << std::endl; WSACleanup(); return -1; }
// 서버의 주소 정보를 설정합니다. serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8080); // 서버의 포트 번호 serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 서버의 IP 주소
// 서버에 연결합니다. if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { std::cerr << "Connection to server failed!" << std::endl; closesocket(clientSocket); WSACleanup(); return -1; }
// 소켓 핸들을 추적하는 컨테이너를 생성합니다. std::vector<SOCKET> socketHandles; socketHandles.push_back(clientSocket);
// WSAEventSelect 함수를 사용하여 소켓에 이벤트를 연결합니다. hEvent = WSACreateEvent(); WSAEventSelect(clientSocket, hEvent, FD_READ | FD_CLOSE);
// 이벤트를 기다립니다. while (true) { if (WSAWaitForMultipleEvents(1, &hEvent, FALSE, WSA_INFINITE, FALSE) == WSA_WAIT_EVENT_0) { WSAEnumNetworkEvents(clientSocket, hEvent, &networkEvents);
for (SOCKET s : socketHandles) { if (networkEvents.lNetworkEvents & FD_READ && FD_ISSET(s, &networkEvents.lNetworkEvents)) { // 데이터를 읽습니다. int bytesReceived = recv(s, buffer, sizeof(buffer), 0); if (bytesReceived > 0) { buffer[bytesReceived] = '\0'; std::cout << "Received: " << buffer << std::endl; } }
if (networkEvents.lNetworkEvents & FD_CLOSE && FD_ISSET(s, &networkEvents.lNetworkEvents)) { std::cout << "Connection closed by server." << std::endl; goto cleanup; } } } }
cleanup: // 리소스를 정리합니다. WSACloseEvent(hEvent); closesocket(clientSocket); WSACleanup();
return 0; } |
이 코드에서는 std::vector<SOCKET> socketHandles;를 사용하여 fd_set에 추가된 소켓 핸들을 추적합니다. 그런 다음 WSAEnumNetworkEvents 함수를 사용하여 네트워크 이벤트를 검색하고, FD_ISSET를 사용하여 각 소켓 핸들에 대해 이벤트를 확인합니다. 이 방법으로, FD_ISSET가 신뢰할 수 없는 문제를 해결할 수 있습니다.
첫댓글 안녕하세요. 예제설명해 주셔서 감사합니다. 근데 실제 게임 소스를 보면 FD_ISSET과 상관없이
for(i=0; i<Tablesize; i++) 이런식으로 처리되는 부분이 아주 많이 있거든요. Tablesize는 최대 동시접속자수라고 생각하시면 돼요. 256명이 기본 설정값이고요.
리눅스에서는 소켓이 0번부터 시작해서 하나씩 올라가기 때문에 그냥 저렇게 for문을 써서 돌려도 상관없는 것 같고요.
윈도에서는 소켓이 임의의 숫자부터 시작하기 때문에 그냥 0부터 for문을 돌리면 안되고 소켓 핸들을 따로 관리해주어야 할 필요가 있는 것으로 이해하고 있습니다.
단순히 한군데만 수정해서 해결가능하면 좋겠지만 저런식으로 사용되는 곳이 게임 소스에 아주 많이 있어요.
while문을 사용하는 곳도 있고 아예 player[i] 이런식으로 사용자 구조체에 아예 소켓 번호를 직접 넣어서 사용하는 부분도 아주 많습니다.
윈도 7에서는 대부분의 경우 맨처음 접속자의 소켓 핸들이 0이 아니라 120부터 시작하더라고요. 여전히 256보다는 작기 때문에 여태까지는 for문을 돌릴때 문제가 없었습니다. 물론136명넘게 접속하면 문제가 될수도 있었겠지만 여태까지 윈도에서는 혼자 즐기는 경우만 있었기 때문에 발견 못했던 거고요.
윈도 10에서는 보통 256보다 좀 높은 (512보다는 작은) 값에서 시작합니다. 가끔 256보다 낮은 값에서 시작하면 접속가능하고요.
아무튼 소스를 전체적으로 다 살펴보고 일일이 수정해야 하는데 시간도 많이 걸리는 일이고, 혹시 빼먹는 부분이 있다해도 컴파일에는 문제가 없기 때문에 나중에 다시 찾아내기도 쉽지 않습니다.
임시 방편으로 Tablesize를 512정도로 높이는 방법이 있을수 있는데, 실제로 해보니 잘 안되더라고요. 어디서 문제가 발생하는지는 소스를 좀 더 자세히 분석해 봐야할것 같고요.
다른 방법으로는 서버를 시작할때 첫번째 소켓 핸들값이 256보다 작게 나올때까지 무한루프 돌리는 것도 가능합니다. 서버가 실행되면 접속도 잘되고 게임도 잘 됩니다. 그런데 가끔가다 저 조건을 만족하지 못해서 서버 시작하는데 시간이 오래걸리는 경우가 있어요.