디씨 프갤에 패킷 뭉치는 현상 질문한 넘이 있어서 간단하게 짜 본 코드.
메모리 관련해서 최적화할 여지는 많은데, 일단 로직을 보라는 개념으로 짠 거.
[질문 글] http://gall.dcinside.com/board/view/?id=programming&no=534180&page=3
[답 변]
우선 핸들러 recv_packet 을 만들면 그 쪽으로 수신 패킷이 넘어온다고 가정하자.
너가 만든 뭉치지 않은 순수한 패킷을 처리하는 로직 함수는 process_packet 이라 가정.
함수의 프로토타입은 대충 아래 정도로 되어 있다고 하자.
void recv_packet(const char *buffer, size_t len);
void process_packet(const char *buffer, size_t len) {
/* 너가 알아서 짜야 하는 부분 */
}
이럴 때 이 문제를 해결하는 방법은 크게 두 가지가 있다.
첫째로 구분자(EOS; End Of Stream) 방식을 이용한 해결 방법이 있어.
특정 바이트 패턴을 패킷의 끝을 나타내는 마커(end marker) 표지로 활용하는 거지.
패킷이 binary data가 아닌 text string이라면 구분자로 NUL character(널 문자, '\0')를 쓰는 게 간단할 거고.
백문이 불여일타. 코드를 보자.
void recv_packet(const char *buffer, size_t len) {
static char *data_total = NULL;
static size_t data_total_len = 0;
// 일단 복사하고 보자~
data_total = (char *)realloc(data_total, data_total_len + len);
memcpy(data_total + data_total_len, buffer, len);
// 새로 들어온 패킷 데이터에 널 문자가 있는지 루프를 돌면서 체크.
first:
size_t s;
for(s = data_total_len, data_total_len += len;
s < data_total_len && data_total[s] != '\0'; ++s);
// 널 문자가 있었다면 패킷이 끝난 것이니 너가 만든 패킷 처리 함수로 보내주자.
if(s < data_total_len) {
process_packet(data_total, s);
// 그리고 혹시나 남은 패킷이 있다면
if(++s < data_total_len) {
// 일단 처음으로 보내주고
memcpy(data_total, data_total + s, data_total_len - s);
// 길이를 바꿔서 처음부터 다시~
len = data_total_len - s, data_total_len = 0;
goto first;
}
}
}
* 참고로, 노파심에 첨언하자면.
이 코드에서 문제점이 있다면, realloc 함수로 메모리의 크기를 늘였다 줄였다 하기 때문에 메모리 단편화 현상이 심할 순 있어.
그 부분은 알아서 메모리 관리 루틴을 작성해서 해결하면 돼. 컨셉을 보여주기 위한 예제이므로 굳이 신경 안 씀.
한 가지 간단하게 해결하는 방법은, 어차피 패킷을 받아들이기 위한 버퍼는 늘어나기면 하면 되지 줄 필요는 없으니깐
peak 값만 realloc 처리하고 나머진 걍 대충 그냥 받아들여도 될 거야.
두번째로 프로토콜을 제대로 설계해서 그 헤더 정보 안에 패킷 길이 정보를 담는 방법이 있지.
그냥 간단히 패킷 길이만 헤더로 해서 보내는 상황을 가정해서 짠다면~
다음과 같이 간단히 짜볼 수 있어.
void recv_packet(const char *buffer, size_t len) {
static char *data_total = NULL;
static size_t total_size = 0, required_size = 0;
if(!data_total) { // 새로운 패킷을 받아들여야 하는 경우~
if(len < 8) exit(1); // 헤더에 패킷 길이는 8 bytes로 보내준다고 가정할게.
required_size = size_t(*(uint64_t *)buffer); // 요구되는 사이즈~
data_total = (char *)malloc(required_size); // 요구되는 사이즈만큼 메모리 할당
buffer += 8, len -= 8; // 사이즈 부분은 건너뛰자.
}
// 일단 데이터를 복사해야겠지?
memcpy(data_total + total_size, buffer, len);
total_size += len;
// 우리가 원하는 사이즈가 되었나 보자
if(total_size >= required_size) {
process_packet(data_total, required_size); // 패킷을 처리해 주고~
if( (total_size -= required_size) ) { // 아직도 패킷이 남았다면
char *old_data = data_total;
char *new_data = data_total + required_size;
size_t new_size = total_size;
// 버퍼를 초기화하고
data_total = NULL; total_size = required_size = 0;
// 남은 부분은 재귀 호출로 다시 처리하자.
recv_packet(new_data, new_size);
// 메모리는 해제해 주자
free(old_data);
} else { // 모든 게 끝났다면
// 메모리 해제하고 버퍼 정리만~
free(data_total);
data_total = NULL; total_size = required_size = 0;
}
}
}
* 잠깐!
이 코드도 잠재적 문제가 있는데, 할당 해제를 너무 잦게 해서 메모리 단편화 현상이 생길 수 있고,
또 재귀 호출을 썼기 때문에 스택 오버플로를 노린 DoS 공격이 가능할 수 있어.
어디까지나 개념 이해를 돕기 위해 짠 코드니깐 실제 이 코드를 긁어다가 쓸 때는 저 부분도 꼭 유의해서 쓰도록 해.