TCPとUDPでのデータ送信の違い(UDPの場合)

TCPとUDPとではアプリケーションデータを相手に届ける際の仕組みが異なります。今回はUDPの場合です。CでもJavaでも言語は何でも良いのですが、ここではC言語で通信を行うプログラムを作成したとします。C言語の場合、相手にデータを送る関数はsendto()を使用し、送られてきたデータを取り出す関数にはrecvfrom()を使用します。

送信側:sendto() ※データを送る。
 ↓
 IPやUDPの機能でデータが送られる。
 ↓
受信側:recvfrom() ※データを取り出す。
 
UDPがIPやイーサネットとどのように連携するのかも考えてみたいため、sendtoの送信バッファを数メガ単位で確保し、いっきにデータ送信しようとした場合で考えます。

注意:
実際はこのようなよろしくないプログラムは作らないと思いますが、そういう想定の話ということで。またソケットオプションのSO_SNDBUF、SO_RCVBUFもサイズ拡張しているという想定で。SO_SNDBUF、SO_RCVBUFはアプリケーションからOSに制御が渡ったときのOS側の送信/受信用のデータ格納領域です。

で、実際にそのような特大データをsendtoしてみると、エラーになります。UDPでは送信できるデータサイズに制限があるからです。送信できるデータサイズがいくつまでかというと65507バイトまでです。この数値がどこからきているのかということですが、以下にIPとUDPのヘッダフォーマットの図を示すのでまずは見てください。

IPヘッダにある「全パケット長」はIPパケットのサイズを表すのですが、全パケット長は16ビットであるため最大で2の16乗(つまり、65535バイト)までを格納します。このサイズはIPパケットのヘッダ長(20バイト)も含みますのでIPパケットのデータ部は65535バイトから20バイトを引いた65515バイトまでとなります。ここにUDPのデータグラムが格納されるわけですが、UDPにもヘッダがありUDPの実際のデータはもっと小さくなります。つまり、65515バイトからUDPヘッダ長(8バイト)を引いた65507バイトがUDPが一度に送信できるデータサイズとなります。なお、UDPヘッダの「長さ」のフィールドはUDPヘッダとデータ部をあわせた値が格納されます。

送信データのサイズを小さくして65507バイトのデータを送信したとしましょう。UDPはTCPと違い再送制御などの仕組みはないためアプリケーションデータはUDPからIPに引き渡されます。IPはイーサネットにデータを渡そうとするのですが、その前にMTUのチェックを行います。MTU(Maximum Trunsmission Unit)とはデータリンク層で送信できる最大データサイズのことです。イーサネットではMTUが1500バイト、光ファイバ(FDDI)は4352バイトと媒体により異なっています。アプリケーションデータがMTUのサイズを超えている場合、IPはパケットを分割してMTUのサイズ以下になるようにします。IPフラグメンテーションと言われている仕組みです。

なおフラグメントされたパケットのUDPヘッダですが、先頭のパケットのみにUDPヘッダが付与されます。残りのパケット(2個目以降のパケット)にはUDPヘッダは付与されません。経路途中にあるロードバランサがUDPヘッダの情報を見て処理をするようなケースでは、フラグメントされたパケットの処理がうまくできないので注意が必要です。

IPパケットの分割と組み立てはIPで行いますが、その際にIPヘッダにある「識別子」、「フラグ」、「フラグメントオフセット」のフィールドを利用します。IPはパケットを分割する際、フラグメンテーションしたパケットの識別子に同じID番号を割り当てます。そしてフラグを使い(厳密にはフラグの3ビット目を使い)フラグメントの途中のパケットか最後のパケットかを示します。フラグメントオフセットには元のデータのどの位置だったかを示す位置情報を格納するため、受信側でパケットを組み立てる際に利用します。

MTU以下に抑えられたパケットはイーサネットにわたり物理層のケーブルを通してネットワークに送られます。受信側にはパケットは分割されて届きますがIPがパケットの組み立てを行いUDPに引き渡します。受信側のアプリケーションプログラムは1回recvfromをすることで送られたデータを取り出すことができます。UDPでは1回の送信を1回の受信で行うことができます(パケットは分割されて到達しますが受信側で何度もrecvfromを行う必要はない、1回のrecvfromで受信できることが保証されている)。当たり前のことを書いているようですが、TCPにおいてはそうでもないのです。これについては次回書いてみようと思います。

アプリケーションデータのネットワークへの送信順

