Build GameNetworkingSockets on Windows Programming

Build

Install OpenSSL

Install cmake

Install ninja

Enable vcvarsall

  • Add C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC to Path

Build protobuf

mkdir cmake_build
cd cmake_build
vcvarsall amd64
cmake -G Ninja -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_BUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=c:\sdk\protobuf-amd64 ..\cmake
ninja
ninja install
content_copy

Build GameNetworkingSockets

mkdir build
cd build
set PATH=%PATH%;C:\sdk\protobuf-amd64\bin
vcvarsall amd64
cmake -G Ninja ..
ninja
content_copy

Test example_chat

Copy files

  • GameNetworkingSocks dll files
  • example_chat.exe
  • protobuf dll files

Run Server

example_chat server
content_copy

Run Clients

example_chat client 127.0.0.1
/nick PlayerAAA
hi
How are you?
/quit

example_chat client 127.0.0.1
/nick PlayerBBB
hello
Fine
/quit

Output would be following.
example_chat client 127.0.0.1
/nick PlayerAAA
Ye shall henceforth be known as PlayerAAA
/nick PlayerBBB
Ye shall henceforth be known as PlayerBBB
PlayerAAA: hi
PlayerBBB: hello
PlayerAAA: How are you?
PlayerBBB: Fine
PlayerAAA departed
PlayerBBB departed

content_copy
content_cop

TCP로 작은 크기의 패킷 전송 번역

원문

Winsock TCP로 작은 크기의 data segment를 전송

Summary

TCP를 사용하여 작은 패킷을 보내야 할 경우, 어플리케이션의 디자인은 매우 중요합니다. 지연된 응답의 상호작용(네이글 알고리즘)을 고려하지 않으면 Winsock 버퍼링이 성능에 큰 영향을 끼칩니다. 이 글에서는 몇 가지 케이스 스터디를 통해 작은 패킷을 효율적으로 전송하는 방법을 알아봅니다.

Background

Microsoft TCP 스택이 패킷을 받으면 200ms의 지연 타이머가 시작됩니다. 수신 측에서 송신 측으로 ACK가 전송되면 지연 타이머는 리셋되고, 다음 패킷을 받으면 또 다시 시작됩니다. 수신된 패킷의 ACK를 언제 전송할 것인가를 결정하기 위해 MS TCP스택은 아래의 기준을 사용합니다.
  • 지연 타이머가 만료되기 전에 다음 패킷을 수신하면 ACK 전송
  • 다음 패킷을 수신하거나 지연 타이머가 만료되기 전에 전송 할 다음 패킷이 있으면 그 패킷에 편승하여 ACK 전송
  • 지연 타이머가 만료되면 ACK 전송
작은 패킷들이 네트워크를 혼잡하게 만드는 것을 회피하기 위해, MS TCP스택은 네이글을 기본적으로 on 합니다. 네이글 알고리즘은 원격에서 이전 패킷의 전송에 대한 ACK를 수신하기 전까지 작은 패킷들을 모으고 전송을 지연시킵니다. 아래는 네이글 알고리즘의 두 가지 예외 상황입니다.
  • MTU(최대 전송 유닛)보다 더 큰 패킷이 모아졌을 때, ACK를 기다리지 않고 바로 전송합니다. 이더넷 네트워크에서 TCP/IP의 MTU는 1460 byte 입니다.
  • TCP_NODELAY 옵션이 활성화 되어 있으면 작은 패킷들을 원격으로 바로 전송합니다.
어플리케이션 레이어의 성능을 향상 시키기 위해, Winsock은 어플리케이션의 전송 버퍼의 데이터를 Winsock 커널 버퍼로 복사합니다. 그리고 나서 스택은 패킷을 언제 원격으로 전송할지 결정하기 위해 휴리스틱을 사용합니다. SO_SNDBUF 옵션을 사용하여 Winsock 커널 버퍼의 크기를 변경할 수 있습니다. 기본값은 8K 입니다. 필요하다면 Winsock은 SO_SNDBUF 사이즈보다 더 큰 사이즈를 버퍼링 할 수 있습니다. 대부분의 경우 어플리케이션이 인지할 수 있는 전송 완료는 어플리케이션의 버퍼가 Winsock 커널 버퍼에 복사되었을 때 이고, 실제로 네트워크에 전송 중인지는 알 수 없습니다. SO_SNDBUF 를 0으로 설정함으로써 Winsock 버퍼링을 껐을 때는 예외입니다.

