可変長データを含むUDPソケット通信のやり方

可変長のデータを含むUDPソケット通信のやり方が分からず、色々思考錯誤した結果をまとめてみます。間違いやもっと効率的なやり方があれば教えてください!

(2009/03/27)コード例の一部が間違っていました。指摘してくれたsatsumaさん、ありがとうございます。

問題設定

struct msg {
	uint8_t type;
	uint8_t value;
	uint16_t packet_size;   // パケットのサイズ
	char data[];            // 可変長データ
}

data部分が可変長であるようなパケットをUDPでやりとりするプログラムを考えます。可変長部分はクライアントが任意のデータを入れるとします。type、value、packet_sizeはヘッダーです。

これと同じパケットフォーマットで何人かがクライアントとサーバを実装しています。しかし、人によってデータ長部分の長さが違うため、このままだと相互接続することができません。具体的にいうと、recvfrom()で受信する際に受信バッファをどの程度取れば良いか分かりません。

固定長にしてみる(面白くない・・・)

解決策のひとつは、サーバ側でdata部分を固定長として扱ってしまうことです。例えば、サーバ側では以下の構造体をバッファとして受信します。

struct msg {
	uint8_t type;
	uint8_t value;
	uint16_t packet_size;
	char data[1024];       // 固定長にする
}

通信はちゃんとできますが、固定長じゃ面白くない。

構造体のメンバをポインタで扱う(これは間違い)

次にパケットのメンバをポインタとして扱ってみることを考えます。例えば、次のような構造体を定義してみます。

struct msg {
	uint8_t type;
	uint8_t value;
	uint16_t packet_size;
	char *data;            // ポインタにする
}

malloc()で任意のバイト長を割り当ててあげれば、data部分が可変長になります。しかし、このdata部分は実体を持たない(アドレスを保持しているだけ)ので、このパケットを受信した側ではdataを読み取ることができません。

可変長な構造体で扱う(今回の解)

ここまで考えて手詰まりになりました。そこでグーグル先生に質問してみたら、次のような解決策が見つかりました。

可変長な構造体 ( パソコン ) - udumbara的なメモ - Yahoo!ブログ

構造体を可変長にして扱います。まず、次のような構造体を定義します。

struct msg {
	uint8_t type;
	uint8_t value;
	uint16_t packet_size;
	char data[1];            // ひとまず固定長で定義
}

これを次のようなコードで使用します。

struct msg *recv_msg;
recv_msg = malloc(sizeof(msg) + data_len);     // 可変長分足して割り当てる

構造体に領域を後付けしていて、なんだかつぎはぎみたいな感じですが、これでうまくいきます。

構造体って、オフセットのアドレス値に名前をついているようなものと思うとわかりやすいかもです。

可変長な構造体 ( パソコン ) - udumbara的なメモ - Yahoo!ブログ(コメント欄より)

なるほど!

可変長部分の大きさを知るには?

これでdataのサイズに応じて受信バッファの大きさを変えることができるようになりました。しかし、まだ問題があります。

可変長部分の大きさは、送られてきたパケットのpacket_sizeメンバを見れば分かります。でもよく考えてみてください。packet_sizeメンバを見るには、一度受信する必要があります。受信バッファのサイズを決めるために、一度受信しなければいけないのです。まるで、「ニワトリが先か、ヒヨコが先か」みたいですね。

この問題を手っ取り早く解決する方法は、パケットを二度送ってもらうことです。一度適当なサイズのバッファ(dataを固定長にしたバッファ)で受信してpacket_sizeを知り、前述した方法で可変長バッファを用意します。そして、クライアントからもう一度同じパケットを再送してもらいます。

しかし、他の人が作ったクライアントが、二度も送ってくれるように実装されているとは考えられませんし、同じパケットを二度送ることはネットワークを輻輳させる原因になります。

受信キューから削除しない

実は、パケットを二度送信してもらわなくても、同じパケットを読み出すことができます。ここで、UDPにおいてパケットを受信する様子を知っておく必要があります。

UDPではソケットにパケットが到着すると、ソケット内の受信キューに登録されます。そして、recvfrom()によって受信キューに登録されたパケットを順に読み込みます。この読み込まれたパケットはrecvfrom()の引数で指定したバッファに格納されます。もしここで、指定したバッファよりもパケットサイズが大きかった場合は、残りのデータをすべて破棄します。また、バッファに読み込んだパケットは受信キューから消去されます。詳しくは次のページを参考にしてください。

第14回 TCPとUDP | 日経 xTECH(クロステック)

以上がデフォルトの動作です。このような状況下では、一度recvfrom()でパケットを読み込むと受信キューからデータが消されてしまうため、再度クライアントにパケットを送信してもらう必要があります。

これを防ぐために、もし受信キューからパケットを読み込んでも、キューからデータを消さないようにする必要があります。これはrecvfrom()の引数flagsにMSG_PEEKを指定することで実現できます。MSG_PEEKは「キューからデータを削除しない」フラグです。これにより、再度recvfrom()を行っても同じデータを読み込むことができるので、一旦固定長バッファで読み込んでから再度可変長バッファで読み込むといったことが可能になります。

コードとしては、次のようになります。

struct msg recv_msg1;
struct msg *recv_msg2;

// 受信キューから削除せずに、パケットを読み込む
recvfrom(sd, &recv_msg1, sizeof(recv_msg1), MSG_PEEK, (struct sockaddr *)&clnt_addr, &clnt_addr_len);

// パケットサイズの大きさに、受信バッファを拡張
recv_msg2 = malloc(recv_msg1.packet_size);

// データを受信する
recvfrom(sd, recv_msg2, sizeof(*recv_msg2), 0, (struct sockaddr *)&clnt_addr, &clnt_addr_len);

まとめ

  • UDPにおいて可変長データを含むパケットを扱うには、可変長構造体を用いる。
  • 可変長構造体は一旦固定長で定義し、必要に応じてmalloc()しなおすことで実現できる。
    • 構造体はメモリ領域にラベルを貼っているものと考えると分かりやすい。
  • 受信側でdataサイズを知るには、一旦固定長バッファで受信し、パケットサイズに応じて再度可変長バッファで受信しなおせばよい。
    • 同じパケットを読み込むには、recvfrom()にMSG_PEEKフラグを指定する。