저번에는 SendPacket 함수까지 알아보았다
이제는 받기 함수를 소개할 차례인데
참 어려운 고민이 많았다
일단 서버를 생각한다면 IOCP , select, epoll , kqueue 등등 다양한 소켓이 존재했고
그리고 소켓을 캡슐화한 다른 형태의 라이브러리까지 생각한다면 ,
그런것을 고려해 코드를 생성하기는 매우 어려운 일이었다
게다가 Send쪽은 SendPacket은 void* 와 size 를 인자로 하나의 함수만 어플리케이션쪽에서
정의해 주면 끝났지만
받는쪽은 반대로 void* 와 size 를 받아
패킷을 분석해 PT_INTERFACE로 정의되어 있는 구조체를 인자에 맞게 함수를 호출해줘야 했기때문에
더더욱 쉽게 자동화 하긴 어려웠다
그래서 가장 덜 손이 갈수 있겠다 싶은 방법을 생각해 보았다
먼저 해야할것이
소켓에서 데이타를 받은 데이터를 stream에 입력하고 enumID에 맞게 PT_INTERFACE에 정의한
구조체로 변환 하는 작업이다
예를 들면 이러한 함수 일것이다
이제는 받기 함수를 소개할 차례인데
참 어려운 고민이 많았다
일단 서버를 생각한다면 IOCP , select, epoll , kqueue 등등 다양한 소켓이 존재했고
그리고 소켓을 캡슐화한 다른 형태의 라이브러리까지 생각한다면 ,
그런것을 고려해 코드를 생성하기는 매우 어려운 일이었다
게다가 Send쪽은 SendPacket은 void* 와 size 를 인자로 하나의 함수만 어플리케이션쪽에서
정의해 주면 끝났지만
받는쪽은 반대로 void* 와 size 를 받아
패킷을 분석해 PT_INTERFACE로 정의되어 있는 구조체를 인자에 맞게 함수를 호출해줘야 했기때문에
더더욱 쉽게 자동화 하긴 어려웠다
그래서 가장 덜 손이 갈수 있겠다 싶은 방법을 생각해 보았다
먼저 해야할것이
소켓에서 데이타를 받은 데이터를 stream에 입력하고 enumID에 맞게 PT_INTERFACE에 정의한
구조체로 변환 하는 작업이다
예를 들면 이러한 함수 일것이다
// 이함수는 AC_LogIn 이 데이터가 어떻게 쓰일지 모르기에 , 선언만 되어 있음
void ProcessPacket(SOCKET_ID socketID, const AC_LogIn& ptData);
void RecvPacketAC_LogIn(SOCKET_ID socketID,int8* pBuffer,uint32 bufferSize)
{
AC_LogIn ptData;
StreamReaderSelfBuffer stream(pBuffer,bufferSize);
stream & ptData;
ProcessPacket(socketID,ptData);
}
여기서 보면 내부에서 ProcessPacket 함수가 다시 호출되는데 이것은 스트림에서 구조체로
변환한다음 이 구조체를 사용하는 함수를 호출해주는 역활이다
이 구조체로 무엇을 할지 모르기때문에 이 함수의 구현은 어플리케이션 단에서 해줘야한다
그러므로 패킷 생성기에서 생성된 코드를 보면 선언만 되어 있고 구현은 없다
마치 SendPacket를 어플리케이션 단에서 다시 정의해줬던것과 같은것이다
따라서 저러한 함수를 어플리케이션 단에서 구현해줘야 한다
자 그럼... 어떻게 저러한 함수가 호출하도록 만들것인가를 살펴보자
일단 저함수를 호출할수 있도록 Map에 담아 가져오기로 한다
enumID 를 키로하고 void (SOCKET_ID ,int8* ,uint32 ) 의 함수포인터를 가지는 Map 가져오기로 하는것이다
void GetFuncMap_gene_recv_PT_AC_Data(RecvPacketMap& ptRecvMap);
그럼 이제 이 Map 담긴 함수를 호출해야 하는데 아주 간략하게 코드로 보자면 이렇게 될것이다
여기서 보면 내부에서 ProcessPacket 함수가 다시 호출되는데 이것은 스트림에서 구조체로
변환한다음 이 구조체를 사용하는 함수를 호출해주는 역활이다
이 구조체로 무엇을 할지 모르기때문에 이 함수의 구현은 어플리케이션 단에서 해줘야한다
그러므로 패킷 생성기에서 생성된 코드를 보면 선언만 되어 있고 구현은 없다
마치 SendPacket를 어플리케이션 단에서 다시 정의해줬던것과 같은것이다
따라서 저러한 함수를 어플리케이션 단에서 구현해줘야 한다
자 그럼... 어떻게 저러한 함수가 호출하도록 만들것인가를 살펴보자
일단 저함수를 호출할수 있도록 Map에 담아 가져오기로 한다
enumID 를 키로하고 void (SOCKET_ID ,int8* ,uint32 ) 의 함수포인터를 가지는 Map 가져오기로 하는것이다
void GetFuncMap_gene_recv_PT_AC_Data(RecvPacketMap& ptRecvMap);
그럼 이제 이 Map 담긴 함수를 호출해야 하는데 아주 간략하게 코드로 보자면 이렇게 될것이다
// 스트림을 구조체로 바꿔 준후 구조체를 사용하는 함수를 호출해주는 Map얻어오기
// 자동 생성된 코드에서 이 함수가 선언되어 있다
GetFuncMap_gene_recv_PT_AC_Data(g_funcRecvMap);
//------------------------------------------------------------------------------
// 소켓으로부터 버퍼 얻어오기 ( 간단히 만든거므로 딴지 금지 )
// 전체 크기 얻어 오기
uint32 totalSize;
ptEnumType enumID;
int recvSize = recv(g_sockfd,(char*)&totalSize,HEADER_SIZE,0);
if(recvSize <= 0)
break;
ptEnumType enumID;
int recvSize = recv(g_sockfd,(char*)&totalSize,HEADER_SIZE,0);
if(recvSize <= 0)
break;
// enum 얻어오기
PG_ASSERT(recvSize == HEADER_SIZE);
recvSize = recv(g_sockfd,(char*)&enumID,PT_ENUM_TYPE_SIZE,0);
if(recvSize <= 0)
break;
PG_ASSERT(recvSize == PT_ENUM_TYPE_SIZE);
recvSize = recv(g_sockfd,(char*)&enumID,PT_ENUM_TYPE_SIZE,0);
if(recvSize <= 0)
break;
PG_ASSERT(recvSize == PT_ENUM_TYPE_SIZE);
// 데이타 버퍼 얻어오기
char recvBuf[MAX_PACKETLEN];
uint32 ptSize = totalSize - PT_ENUM_TYPE_SIZE;
recvSize = recv(g_sockfd,recvBuf,ptSize,0);
if(recvSize <= 0)
break;
uint32 ptSize = totalSize - PT_ENUM_TYPE_SIZE;
recvSize = recv(g_sockfd,recvBuf,ptSize,0);
if(recvSize <= 0)
break;
PG_ASSERT(recvSize == ptSize);
//------------------------------------------------------------------------------
// 위 루틴으로부터 enumID 와 버퍼 버퍼크기를 얻어올수 있었다
try
{
// check enumID valid
RecvPacketMap::iterator it = g_funcRecvMap.find(enumID);
if( it != g_funcRecvMap.end())
g_funcRecvMap[enumID](g_sockfd,recvBuf,ptSize);
}
catch(StreamExceptionUnderflow* )
{
// 패킷 Stream Underflow
}
g_funcRecvMap 함수포인터 맵에 enumID를 넘겨주면
위에서 스트림을 구조체로 변환해주는 함수포인터를 넘겨 받게 된다
그리고 그 넘겨 받은 함수에 버퍼와 크기를 넘겨주면 자동으로
( g_funcRecvMap[enumID](g_sockfd,recvBuf,ptSize); )
위에서 말한
void ProcessPacket(SOCKET_ID socketID, const AC_LogIn& ptData);
이러한 함수가 호출된다
단 패킷 생성기에서는 선언만 만들기 때문에 구현은 어플리케이션단에서
정의해주어야 하는것이다
자동으로 많은것을 만들려주다보면 소켓 구조에 의존성을 가지게 되어
재활용율이 떨어지게 되므로 이정도까지가 최선이라 생각이 되었다
필요하다면 코드를 수정해 더 필요한만큼 만들어내면 될것이지만
가장 최적으로 재활용 될만한 수준에서 공개하는게 바람직할거 같아 여기까지 해주는 것으로
일단락 하였다
참 그리고 패킷 제네레이터가 해줘야 할 중요한 작업이 하나더 있는데
PT_INTERFACE의 내용 구성물이 바뀔때마다 ( 멤버 데이터의 순서 바뀜 혹은 추가 삭제 )
버전업을 시켜 줘야 한다 그래서 버전이 다른 서버/클라이언트 간에 통신을 막아주어야 한다
예전 XX 게임에서는 빌드 할때마다 버전업을 시켜 , 클라이언트만 빌드해도 버전업이되어
서버를 내리고 다시 버전업 시킨 서버를 올려야하는 시스템이 있었다
프로토콜이 바뀌지 않는 이상 그럴 필요는 없으므로 버전은 프로토콜이 바뀔때만 올라가야한다
따라서 패킷 제네레이터는 PT_INTERFACE의 스트림에
담는 구조가 바뀌었는지 파악하기때문에
자동으로 프로토콜 버전업 관리도 하게 된다
----------------------------------------
다음버전에는 좀더 최적화된 버전으로 코드를 생성할수도 있게 옵션을 추가 할 예정이다
-------
참고 사항 :
스트림 클래스를 boost의 serialization을 쓰려 했으나 너무 무겁고 , 파일 쓰기에 중심을 맞춘거 같아
스트림 코드를 재작성하였다 , 그리고 제가 주말에만 취미로 작업을 하는데
일요일 회사 문이 잠겨 있어 T_T 문서화 작업을 끝내지 못해 공개를 조금 미루게 될거 같다
std::vector 와
답글삭제std::string 은
내부적으로 동적할당을 하기 때문에 정적할당보다 오버헤드가 높습니다.
네트윅의 환경이라면 대단히 빠른 주기로 패킷을 주고 받는 상황이 될 텐데
그때마다 동적할당으로 패킷을 만들어서 보내면 서버가 곤란할 것 같은데 어떤가요
패킷 생성기
#define PT_INTERFACE struct
#define PG_VECTOR std::vector
// 이것이 패킷 정의다
PT_INTERFACE CS_LOG_IN
{
String id;
String password;
PG_VECTOR itemList;
}
패킷 스트림
class PacketStream
{
PacketStream& operator << ( int value );
PacketStream& operator >> ( int value );
char m_buffer[1024];
}
보낸 사람 : han_juho@naver.com