C言語でint型の整数値「1144201745」をメモリ上に格納すると、メモリの低位アドレスから高位アドレスに向かって4バイト分の領域が確保されます。ちなみに10進数の「1144201745」は16進数で表すと「0x44332211」です。メモリへの格納を考えると10進数で話をするより16進数で話をするほうがわかりやすいので「0x44332211」で話を進めます。

確保された4バイト分の領域ですがビットになおすと32ビット分です。このデータ値を構成するビット列のうち最上位にあるビットをMSB(Most Significant Bit)、最下位にあるビットをLSB(Least Significant Bit)と言います。なにが最上位でなにが最下位か(どちらが上でどちらが下か)というと、「0x44332211」を2進数に直したときに左側の桁(31ビット目の方)が最上位で、右側の桁(0ビットの方)が最下位になります。

MSB、LSBのことをバイトでの意味合いということで、(Most Significant Byte)、(Least Significant Byte)と書くときもあるようですが、この場合の捉え方は、MSBは最上位ビットが属するバイト、LSBは最下位ビットが属するバイト、と考えるべきと思います。

MSB、LSBをバイト単位で捉えるときの説明としてよく4バイトの領域をバイト単位で分割して左側を上位のバイト(MSB)、右側を下位のバイト(LSB)とするような説明を目にするのですが、そもそも4バイトで1つのデータを意味しており、それをバイト単位で「分割」してしまったらもとのデータの意味あいと変わってしまいます。それを上とか下とか言うのには違和感があって、MSB、LSBをバイト単位で捉えるときは、最上位ビット、最下位ビットそれぞれが属するバイトと考えるほうが納得がいきます。(これはあくまでも個人的な見解です。)

データは「バイト単位」にメモリに書き込まれるのですが、CPUの特性により格納方式が異なります。低位のアドレスから高位のアドレスに向かって、MSBからメモリに格納する方式をビッグエンディアン(big-endian)、LSBからメモリに格納する方式をリトルエンディアン(little-endian)と呼んでいます。「0x44332211」の場合、ビッグエンディアン、リトルエンディアンそれぞれの格納方法は以下のようになります。

メモリ上にビッグエンディアンで格納しようがリトルエンディアンで格納しようが、int型整数は4バイト単位でのアクセスとなるため、特に問題にはなりません。ただプログラムの実装にもよりますが、int型整数をchar型にキャストしてアクセスする場合は並び順が異なるためシステム環境(CPU特性)に左右されます。Aという機種でうまく動いていたものが、Bという機種でリコンパイルして動かしてみたら想定と違う、となることもありえます。

ネットワークへの送信についても同じで、ビッグエンディアンとリトルエンディアンとでは送信順が変わります。送信する際はメモリの低位アドレスのバイトから送信していきますが、ビッグエンディアンとリトルエンディアンではバイト単位で見ると並び順が異なりますので、送る側と受け取る側で並び順を決めておく必要があります。ネットワークへの送信にはビッグエンディアンを使うことになっていて、ネットワークバイトオーダーと呼ばれています。C言語ではネットワークバイトオーダーにあわせるためにlong型用にhtonl()、short型用にhtons()関数が用意されています。

ちなみにですが、big-endian、little-endianというのは「ガリバー旅行記」に出てきたものをマイクロプロセッサの世界で使っているみたいですね。

ネットワークへの送信はバイト単位で行われるのですが、バイトを構成するビットで考えた場合、一番最初に送信されるビットは最下位ビット(LSB)からとなります。

複数バイトのデータを送信する場合、低位アドレスの1バイト分がまず送信されるのですが、そのLSBが一番先にネットワークに流れます。1バイト(8ビット)が送られると次のバイトがネットワークに流れていきます。

イーサネットのMACアドレスは6バイト(48ビット)で構成されていて、前半の24ビットはOUI(Organizationally Unique Identifier)と呼ばれる機器ベンダー固有のコート値、後半24ビットがベンダーが自由に割り当てできる製品番号となっています。このMACアドレスですが、先頭1バイト目のLSBはI/Gビット(Individual/Groupビット)と呼ばれていて、これが「0」であればユニキャスト通信、「1」であればブロードキャストもしくはマルチキャスト通信を意味します。ネットワーク機器はI/Gビットを一番先に読み込んで、宛先の判断をしています。

なお、I/Gビットの左隣のビットはU/Lビット(Universal/Localビット)と呼ばれていて、これが「0」であれば世界で一意のグローバルアドレス、「1」であればローカルアドレスを意味しています。