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

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