通信管理クラスを使おう

通信対戦は当たり前?

最近は同人ゲームであっても、通信対戦が実装されて当たり前になってきています。
しかし、WinSockは色々と低レベルすぎて手が出しにくく、DirectPlayはサポートが終了してしまい死に体です。
そこで気軽に通信をするためのクラスを作りました。
CSocketクラスは、電装天使ヴァルフォースでも使用しているものです。

CSocketの使い方

  • connectで接続
  • sendでデータ送信
  • recvで受信データにアクセス
  • closeで通信終了

WinSockとほぼ変わりません。
しかし、WinSockで色々と義務付けられている面倒事をクラス内部で吸収するようになっています。

CServerの使い方

  • startで待ち受け開始
  • acceptで接続SOCKETを取得
  • stopで待ち受け終了

bindがどうとか、listenがどうとか、そういうWinSockの面倒な手順を吸収してあります。

通信内容を考えよう

サンプルを動かし、通信できることを確認したら、次は通信内容を考えましょう。
通信内容は単なるバイトデータのストリームです。
 
初心者が陥りやすい罠のため、まず真っ先に説明しておくべきことがあります。
それは「1回のsendで送ったからといって、1回のrecvで受信できるとは限らない」ということです。
5byteのデータをsendしたとしても、相手には1秒ごとに1byteずつ届く可能性もあります。
なのでまずは「これから何byteのデータを送ろうとしているか」を相手に認識させる必要があります。
下記のように、送信データの先頭に「送ろうとしているデータのサイズ」をつけるのが一般的です。
 
送信側

DWORD size = strlen(message);
socket->send(&size, sizeof(DWORD));
socket->send(message, size);
socket->update();

受信側

socket->update();
vector<byte> &data = socket->recv();
while(sizeof(DWORD) <= data.size())
{
  // サイズを表すDWORDが受信できていることを確認しました
  DWORD size = *(DWORD*)&data[0] + sizeof(DWORD);
  if (size <= data.size())
  {
    // データの内容も既に受信できていることを確認しました

    // ここで、データの中身を取り出して処理を行います

    // 処理が終わったので、受信バッファから処理済み部分を削除します
    data.erase(data.begin(), data.begin() + size);
  }
  else
  {
    // データの内容がまだ届いていないので、しばらく待つことにします
    break;
  }
}

これでもう、自由にデータをやりとりすることができます。

電装天使ヴァルフォースではどんな感じ?

// 構造体番号を定義
enum
{
  STRUCT_ID_ClientSitdown = 1,
  STRUCT_ID_... //他にもたくさん
};

// 構造体を定義
struct ClientSitdown
{
  DWORD size;     // 構造体のサイズ
  DWORD structId; // 構造体番号
  DWORD tableId;  // 筐体ID
};

struct... //他にもたくさん

// 筐体に着席し、対戦開始を待ちます
ClientSitdown c = {sizeof(ClientSitdown), STRUCT_ID_ClientSitdown, tableId};
socket->send(&c, sizeof(c));

こんな感じで、構造体をやりとりしています。
実際は改竄を検知できるような工夫を加えたり、解析しにくいように複雑化したりしていますが。

注意!

もちろんポインタはやりとりできません。
接続先は(普通は)違うアプリケーションですから、ポインタの指すアドレスが相手には理解不能ですからね。
構造体の中にポインタは含めないようにしましょう。