Winsock은 전송 완료를 어플리케이션에 통지하기 위해 아래의 룰을 따릅니다. 
  • SO_SNDBUF에 남는 버퍼가 있다면 어플리케이션 버퍼에서 복사하고, 어플리케이션에 전송 완료를 통보합니다.
  • SO_SNDBUF에 남는 버퍼가 없지만, 아직 보내지 않은 이전 패킷이 하나만 쌓여있다면 어플리케이션 버퍼에서 복사하고 어플리케이션에 전송 완료를 통보합니다.
  • SO_SNDBUF에 남는 버퍼가 없지만, 아직 보내지 않은 이전 패킷이 하나 이상 쌓여있다면 일단 어플리케이션 버퍼에서 복사하고 어플리케이션에는 전송 완료를 나중에 통보합니다.
Case Study 1

Overview

Winsock TCP 클라이언트가 TCP 서버에게 DB에 저장하기 위한 10000개의 레코드를 보내야 합니다. 레코드의 크기는 20 ~ 100 바이트 입니다. 어플리케이션 로직을 단순화 하기 위한 디자인은 아래와 같습니다.
  • 클라이언트는 블로킹 전송만 하고, 서버는 블로킹 수신만 합니다.
  • 클라이언트 소켓은 SO_SNDBUF를 0으로 설정하여, 매번 데이터 세그먼트를 하나씩 전송합니다.
  • 서버는 루프 안에서 recv를 호출합니다. 수신 버퍼를 200 byte로 배정하여 각각의 레코드가 하나의 recv 호출로 수신이 가능하도록 합니다.
성능

테스트를 통해 클라이언트는 1초에 5개의 레코드만 서버로 전송할 수 있다는 사실을 알수 있습니다. 10000개의 레코드(976KB, (10000x100)/1024)를 전송하는데 30분의 시간이 걸립니다.

분석

클라이언트 TCP_NODELAY가 off이기 때문에, 네이글 알고리즘이 ACK가 도착하기 전까지 다음 패킷을 원격으로 전송하지 못하게 합니다. 하지만 SO_SNDBUF를 0으로 설정해서 Winsock 버퍼링을 off 했습니다. 따라서 10000번의 send 호출과 ACK는 개별적으로 수행됩니다. TCP서버 스택에 아래의 현상이 발생해서 ACK는 매번 200ms 지연 됩니다.
  • 서버가 패킷을 받을 때마다 200ms 지연 타이머가 시작됩니다.
  • 서버는 클라이언트로 전송할 패킷이 없기 때문에, 수신한 패킷에 대한 ACK를 편승해서 전송 할 수 없습니다.
  • 서버의 지연 타이머가 만료되면 ACK가 전송됩니다.
개선 방안

위 디자인에는 2개의 문제점이 있습니다. 첫번째로 지연 타이머 문제 입니다. 클라이언트는 2개의 패킷을 200ms안에 서버로 전송 할 수 있어야 합니다. 네이글 알고리즘 기본값이 on이기 때문에, 클라이언트는 SO_SNDBUF를 0으로 하지 말고 Winsock 버퍼링을 사용해야 합니다. TCP 스택이 MTU보다 큰 패킷을 모으면, full-size 패킷이 ACK를 기다리지 않고 한번에 원격으로 전송됩니다.
두번째로 이 디자인은 매번 작은 크기의 패킷마다 send를 호출합니다. 작은 크기의 패킷을 전송하는 것은 매우 비효율적 입니다. 이 경우에는 80개의 레코드를 모아서 send 호출을 할 수 있습니다. 서버에게 전송될 전체 레코드 개수를 알려주기 위해, 클라이언트는 레코드 개수가 포함된 고정 크기 헤더 전송으로 통신을 시작할 수 있습니다.


