출처 :http://mastercho.tistory.com/6
온라인 프로그램 패킷의 전통적 처리 방법 ( 장 단 점 )
온라인 프로그램을 짜는 프로그래머라면 누구나 프로토콜을 정의해 패킷을 만들고
클라이언트와 서버간에 통신하는 모듈을 만들어보았을것이다
그리고 이것은 일찍감치 프로그래머들에게 의해 2가지 패턴으로 정형화 되었는데 .....
첫째로는 구조체를 이용하는 방법이고 두째로는 스트림 클래스를 이용하는 방법이다
잘 이해가 안갈수 있으니 예를한번 살펴보자.
클라이언트와 서버간에 통신하는 모듈을 만들어보았을것이다
그리고 이것은 일찍감치 프로그래머들에게 의해 2가지 패턴으로 정형화 되었는데 .....
첫째로는 구조체를 이용하는 방법이고 두째로는 스트림 클래스를 이용하는 방법이다
잘 이해가 안갈수 있으니 예를한번 살펴보자.
첫번째 방법: 구조체 이용하기
struct HEADER
{
int type;
int size;
}
enum {ID_CS_LOG_IN = 25 };
struct CS_LOG_IN : public HEADER
{
CS_LOG_IN() { type = ID_CS_LOGIN; size = sizeof(*this); }
char id[32];
char password[32];
};
이렇게 struct를 정의하고 해당하는 enum값을 정의 한다음
struct 의 생성자에서 필요한 값을 넣어준다
그다음 내부 내용을 복사해 채워 넣은후 소켓함수로 데이타를 보내는게 일반적인것이다
CS_LOG_IN packet;
strcpy(id,"myID");
strcpy(password,"mypassword")
SendPacket(socket,(char*)&packet,sizeof(CS_LOG_IN));
받는쪽도 그냥 통째로 받아 헤더의 type를 보고 CS_LOG_IN 으로 캐스팅 처리하면 된다
장점 :
1. 대단히 직관적이고 쉽다.
프로토콜 패킷정의를 구조체로 볼수 있고, 구조체는 누구나 쉽게 바로 파악할수 있기 때문이다
2. 직관적이기 때문에 고전적으로 많이 쓰였다. 따라서 누구나 쉽게 소스를 구할수 있고
편하게 접근 할수 있다
3. 컴파일 타임때 구조체 크기가 결정되므로 패킷의 버퍼 오버플로우를 컴파일 타임때 체크할수 있다
4. 구조체 내용이 바뀌면 컴파일타임때 패킷을 쓰는 부분에서 에러를 내뱃어주므로
컴파일때 패킷처리의 오류를 고치기 쉽다
프로토콜 패킷정의를 구조체로 볼수 있고, 구조체는 누구나 쉽게 바로 파악할수 있기 때문이다
2. 직관적이기 때문에 고전적으로 많이 쓰였다. 따라서 누구나 쉽게 소스를 구할수 있고
편하게 접근 할수 있다
3. 컴파일 타임때 구조체 크기가 결정되므로 패킷의 버퍼 오버플로우를 컴파일 타임때 체크할수 있다
4. 구조체 내용이 바뀌면 컴파일타임때 패킷을 쓰는 부분에서 에러를 내뱃어주므로
컴파일때 패킷처리의 오류를 고치기 쉽다
단점 :
1. 일단 구조체 이므로 바이트 정렬에 신경 써야 한다.
컴파일러는 기본 정렬이 4-16 사이에 정의 되어 있으므로
#pragma pack(1) 같은 컴파일러에 영향주는 옵션을 줘야 하고
저걸 제대로 안했을경우 (클라이언트/서버) 구조체 데이터가 서로 호환이 안될수도 있기에
바이트 정렬 자체를 신경써야 한다는거 자체가 부담스럽다
2. 1번과 비슷한 문제지만 서버와 클라이언트의 플레폼이 다를 경우 CPU의 (빅/리틀)엔디안
때문에 호환이 되지 않는 경우가 발생할수도 있다. Marshal(마샬) 같은 작업이 필요할수가 있는것이다
3. 구조체는 컴파일 타임때 크기가 결정되므로 항상 최대 크기의 데이터를 보내야한다
예를 들면 유저가 채팅방에 0-100명까지 들어갈수 있다고 보자
입장할때 실제 방안에 있는 사람 숫자만큼만 데이터를 줄수 있어야 하지만
구조체는 항상 최대 크기인 100명을 소켓에 출력할수 밖에 없다
따라서 런타임시에 , 가변적인 크기에 대응하기 위해선
꽁수를 써야한다(이건 설명을 생략), 하지만 근본적으로 구조체 방법은 컴파일타임때 결정되는
구조이므로 꽁수로써 쓰다보면 프로토콜 자체가 점점 복잡해지며 처리 방법이 지저분해진다
패킷이 가변적인지 아닌지를 신경 써야 하는것이다.
컴파일러는 기본 정렬이 4-16 사이에 정의 되어 있으므로
#pragma pack(1) 같은 컴파일러에 영향주는 옵션을 줘야 하고
저걸 제대로 안했을경우 (클라이언트/서버) 구조체 데이터가 서로 호환이 안될수도 있기에
바이트 정렬 자체를 신경써야 한다는거 자체가 부담스럽다
2. 1번과 비슷한 문제지만 서버와 클라이언트의 플레폼이 다를 경우 CPU의 (빅/리틀)엔디안
때문에 호환이 되지 않는 경우가 발생할수도 있다. Marshal(마샬) 같은 작업이 필요할수가 있는것이다
3. 구조체는 컴파일 타임때 크기가 결정되므로 항상 최대 크기의 데이터를 보내야한다
예를 들면 유저가 채팅방에 0-100명까지 들어갈수 있다고 보자
입장할때 실제 방안에 있는 사람 숫자만큼만 데이터를 줄수 있어야 하지만
구조체는 항상 최대 크기인 100명을 소켓에 출력할수 밖에 없다
따라서 런타임시에 , 가변적인 크기에 대응하기 위해선
꽁수를 써야한다(이건 설명을 생략), 하지만 근본적으로 구조체 방법은 컴파일타임때 결정되는
구조이므로 꽁수로써 쓰다보면 프로토콜 자체가 점점 복잡해지며 처리 방법이 지저분해진다
패킷이 가변적인지 아닌지를 신경 써야 하는것이다.
결론 :
구조체 방법은 나름 편리하기도 하지만 가변적인 문제는 치명적이다
따라서 네트웍 부하를 고려해야하는 온라인 게임에는 일반적으로 적합하지 않다
하지만 꽁수를 씀으로써 극복하려는 프로젝트를 상당히 많이 보았으며 상용 게임에서도
꽤 많이 쓰이곤 한다. 하지만 꽁수로 극복해야 하는 것들이 발목잡으며 상당히 거슬리곤 한다
익숙해지면 괜찬다고 하지만 ..........
결론적으로는 네트웍 대역폭을 별로 신경 쓸 필요 없는 프로젝트에서 대부분 많이 쓰인다
------------------------------------------------------------------------------------------------
두번째 방법: 스트림 클래스 이용하기
c++ 프로그래머라면 누구나 std::cout<<"hello world"<<std::endl; 프로그램을 짜봤으리라
의심치 않는다
하지만 누구나 스트림의 대표격인 cout를 쓰면서도 스트림 클래스을 제대로 이해하고 활용하는 사람들은
일반적으로 많진 않은 편이다 , 나 역시 스트림을 그리 즐겨 사용하진 않는다
그렇지만 패킷 처리에 있어서만큼은 필수라 할만큼 꼭 필요한 방법인데 예를 한번 보자
(스트림 클래스의 구현은 인터넷에 많이 있다 모르는분은 찾아보자)
class PacketStream
{
// value to buffer
PacketStream& operator << ( int value );
// buffer to value
PacketStream& operator >> ( int value );
-------------------------------------------
char m_buffer[1024];
}
{
// value to buffer
PacketStream& operator << ( int value );
// buffer to value
PacketStream& operator >> ( int value );
-------------------------------------------
char m_buffer[1024];
}
enum { ID_LOBBY_USER_LIST = 26 };
struct User
{
char id[32];
int flag;
}
{
char id[32];
int flag;
}
-------------- 보내는쪽 ------------------------
// 서버에 있는 유저 데이타 리스트
std::vector< User > m_serverUserList;
PacketStream stream;
// 먼저 ID를 입력해주고
stream << ID_LOBBY_USER_LIST;
// 가변 크기의 입력
stream << m_serverUserList.size();
for(size_t i=0; i < m_serverUserList.size(); ++i)
{
// 갯수만큼 내용을 스트림에 입력
stream << m_serverUserList.id;
stream << m_serverUserList.flag;
}
SendPacket( socket, stream.GetBuffer(), stream.size() );
--------------------------------------------------------------------------
-------------- 받는 쪽 ------------------------
PacketStream stream;
// 소켓으로 부터 복사된 버퍼를 받아 stream에 입력한다
stream.CopyBuffer( socketBuffer, socketSize);
// id 출력해 파싱한다
int type;
stream >> type;
switch( type )
{
case ID_LOBBY_USER_LIST:
{
std::vector< User > m_clientUserList;
size_t userCount;
stream >> userCount;
{
std::vector< User > m_clientUserList;
size_t userCount;
stream >> userCount;
m_clientUserList.resize( userCount );
for(size_t i=0; i < m_serverUserList.size(); ++i)
{
stream >> m_clientUserList[i].id;
stream >> m_clientUserList[i].flag;
}
}
}
for(size_t i=0; i < m_serverUserList.size(); ++i)
{
stream >> m_clientUserList[i].id;
stream >> m_clientUserList[i].flag;
}
}
}
--------------------------------------------------------------------------
참고 : stream에서 STL를 지원하게 만들면 stream 작업이 상당히 간편해진다
장점:
1. 구조체의 가장 큰단점인 고정크기로 부터 자유롭다
2. 고정크기에 자유롭다는것만으로 온라인게임에서 가장 적합한 크기의 패킷을
만들어 낼수가 있다
자유롭다
단점 :
1. 런타임때 크기를 알수 있기때문에 런타임시에 버퍼 오버플로우와 언더플로우를 체크해야한다
2. 정확히 보내는 쪽과 받는쪽이 짝을 이루어야 한다
실수로 한쪽만 고치던가 다르게 고치면 컴파일 타임때는 잡아낼수 없고
런타임때 스트림이 꼬여 크래쉬를 내거나 눈에 띄는 버그를 발생 시킬때나 인식할수 있으므로
실수는 치명적이다.
[ 예를 들면 보내는쪽과 받는쪽의 타입 크기가 다르게 실수로 설정하던가 순서가 바뀌던가 하는... ]
이러한 실수는 적당히 스트림이 꼬여버려 애매한 버그를 만들어 버리고
마치 잘못된 메모리 참조 마냥 ...... 충격으로 다가오곤 한다.
[차라리 바로 크래쉬를 내주면 고맙다]
게다가 반복 노가다적인 패킷 설정 작업은 구조체 설정에 비해 상당히 불편한 편이며
패킷의 복잡도에 따라 나름 상당한 노가다 이기에 복사 붙여넣기를 무조건?하게 유발한다
또한 구조체에 비해 직관성이 떨어진다
따라서 작업을 하다보면 스트림 꼬이는 버그는 근본적으로 피하긴 어렵다