<서론>
분산서버를 만드려면 서버와 서버의 연결은 필수다,,
뭐 다른 소켓을 쓴다면 소켓을 기반으로 통신이 되지만 IOCP를 쓴다면 CompletionKey로 구분을 한다.. 그리고 WSAOVERLAPPED 구조체를 이용하여 op를 구분하고 gqcs에서 넘겨받은 wsaover로 이제 할 일을 하는데,,
예전에 했던 프로젝트에서는 Accept를 받고, 서버면 특정 패킷을 한 번 더 보내서 CompletionKey를 따로 구분을 했는데,클라이언트가 이 패킷을 알아서 (해킹으로 무한히 실험해보던가, 아니면 내부 코드를 아는 사람이라던가의 이유로) 시도한다면 이는 반드시 큰 문제이다. 이를 해결하기 위해 생각을 하다가 AcceptEx에서 데이터를 받을 수 있다는 얘기를 듣고, ConnectEx의 존재를 알게되어 이것을 사용해보는것이 목적이다.
헉!!!!!!!!! 방금생각난건데 굳이 connectEx를 안 쓰더라도, 처음에 가동시킬 서버의 갯수를 안다면 굳이 이렇게 할 필요가 없다,,, 나는 뻘짓을 한것인가..? ㅜㅜ
하지만.. 그래도 포스팅한다,,,,
주된 요는 Connect할때 특정 data를 받아서 서버인지 클라인지 구분한다는게 point 이다.
본론
보통 비동기로 Accept를 받을때는 AceeptEx를 사용한다.
https://learn.microsoft.com/ko-kr/windows/win32/api/mswsock/nf-mswsock-acceptex 를 보면
AcceptEx의 인자는
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
);
이다. 보통은 accept할때 데이터 주고받을게 없다면 4번째 인자를 0을 주면된다.
하지만 우리의 목적은 Connect할때 데이터를 줘서 서버/클라 구분을 할거니깐 4번째 인자를 설정하도록 한다.
얼만큼? 설정한 wsabuf 만큼! (더 작게 줘도 됨)
다만!!!!!!!!!!!!!!!!! 주의점이 있음
저 위의 acceptEx의 dwReceiveDataLength(4번째인자) 의 설명을 보면
버퍼의 시작 부분에 있는 실제 수신 데이터에 사용할 lpOutputBuffer 의 바이트 수입니다. 이 크기에는 서버의 로컬 주소 크기나 클라이언트의 원격 주소가 포함되지 않아야 합니다. 출력 버퍼에 추가됩니다. dwReceiveDataLength가 0이면 연결을 수락해도 수신 작업이 발생하지 않습니다. 대신, 데이터를 기다리지 않고 연결이 도착하는 즉시 AcceptEx 가 완료됩니다.
라고 되어있음. 우리는 빨간색을 주목하면 됨. 서버의 로컬주소크기 (IPv4기준 4 127.0.0.1 이건 4바이트. char 4개니까)랑 클라이언트 원격주소 (이것도 4바이트)가 포함이 안되어야함. 버퍼크기가 200이라면? 8을 뺀 192까지만 받을 수 있는거임. 193을 받는다고하면 바로 데이터 못읽고 깨져버림! 궁금하면 해보도록 하자
자 그러면 얼 추 알아냈음.
이제 ConnectEx의 설명을 보러 가자.
LPFN_CONNECTEX LpfnConnectex;
BOOL LpfnConnectex(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen,
[in, optional] PVOID lpSendBuffer,
[in] DWORD dwSendDataLength,
[out] LPDWORD lpdwBytesSent,
[in] LPOVERLAPPED lpOverlapped
)
{...}
https://learn.microsoft.com/ko-kr/windows/win32/api/mswsock/nc-mswsock-lpfn_connectex
LPFN_CONNECTEX(mswsock.h) - Win32 apps
ConnectEx 함수는 지정된 소켓에 대한 연결을 설정하고 연결이 설정되면 필요에 따라 데이터를 보냅니다.
learn.microsoft.com
에 나와있는 ConnEx 설명임.
얘는 좀 특이한게, 함수를 바로 쓸 수가 없고, 함수포인터로 등록을 해서 써야 함;; 그리고 이미 바인딩 된 소켓을 사용해야함!
참고ConnectEx 함수에 대한 함수 포인터는 지정된 SIO_GET_EXTENSION_FUNCTION_POINTERopcode를 사용하여 WSAIoctl 함수를 호출하여 런타임에 가져와야 합니다.WSAIoctl 함수에 전달된 입력 버퍼에는 값이 ConnectEx 확장 함수를 식별하는 GUID(Globally Unique Identifier)인 WSAID_CONNECTEX 포함되어야 합니다. 성공하면 WSAIoctl 함수에서 반환된 출력에 ConnectEx 함수에 대한 포인터가 포함됩니다. WSAID_CONNECTEX GUID는 Mswsock.h 헤더 파일에 정의되어 있습니다.
SOCKADDR_IN server_addr;
ZeroMemory(&server_addr, sizeof(server_addr));
server_addr.sin_family = PF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
mSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (bind(mSocket, reinterpret_cast<sockaddr*>(&server_addr),
sizeof(server_addr)) == SOCKET_ERROR)
{
std::cout << "bind error\n";
return;
}
LPFN_CONNECTEX Conn;
GUID guid = WSAID_CONNECTEX;
DWORD dwbyte{};
WSAIoctl(mSocket, SIO_GET_EXTENSION_FUNCTION_POINTER,
&guid, sizeof(guid),
&Conn, sizeof(Conn),
&dwbyte, NULL, NULL);
등록하는 방법은 이렇게 됨.
주의사항을 보면 conn할때 데이터를 보내는게 되기는 한다만, 데이터 다 보낼때까지 시스템 메모리를 많이 쓰니까 많은 양을 보내지는 말라고 함. 조금만 보내도록 하자.
자 ! 그럼 이제 데이터를 보내볼 차례이다.
보내는 데이터는 WSABUF로 보내므로
WSABUF temp;
unsigned char tbuf[120];
memcpy(tbuf, &데이터쪼가리, sizeof(데이터쪼가리));
temp.buf = (char*)tbuf;
temp.len = sizeof(tbuf);
를 통해 만들어주도록 하자.
int ret = Conn(mSocket, reinterpret_cast<sockaddr*>(&server_addr), sizeof(server_addr),
tbuf, sizeof(tbuf),
NULL, reinterpret_cast<LPOVERLAPPED>(overex));
그리고 보내주면 끝~~~
비동기이기 때문에 그냥 넘어간다.
하지만 궁금할 수 있다. (완료된걸 어떻게 알지 ?)
필자는 IOCP를 사용하고 있다. 그리고 ConnectEx또한 비동기함수 이고 wsaoverlapped를 쓰기 때문에 iocp로 완료처리를 받을 수 있다.
How to?
socket을 iocp에 등록해주고 확장 overlapped 구조체로 op만 설정해주면된다!
CreateIoCompletionPort(reinterpret_cast<HANDLE>(mSocket), gMainServer->GetIOCPHandle(), 0, 0);
WSA_OVER_EX* overex = new WSA_OVER_EX();
overex->SetCmd(eCOMMAND_IOCP::CMD_SERVER_CONN);
wsa_over_ex 구조체는 필자가 만든것이기 때문에 여러분이 직접 만드시길 바란다. 이 글을 본다면 이정도는 할 줄 알거라 생각한다
참고로 overex를 new 할당을 안받고 만들어도 되긴 한다만,, 스택에 만든 지역변순데 gqcs에서 온전히 가져온다!. 이건 꽤 찜찜한 이야기다. new로 받고 gqcs에서 delete로 해제하도록 하자 ㅡㅡ;;
그래서 연결이 성공하면 gqcs에서 반환을 할거고 여기서 이제 우리는 서버를 등록하고 wsarecv를 걸어두면 된다.
BOOL ret = GetQueuedCompletionStatus(mMainServer->GetIOCPHandle(), &bytes, (PULONG_PTR)&iocp_key, &overlapped, INFINITE);
WSA_OVER_EX* wsa_ex = reinterpret_cast<WSA_OVER_EX*>(overlapped);
...
case eCOMMAND_IOCP::CMD_SERVER_CONN:
{
auto lobbyserver = mMainServer->GetLobbyServer();
lobbyserver->AcceptSetting(eSocketState::ST_ACCEPT, eCOMMAND_IOCP::CMD_SERVER_RECV, lobbyserver->GetSocket());
lobbyserver->PreRecvPacket(NULL, 0);
lobbyserver->RecvPacket();
delete wsa_ex;
std::cout << "Lobby Connect Complete\n";
break;
}
코드 전문
SOCKADDR_IN server_addr;
ZeroMemory(&server_addr, sizeof(server_addr));
server_addr.sin_family = PF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
mSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (bind(mSocket, reinterpret_cast<sockaddr*>(&server_addr),
sizeof(server_addr)) == SOCKET_ERROR)
{
std::cout << "bind error\n";
return;
}
CreateIoCompletionPort(reinterpret_cast<HANDLE>(mSocket), gMainServer->GetIOCPHandle(), 0, 0);
WSA_OVER_EX* overex = new WSA_OVER_EX();
overex->SetCmd(eCOMMAND_IOCP::CMD_SERVER_CONN);
server_addr.sin_port = htons(LOBBYSERVERPORT);
inet_pton(AF_INET, LOOBYSERVER_ADDR, &server_addr.sin_addr);
LPFN_CONNECTEX Conn;
GUID guid = WSAID_CONNECTEX;
DWORD dwbyte{};
WSAIoctl(mSocket, SIO_GET_EXTENSION_FUNCTION_POINTER,
&guid, sizeof(guid),
&Conn, sizeof(Conn),
&dwbyte, NULL, NULL);
WSABUF temp;
unsigned char tbuf[120];
memcpy(tbuf, &INCODE_SERVER_PACKET, sizeof(unsigned long long));
temp.buf = (char*)tbuf;
temp.len = sizeof(tbuf);
int ret = Conn(mSocket, reinterpret_cast<sockaddr*>(&server_addr), sizeof(server_addr),
tbuf, sizeof(tbuf),
NULL, reinterpret_cast<LPOVERLAPPED>(overex));
if(ret == SOCKET_ERROR)
{
int err = WSAGetLastError();
if (err != WSA_IO_PENDING)
{
std::cout << "error\n";
return;
//error !
}
}
자 이렇게 하여 성공적으로 connect시 데이터를 보냈다.
놀랍게도 아직 끝이 아니다
받는쪽은 어떻게 받냐? 도 중요한 부분이다.
AcceptEx(mListenSocket, c_socket, a_over.mMessageBuf,
BUF_SIZE - 8,
addr_size + 16,
addr_size + 16, 0, &a_over.mOver);
맨 위쪽에서 설명한걸 코드로 보여주면 대충 이렇다.
받는쪽에서는 그대~로 받아주면 된다. AcceptEx에 등록했던 저 버퍼를 GQCS에서 그대로 꺼내주기만 하면 끝~
unsigned long long k{};
memcpy(&k, exOver->mMessageBuf, sizeof(unsigned long long));
필자는 long long 값 하나를 보냈으므로 받을때도 똑같이 받는다.
다만 문제가 있다. acceptEX에서 100바이트를 받을게요~ 라고 설정했을때,
Connect해주는애가 아무것도 안보내면 무언가 데이터를 받을 때까지 gqcs가 반환을 안한다;;
이렇게 된다면,, 클라이언트에서도 ConnectEx로 데이터를 보내는게 아니라면, 어차피 이 gqcs반환을 위해서 특정 번째까지 또 처리를 해줘야 한다는 불편한 점이다..
이럴거면 그냥 connectEX 안쓰고 connect로 하고 특정 번째까지만 서버라고 판정하면 되는거 아님?? 이라고 할 수 있다.
맞는말이다. 근데 해보기 전까지 몰랐잖아!!!!!!!!!!!!!
난 해봤으니까 아는거다.. 이런 삽질을 안해도 된다는 것을
근데 의미가 있을 수 있다. 클라이언트에서도 ConnectEx로 연결을 한다면, 이는 정말 유의미한 방법이다.
클라에서는 1byte만 connectEx에 담아 보내고, 서버는 ConnectEx에 10바이트를 담아 보내면 그냥 바로 구분할 수 있다.
1. 서버 런타임 도중에 서버및 클라 연결을 시도해야하고, 이때 구분자가 필요하다면 이 방법이 유효할 수 있다. (서버 재가동시 구분자용)
2. 하지만 우리 서버는 클라접속 전에 이미 서버세팅을 다 끝내놓기 때문에 딱히? 의미가 없을 수 있다.
나중에 1번방법이 필요하다면 이 방법을 사용해서 연결하도록 하자.
'개발 > C++' 카테고리의 다른 글
lua coroutine을 이용한 퀘스트 스크립트 (1) | 2023.07.12 |
---|---|
첫 글 (0) | 2022.08.12 |