코드 리뷰 시스템 Crucible Programming

Crucible은 Jira로 유명한 Atlassian에서 만든 코드 리뷰 시스템 입니다.
http://www.atlassian.com/software/crucible/

Code Collaborator에 비해 기능은 적지만 가격이 저렴합니다.

간단하게 리뷰를 올리는 프로세스를 보여드리겠습니다.

리뷰를 올리는 방식은 Post-Review가 있고 Pre-Review가 있습니다.
  • Post-Review : 저장소에 Commit 된 Changelist 를 기반으로 리뷰를 진행
  • Pre-Review : 소스를 Commit 하기전에 리뷰를 올려서 진행
먼저 Post-Review 프로세스 입니다.

대쉬보드에서 Review 탭을 클릭하면 "Create New Review"가 보입니다. 클릭.

Browse Changesets를 클릭

본인이 Commit한 Changelist가 보입니다. 적절한 항목을 선택하고 "Edit Details"를 클릭.
이렇게 바로 Changelist가 나오게 하려면 Crucible의 계정과 소스저장소의 계정이 동일해야 합니다.


리뷰어를 지정하고 "Start Review"를 클릭합니다.


여기서 부터는 Reviewer의 시선입니다. 변경된 파일들이 좌측에 트리구조로 보이는데, 하나씩 클릭해서 보면 됩니다.
이것은 없던 소스가 삽입된 경우.

이것은 기존 소스가 변경된 경우 입니다.

라인을 클릭하면 코멘트를 달 수 있습니다.

반드시 고쳐야하는 심각한 오류는 Defect를 체크하고 심각도, 분류 항목을 선택해 줍니다.

"Complete"를 클릭해서 Reviewee에게 보냅니다.

Reviewee는 코멘트를 확인하고 수정할것 수정한 후 "Summarize"를 클릭하여 리뷰를 정리합니다.


"Close Review"를 클릭하면 리뷰가 종료됩니다.

다음은 Pre-Review 입니다. Pre-Review를 받기 위해서는 소스저장소에서 patch를 만들어야 합니다. 퍼포스를 예로 들면 아래와 같이 명령줄에 입력하면 현재 pending list를 patch.txt로 만들어 줍니다.

p4 diff -dcu > patch.txt

Post-Review에서 Browse Changesets 선택했던 화면에서 Pre-Commit 선택하고 미리 만들어둔 patch.txt를 업로드 하면 됩니다.

이후는 Post-Review와 같습니다.

그들이 말하지않는 23가지 등 서평

★★★★★ (5개)

나쁜 사마리아인과 비슷한 이야기로 신자유주의의 허상에 대해 이야기 하는 책입니다. 나쁜 사마리아인보다 쉽게 설명되어 있어서 이해하기 좋았습니다. 읽는 재미도 있고 여러모로 추천할만한 책.


★★★★ (4개)
일본 스님이 쓰신 책인데, 잡념이 많을때, 쉽게 화가 날때, 이럴때 스스로를 어떻게 컨트롤 해야 할 것인가를 알려주는 책입니다. 무의식적으로 혹은 수동적으로 여러가지 행동을 동시에 하지 말고(음악 들으면서, 책 보면서, 옆사람 통화 엿들으면서... ), 하나의 행동에 집중해서 느끼고 생각하고 행동하라는 내용이 와닿았습니다.

★★ (2개)
저에겐 너무 어려웠습니다. 중간까지 읽다가 포기. 한국사람에게는 너무 생소한 미국역사상 정치적/사회적인 사건들을 별 설명없이 인용하는 경우가 많습니다. 미국 정치, 역사에 대한 지식이 좀 있어야 이해를 할듯합니다.

