패킷 직렬화 / 핸들링 라이브러리
What a Wontaeful World: 요즘의 삽질 #1 에서 트랙백
리카넷: 가짜블로그 – 패킷 직렬화/핸들링 라이브러리에서 트랙백
- IRC 에서도 몇 가지 사항을 논의 했던걸 덤프.
일단 발당군이 IRC에서 말했던, 각 메시지 타입 별로 비지터 패턴(visitor pattern)을 쓰는 것의 문제
- 각각의 패킷 핸들러마다 클래스를 정의 해줘야하는 문제 — 익명 클래스로 약간 단순화는 할 수 있음
- 패킷 구조(메시지에 담긴 데이터들의 형태?)가 바뀌는 경우, 비지터 패턴은 정말 적응하기 힘들다. (타입이 바뀐다!!)
그래서 다음과 같은, 약간 google protocol buffer를 마이너하게 따라한거 같은 구조를 쓰고 있다.
- 패킷을 각 메시지로 나누고,
- 메시지를 특정 파일 형식(xml 이나 yaml 같은 것)으로 설정하고,
- 이걸 특정 생성기를 만들어서 메시지를 주고/받는 코드(C++ class)를 생성하게 하는 것
- 이걸 가지고 각 메시지는 특정 아이디 값을 들고 다니며, 이걸 기준으로 메시지를 스위칭하는 코드를 (자동으로) 생성한다
- 메시지 스위칭은 (생성된 클래스의) 멤버 함수 포인터를 써서 테이블 룩업하는 형태1로 이루어진다
- 메시지를 실제로 네트웍에 보내거나 / 받을 때 하위 단에서 메시지 길이에 맞게 버퍼링하는 일은 해준다고 가정
접근법을 썼다. 그리고 실제로 패킷을 “네트웍에 보내고/가져오는 부분"은 별도로 처리하고 — 사실 이건 기저 라이브러리가 다를 수 있기에 — 패킷을 만들어내는 byte-stream serializer/deserializer 를 작성했다. 기본적으로 알 수 있는 타입은 ricanet에 댓글 달아놓은 것처럼 자동으로 다뤄지게 했고, STL 일부 타입도 다뤄지게 했다.2 여기서 안되는 확장은 stream 답게 operator<<
와 operator>>
를 선언해주면 동작하게 했다. operator 만 있으면 되기 때문에, 전송될 데이터 타입들에 대해 좀 덜 침입적이라는 판단 하에 저런 형태의 코드를 짰다.3
전체적으로는, A-B 사이의 통신인 경우 아래와 같은 레이어로 나뉘어 동작하게 된다.
다만 처음에 한 가정처럼, 패킷 메시지 형태가 계속 바뀐다면 — 그러니까 보내는 데이터 갯수나 타입 등등, 새로운 패킷 메시지가 추가된다거나/기존 메시지가 제거된다거나 — Generated dispatcher 코드가 계속 새로 생성된다. 이런 경우엔 메시지를 주고/받을 클래스를 인터페이스로 정의하고, 생성코드는 그 안에서 동작하게 해야 컴파일 시간(C++ 얘기지만)을 줄일 수 있다.
사실 이 구조를 써서 얻는 이점은, 실제로 바뀌는 부분이 generated-code로 최대한 한정된다는 점이고, 다른 두 가지 레이어에 해당하는 byte-stream 부분과 network-library 부분만 잘 고치면, 다른 언어/플랫폼과도 호환이 잘된다는 정도? 사실 복잡한 처리가 많은 곳(=예외적인 사항이 많은 부분)인 네트웍 부분을 따로 좀 떼어놓을 수 있는게 개인적인 취향이라 -_-a 덕분에 한 쪽 부분을 C/C++ 대신에 ActionScript 여야하는 상황에서도 금방 해당 부분을 짤 수 있긴 했다.
실제 응용 단에선 dispatch 되서 실행할 핸들러를 짜주고 / generated-code에 있는 전송 메서드들을 보고 쓰기만 하면 끝이다.4 예를 들어 ricanet의 코드라면, 실제로 보내고 / 푸는 부분은 안 보일테고, 생성된 클래스가 dispA 일 때,
dispA_instance->Send_MeatMessage();
dispA_instance->Send_RightRoundMessage(42, "you spin me");
같은 코드를 작성하게 될 것이다. 물론 이를 위해선 다음과 같은 메시지 정의가 필요하다.
<message name='Meat' />
<message name='RightRound'>
<arglist>
<basic type='int'/>
<basic type='std::string'/>
</arglist>
</message>
-
indirect jump cache miss야 좀 있겠지만, 메시지 갯수야 뻔히 유한하기에 어느 정도 완화된다고 가정했다. ↩︎
-
물론 이 코드를 타입별로 다 짜는건 불가능하기에, member function template의 힘을 빌렸다; 그리고
std::string
/std::wstring
등 몇몇 자주 쓰는 타입에 대해서는 template specialization을 써서 동작하게 했다. ↩︎ -
operator가 해당 데이터 타입의 멤버가 아니어도 되기 때문에 선언도 딴 곳에 해도되고, 적당한 getter만 있으면 해당 데이터 타입을 직접 수정할 필요도 없다. ↩︎
-
물론 실제 네트웍 단에 붙는 코드 (send-packet or recv-packet)는 “코드 생성기"에 미리 넣어줘야한다. ↩︎