ブロードキャストプログラミングと、その時のネットワークの動き

ブロードキャストのパケットを送受信するプログラムと、送受信が行われる時のスイッチやルーターの動きを考えてみようと思います。C言語でプログラミングした場合です。まず一般的なユニキャストプログラミングの例を示します。

ユニキャストの場合

C言語でユニキャストプログラミングを行う場合、TCPとUDPとでコーディングが異なります。サーバー(受信)側とクライアント(送信)側があります。以下はTCPの場合の手順です。

次に、UDPの場合です。

TCPのほうがコネクションを確立(3ウェイハンドシェイク)してから通信を始めるため手順が少し複雑になっています。

ブロードキャストの場合

ブロードキャストのパケットを送れるのはUDPのみです。TCPはコネクションを通信相手と1対1で確立するのでユニキャスト通信しか行いません。なので、ブロードキャストパケットを送るプログラミングを行うにはUDPのユニキャストの手順をブロードキャスト用に修正します。修正点は以下の2つです。

  • 送信側のソケットをブロードキャストに対応させる。
  • 送信先IPアドレスにブロードキャストアドレスを指定する。

※受信側はユニキャストの場合と変わりません。

ソケットをブロードキャストに対応させるには、setsockopt()でSOL_SOCKETのSO_BROADCASTを有効にします。ソケットオプションの変更です。

setsockopt(socket, SOL_SOCKET, SO_BROADCAST, *optVal, optLen);

socketはソケットディスクリプタ、*optValは有効にする値「1」へのポインタ、optLenはoptValの長さ、を示します。またブロードキャストアドレスとして「255.255.255.255」を指定します。

ブロードキャストのパケットはホストからスイッチに送られるのですが、そのときの宛先MACアドレスは「FF-FF-FF-FF-FF-FF」となります。このパケット(イーサネットではフレーム)がスイッチに届くと、スイッチは全ポートに対してフラッディングを行います。フラッディングとは全ポートにパケットを転送してしまうことです。これによりそのスイッチにつながるホスト全台にパケットが届くことになります。ホストにあるNICはブロードキャストパケットを受信します(自身のIPアドレスとブロードキャストアドレスのパケットを受信する)。

なお、このパケットはルーターにも届くのですがルーターはブロードキャストパケットを通常は転送しません。ですので、ブロードキャストパケットが届く範囲はホストが属する同一セグメント内に限られます。

次回はマルチキャストのプログラミングと、その時のネットワークでのやりとりを考えようと思います。マルチキャストの場合はブロードキャストに比べ複雑です。

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

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