★★★★★ (5개)
예수의 생애를 좀 더 객관적인 시각에서 바라보아 재해석을 시도하는 책입니다. 사건에 대한 설명과 의견, 그것을 뒷받침하는 설명들이 상당히 논리적이라 많은 동감을 했고 또한 감동도 했습니다. 너무 재미있게 읽어서 김규항씨의 책들을 더 찾아보게 되는 계기가 되었습니다.

★★★★ (4개)
김규항씨의 블로그에 올렸거나 신문 혹은 잡지에 기고했던 글들을 모아서 발행한 책입니다. 동감가는 이야기도 많고 재미도 있습니다만 너무 짧은 글들의 모음이라 몰입은 잘 안되더군요.

Protocol Buffers를 패킷으로 활용해 보자 Programming

Protocol Buffers(이하 PB)를 패킷으로 활용해 보기위한 예제입니다.

패킷으로서 활용하려면 패킷이 뭉쳐서 올수 있으므로  메세지의 길이를 알아낼수 있다거나 끝을 표시해주는 기능이 필요합니다. 하지만 아래 링크의 글을 보시면,
http://code.google.com/apis/protocolbuffers/docs/techniques.html
하나의 스트림에 다수개의 메세지를 쓸때, 메세지의 끝이나 시작을 추적하는 것은 직접 구현해야 합니다. PB에는 메세지의 끝을 알아내는 기능이 없습니다. 이것을 해결하기 위한 가장 쉬운 방법은 메세지를 비트에 쓰기전에 그것의 사이즈를 쓰는것 입니다. 나중에 읽어낼때는 사이즈를 먼저 읽고 그 사이즈 만큼의 메세지의 본체를 읽어내면 됩니다.

헤더를 자체적으로 만들어서 사용하라고 권고합니다. 그래서 아래와 같이 헤더를 먼저 쓰고 그 다음에 메세지를 쓰는 방식으로 구현했습니다.


메세지가 미완성인지 체크해주는 기능은 있으므로 잘려서 오는 것은 걱정없습니다.
bool isCompleted = message.ParseFromCodedStream(&stream);
먼저 simplemessage.proto 파일 입니다.
package simple;

enum MessageType {
    LOGIN = 0;
    CHAT = 1;
    MOVE = 2;
}

message Login {
    required int32 id = 1;
    required string name = 2;
}

message Chat {
    required string name = 1;
    required int32 dst_id = 2;
    required string message = 3;
}

message Move {
    required int32 id = 1;
 
    message Position {
        required float x = 1;
        required float y = 2;
    }
    repeated Position track = 2;
}

이전 포스트 참조해서 h/cc파일을 생성합니다. 아래는 생성된 파일을 이용한 소스 파일 입니다.
#include <string>
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
#include <google/protobuf/text_format.h>
#include "simplemessage.pb.h"

using namespace std;
using namespace google;

// 메세지 사이에 끼워넣을 헤더 구조체
struct MessageHeader
{
   protobuf::uint32 size;
  simple::MessageType type;
};
const int MessageHeaderSize = sizeof(MessageHeader);

class PacketHandler
{
public:
   void Handle(const simple::Login& message) const
   {
    PrintMessage(message);
   }
  void Handle(const simple::Chat& message) const
   {
     PrintMessage(message);
   }
  void Handle(const simple::Move& message) const
   {
      PrintMessage(message);
  }

protected:
   // 메세지의 내용을 텍스트 형식으로 출력
  void PrintMessage(const protobuf::Message& message) const
   {
      string textFormatStr;
     protobuf::TextFormat::PrintToString(message, &textFormatStr);
     printf("%s\n", textFormatStr.c_str());
  }
};

