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

前回は特大データをUDPで送信した場合を考えてみましたが、今回はTCPで特大データを送信した場合を考えます。前回同様にC言語で送受信をする場合、送受信を行う関数はTCPではsend()とrecv()になります。

送信側:send() ※データを送る。
 ↓
 IPやTCPの機能でデータが送られる。
 ↓
受信側:recv() ※データを取り出す。

前回同様にsendの送信バッファを数メガ単位で確保し、いっきにデータ送信しようとした場合です。またソケットオプションのSO_SNDBUF、SO_RCVBUFもサイズ拡張しているという想定です。

UDPでは特大データを送信すると送信できるデータサイズに制限があったのでエラーとなるのですが、TCPでは上限がないので送信できます。実際にはやってないですが(ごめんなさい)、アプリケーションのsendではエラーにならないはずです(SO_SNDBUFが十分大きかったとして)。TCPのヘッダフォーマットを見てもUDPにはあったパケットの「長さ」を表すフィールドがありません。これはTCPには切れ目(境界)がないことを意味しています(これについては後述します)。

では、どのようにTCPがデータ送信するかというと、TCPレベルでデータリンク層のMTUをチェックしそれにあわせてデータを分割してしまいます。UDPの場合はIPがMTUをチェックしパケットを分割(IPフラグメンテーション)していましたが、TCPの場合はIPではなくTCPがパケットの分割を行います。分割して一度に送れるサイズをMSS(Maximum Segment Size)と呼んでいます。TCPの場合は「順序制御」や「再送制御」の機能があり、TCPレベルで1つ1つのパケットを管理する必要があるからでしょう。

MSSがあることによりTCPの場合はIPフラグメンテーションが起こりにくいです。TCPがMTUのサイズに合わせてパケットを分割してしまうのでIPフラグメンテーションは起こらないのでは?と思うかもしれませんが、経路の途中でMTUが小さくなる場合はフラグメンテーションが発生します。TCPがチェックするMTUはホストが接続された回線のMTUだからです。3ウェイハンドシェイクの際にMSSを交換して小さいほうが採用されます。

ちなみにIPv6では経路MTU探索という機能があり、送信する前に経路上のMTUの最小値をチェックするためフラグメンテーションが発生しにくくなっているようです。

パケットはMSSをもとに分割して送られるのですが、アプリケーションから見たときのイメージを以下に書きます。送信側でsendをして受信側でrecvを行うまでです。

送信側のアプリケーションがsendするとデータはOSに渡されSendキューに入ります。受信側のRecvキューに空きがあればTCPの機能によりデータは受信側に転送されます。送信側が送ってよいかどうか(受信側のRecvキューに空きがあるかどうか、あとどれだけ送ってよいのか)は、TCPの「確認応答」と「ウィンドウサイズ」の仕組みを使ってやりとりしています。Recvキューに入ったデータは受信側のアプリケーションがrecvすることによりデータを取得できます(recvすることにより受信側のRecvキューにまた空きができます)。

ここでSendキューには十分な大きさを確保しアプリケーションから同等の大きさのデータをsendで送ったとして、受信側のRecvキューのサイズがかなり小さかったら受信側はこまめにrecvをしないとRecvキューに空きができずデータの送信が完了しません。つまりTCPでは1回のsendでも複数回のrecvを行う必要があるということです。逆に送信側が複数回sendを行っても受信側では1回のrecvでデータを受け取ることもあります。ですのでTCPを使用した通信プログラムを作成する場合は1回のsendは1回のrecvと決めつけず、受信側で区切りとなる条件(バイト長や終了を意味するコードなどを使う)を見てプログラムを作成する必要があります。

この記事の最初にTCPには切れ目(境界)がないということを書いたのですが、これはTCPがデータを区切りのある1つ1つの塊として扱うのではなく、連続して続くストリームのように扱っているということを言いたかったわけです。なお、UDPではデータグラムという塊でデータを扱いますので1回のsendに対応するのは1回のrecvのみです。

以下に、TCPとUDPでの送信と受信の関係性を書いておきます。

TCP UDP
1回の送信で複数回の受信 ある ない
複数回の送信で1回の受信 ある ない
1回の送信で1回の受信 ある ある

また送信と受信の際のデフォルトのプログラムの制御ですが、TCPとUDPとで違う部分がありますので載せておきます。

■TCPの場合

【送信】
Sendキューに空きがあって送信データをキューに格納できればプログラムに制御が戻る。
Sendキューに空きがなければブロックされる(キューに格納できるまでアプリケーションに制御が戻らない)。

【受信】
Recvキューにデータが届いていてキューからデータを取得できればプログラムに制御が戻る。
Recvキューが空でデータが取得できなければブロックされる(データが届くまでアプリケーションに制御が戻らない)。

■UDPの場合

【送信】
再送制御がないためSendキューのようなバッファを使わない。アプリケーションがOSにデータを渡してすぐに制御がプログラムに戻る。

【受信】
UDPでも受信用のキューはあるためキューからデータを取得できればプログラムに制御が戻る。
キューが空でデータが取得できなければブロックされる(データが届くまでアプリケーションに制御が戻らない)。

なお、上記はあくまでデフォルトの動作であってノンブロッキングに制御を変えることもできます(その場合はソケットディスクリプタのパラメータを変更します)。

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においてはそうでもないのです。これについては次回書いてみようと思います。