UDP を使ってみよう (3)

前へ << UDP を使ってみよう (2) UDP を使ってみよう (5) >> 次へ

接続先が存在するかどうかわからない

UDP クライアントのスクリプトをじっくり見てみましょう。 すると、UDP プロトコルの特徴が少しずつ見えてきます。

udp-client-1.pl

   25: $sock_addr = pack_sockaddr_in($port, $iaddr);
まず、接続先を表すソケットアドレス構造体を作ります。
   38:     if ( ! send(SOCKET, "Hello $i", 0, $sock_addr) ){
そしてソケットアドレス構造体を渡しています。

TCP では socket を作って、connect してからデータ送信をしたのですが、 UDP では connect しません。connect しないということは、 そもそも接続先が存在するかどうかを知ることさえできないという意味です。

例えば

% ./udp-client-1.pl localhost 45678 (デタラメなポート番号を指定)
% ./udp-client-1.pl 192.168.111.222 45678 (デタラメな IP アドレスを指定)
などと、(サーバを起動せず) デタラメなポート番号を指定してクライアントを実行してみて下さい。 するとエラーが全く報告されません。send のエラーチェックはしているにもかかわらず、です。 UDP は、
相手先が存在するかどうかさえわからない
という、送りっぱなしのプロトコルなのです。
実は UDP で connect することもできますが、これは後ほど。

送信時に無視されるかもしれない

"Hello 1"〜"Hello 100000" を送信する部分をもう少し詳しく見てみましょう。
   37: for ( $i=1 ; $i<=$num_of_senddata ; ){
   38:     if ( ! send(SOCKET, "Hello $i", 0, $sock_addr) ){
   39:         # エラーが発生した
   40: 
   41:         if ( $! == Errno::ENOBUFS ){
   42:             # 送信バッファがいっぱい (ENOBUFS) ならリトライ
   43: 
   44:             $num_of_enobufs++;
   45:             next;
   46: 
   47:         } else {
   48:             # ENOBUFS 以外なら終了
   49:             die "send に失敗しました ($i)。$!\n";
   50:         }
   51:     }
   52:     $i++;
   53: }
send でエラーが発生したときは $! を見ます。 もし $! の内容が ENOBUFS だったら、再送します。 ENOBUFS というのは「送信バッファが一杯になった」というエラーです。

送信バッファというのは送信側 OS の内部にあるバッファのことで、 send したデータはいったん送信バッファに蓄えられ、その後 外部に送信されます。send を連続して呼び出すと、 外部への送信が間に合わず、ENOBUFS が発生します。

このプログラムでは ENOBUFS が発生すると、 再度同じデータを送信しています。

同じマシン内で udp-server-1.pl と udp-client-1.pl を実行すると、 ENOBUFS は一度も発生しないかもしれません (FreeBSD 4.4-RELEASE の同一マシン内で通信すると ENOBUFS は発生しませんでした)。

udp-server-1.pl を X68000.startshop.co.jp で実行し、 別マシンからインターネット経由で udp-client-1.pl を実行したところ、

% ./udp-client.pl X68000.startshop.co.jp 7000
X68000.startshop.co.jp:7000 に対して send を 100000 回実行しました。
ENOBUFS の発生回数 660182
となりました。10万回の送信に対して ENOBUFS が 66万回というのはかなり多いですが、 これは以下の理由によります。
  • データを連続して送信するという、テスト的なプログラムだったこと。
  • バッファが一杯な状態で即座に同じデータを送り直していること。 ENOBUFS が発生したときに 0.1 秒でもウェイトを入れていればかなり違うはず。
ここで $! について解説しておきましょう。よくファイルオープンのとき
open(IN, "/foo/bar/baz.txt") or die "$!\n";
と書きます。もしオープンに失敗すると
No such file or directory
と出力して終了しますので、 「エラー発生時に $! に文字列がセットされる」と考えがちですが、違います。 $! は文字列コンテキストではエラー文字列を返し、 数値コンテキストではエラー番号を返します。エラー番号というのは OS ごとに決められている番号で、FreeBSD ならば
  1. /usr/include/errno.h を見る
  2. cvsweb で src/sys/sys/errno.h を見る (/usr/include/errno.h の元になるファイル)
  3. intro(2) を見る (man 2 intro)
などの方法でエラー番号を見ることができます。errno.h を見ると
#define ENOBUFS   55    /* No buffer space available */
とありますので、
if ( $! == 55 ){ ... }
if ( $! eq "No buffer space available" ){ ... }
は (FreeBSD においては) 等価です。

ただし、ソースにマジックナンバを直接書くとソースの可読性が低くなりますし、 他の OS では 55 が EONBUFS を表すとは限りません (例えば Solaris9 では ENOBUFS は 132 です)。 エラー文字列も「No buffer space available」ではない OS があるでしょう。もしかしたら日本語 locale を使っていると (例えば LANG=ja としている場合)、 「バッファ容量が不足しています」 などと日本語のエラーメッセージがセットされているかもしれません。

そこで、ここでは use Errno として Errno パッケージを利用し、

if ( $! == Errno::ENOBUFS ){ ... }
としています。詳しい使い方は perldoc Errno して下さい。

なお、$! というのはシステムコールがセットするグローバル変数であることに注意して下さい。 内部でシステムコールを呼ばないライブラリ (例えば sprintf) は $! をセットしません。

C 言語を知っている方向けに書いておくと、perl では数値コンテキストのとき $! は errno の値が入っており、文字列コンテキストのときは perror がセットする文字列が入っています。

受信時に破棄されるかもしれない

送信時のデータ損失は ENOBUFS に対応することで防げました。 それなのに 96.4% のデータが失われたのは、受信側のバッファが溢れたためです。 受信側で、データが届いたにも関わらず、サーバアプリケーションの recv が間に合わない場合、そのデータは破棄されます。

転送時に破棄されるかもしれない

別マシンと通信をした場合、途中経路でデータが破棄されることがあります。

誰が破棄するかと言えば、それはルータです。 ルータはインタフェースを複数持っており、 ルーティングテーブルを参照して適切なインタフェースにデータを転送しなければなりません。 転送しないうちにどんどん新たなデータが入ってくる場合、データは破棄されます。

前へ << UDP を使ってみよう (2) UDP を使ってみよう (5) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。