// 패킷을 파싱해서 그것을 인자로 적절한 메서드를 호출해줌
void PacketProcess(protobuf::io::CodedInputStream& input_stream, const PacketHandler& handler)
{
  MessageHeader messageHeader;
   // 헤더를 읽어냄
  while( input_stream.ReadRaw(&messageHeader, MessageHeaderSize) )
   {
// 직접 억세스 할수 있는 버퍼 포인터와 남은 길이를 알아냄
    const void* payload_ptr = NULL;
     int remainSize = 0;
    input_stream.GetDirectBufferPointer(&payload_ptr, &remainSize);
    if (remainSize < (signed)messageHeader.size)
       break;

     // 메세지 본체를 읽어내기 위한 스트림을 생성
    protobuf::io::ArrayInputStream payload_array_stream(payload_ptr, messageHeader.size);
     protobuf::io::CodedInputStream payload_input_stream(&payload_array_stream);

     // 메세지 본체 사이즈 만큼 포인터 전진
     input_stream.Skip(messageHeader.size);

// 메세지 종류별로 역직렬화해서 적절한 메서드를 호출해줌
     switch(messageHeader.type)
      {
     case simple::LOGIN:
       {
          simple::Login message;
         if (false == message.ParseFromCodedStream(&payload_input_stream))
             break;
         handler.Handle(message);
        }
      break;
     case simple::CHAT:
        {
          simple::Chat message;
          if (false == message.ParseFromCodedStream(&payload_input_stream))
            break;
          handler.Handle(message);
       }
        break;
     case simple::MOVE:
       {
          simple::Move message;
         if (false == message.ParseFromCodedStream(&payload_input_stream))
            break;
         handler.Handle(message);
       }
        break;
   }
   }
}

// 메세지를 출력용 스트림에 쓴다
void WriteMessageToStream(
simple
::MessageType msgType,
const protobuf::Message& message,
protobuf::io::CodedOutputStream& stream)
{
  MessageHeader messageHeader;
   messageHeader.size = message.ByteSize();
  messageHeader.type = msgType;
   stream.WriteRaw(&messageHeader, sizeof(MessageHeader));
  message.SerializeToCodedStream(&stream);
}

int main(int argc, char* argv[])
{
  // 메세지에 값 세팅
   simple::Login login;
  login.set_name("alice");
   login.set_id(1);

  simple::Chat chat;
   chat.set_name("rabbit");
  chat.set_dst_id(2);
   chat.set_message("How are you doing?");

  simple::Move move;
   move.set_id(3);
  simple::Move::Position* pos0 = move.add_track();
   pos0->set_x(10.01f);
  pos0->set_y(10.02f);
  simple::Move::Position* pos1 = move.add_track();
   pos1->set_x(20.01f);
  pos1->set_y(20.02f);
   simple::Move::Position* pos2 = move.add_track();
  pos2->set_x(30.01f);
   pos2->set_y(30.02f);

  // 필요한 버퍼의 사이즈를 알아내서 버퍼를 할당
   int bufSize = 0;
  bufSize += MessageHeaderSize + login.ByteSize();
   bufSize += MessageHeaderSize + chat.ByteSize();
  bufSize += MessageHeaderSize + move.ByteSize();
   protobuf::uint8* outputBuf = new protobuf::uint8[bufSize];

  // 메세지를 출력할 스트림 생성
   protobuf::io::ArrayOutputStream output_array_stream(outputBuf, bufSize);
  protobuf::io::CodedOutputStream output_coded_stream(&output_array_stream);

   // 메세지를 스트림에 쓴다
  WriteMessageToStream(simple::LOGIN, login, output_coded_stream);
   WriteMessageToStream(simple::CHAT, chat, output_coded_stream);
  WriteMessageToStream(simple::MOVE, move, output_coded_stream);

   // 패킷 핸들러와 입력용 스트림 생성
  PacketHandler handler;
   protobuf::io::ArrayInputStream input_array_stream(outputBuf, bufSize);
  protobuf::io::CodedInputStream input_coded_stream(&input_array_stream);

   // 패킷 분석
  PacketProcess(input_coded_stream, handler);

   // 버퍼 해제
  delete [] outputBuf;
   outputBuf = NULL;
  return 0;
}
실행 결과 입니다.



1 2 3 4 5 6 7 8 9 10 다음