レベル: 中級 M. Tim Jones, Consultant Engineer, Emulex
2005年 9月 20日 Sockets APIは、ネットワーキング・アプリケーション開発のためのデファクト・スタンダードなAPIです。このAPIはシンプルですが、開発初心者が共通して経験する問題がいくつかあります。この記事では、このような最も一般的な落とし穴を説明して、それらを克服する方法を示します。
4.2 BSD UNIX®オペレーティング・システムで初めて導入されたSockets APIは、今ではあらゆるオペレーティング・システムの標準機能になっています。事実、最近の言語でSockets APIをサポートしていないものを見つけるのは困難です。このAPIは比較的シンプルですが、それでも開発初心者が陥りがちな落とし穴がいくつかあります。
この記事では、そのような落とし穴を説明して、それらを回避する方法を示します。
落とし穴1. リターン・ステータスの無視
最初の落とし穴は、明白なものですが、開発初心者がしばしば犯す誤りです。関数のリターン・ステータスを無視すると、関数が失敗したことや部分的にしか成功していないことを見逃すことがあります。これがエラーを増殖させ、問題の源泉の特定を困難にすることがあります。
ステータス・リターンを無視するのではなく、それぞれのリターンを毎回捕捉し、チェックしてください。リスト1に示されているソケットsend関数の例を見てみましょう。
リスト1. API関数のステータス・リターンの無視
int status, sock, mode;
/* Create a new stream (TCP) socket */
sock = socket( AF_INET, SOCK_STREAM, 0 );
...
status = send( sock, buffer, buflen, MSG_DONTWAIT );
if (status == -1) {
/* send failed */
printf( "send failed: %s\n", strerror(errno) );
} else {
/* send succeeded -- or did it? */
}
|
リスト1は、ソケットsend(ソケット経由のデータ送信)を行う関数の断片を示しています。関数のエラー・ステータスが捕捉され、テストされますが、この例では、非ブロック・モード(MSG_DONTWAITフラグによって有効になります)でのsendの機能を無視しています。
send API関数には3種類のリターン値があります。
- データが正常に送信キューに入れられた場合は、ゼロが返されます。
- 失敗が発生した場合は、-1が返されます(その失敗はerrno変数を使用して理解することができます)。
- 1回の呼び出しですべての文字をキューに入れることができなかった場合は、送信文字数が最終的なリターン値になります。
MSG_DONTWAIT版sendの非ブロック性から、呼び出しがリターンするのは、全部または一部のデータを送信した後、またはデータをまったく送信できなかった後です。ここでリターン・ステータスを無視すると、不完全な送信となり、後続のデータが失われることになります。
落とし穴2. ピア・ソケットのクローズ
UNIXの興味深い一面として、ほぼすべてのものをファイルとして扱うことができます。ファイルそのもの、ディレクトリー、パイプ、デバイス、およびソケットもファイルとして扱われます。これは珍しい抽象化であり、さまざまなデバイス・タイプに対してAPIの集合を使用できることを意味します。
read API関数を考えてみましょう。これは、ファイルから数バイトを読み取ります。read関数は、読み取ったバイト数(指定した最大数まで)、エラーの場合は-1、またはファイルの終わりに達した場合はゼロを返します。
ファイルから読み取って、終わりに達した場合は(ゼロ長の読み取りによって示されます)、ファイルをクローズして完了です。ソケットでも同様ですが、意味合いが少し違います。ソケットの読み取りを行って、リターン値がゼロの場合、これはソケットのリモート・エンドのピアがclose API関数を呼び出したことを意味します。それが意味することは、ファイル読み取りの場合と同じです。すなわち、そのディスクリプター経由では、それ以上のデータは読み取られません(リスト2を参照)。
リスト2. read API関数リターン値の正しい取り扱い
int sock, status;
sock = socket( AF_INET, SOCK_STREAM, 0 );
...
status = read( sock, buffer, buflen );
if (status > 0) {
/* Data read from the socket */
} else if (status == -1) {
/* Error, check errno, take action... */
} else if (status == 0) {
/* Peer closed the socket, finish the close */
close( sock );
/* Further processing... */
} |
ピア・ソケットのクローズは、write API関数でも検出できます。この場合はSIGPIPE信号を受信します。または、この信号がブロックされていた場合は、write関数は-1を返して、errnoをEPIPEに設定します。
落とし穴3. アドレス使用中エラー(EADDRINUSE)
bind API関数を使用して、アドレス(インターフェースとポート)をソケット・エンドポイントにバインドすることができます。この関数をサーバー設定で使用して、着信接続が可能なインターフェースを制限することができます。また、この関数をクライアント設定から使用して、発信接続に使用されるインターフェースを制限することができます。bindの最も一般的な用途は、ポート番号をサーバーに関連付けて、ワイルドカード・アドレス(INADDR_ANY)を使用することです。これによって、任意のインターフェースを着信接続に使用することができます。
bindでよくある問題は、すでに使用中のポートをバインドしようとすることです。落とし穴は、アクティブなソケットが存在しないのに、ポートへのバインドが許されない(bindがEADDRINUSEを返します)ことです。これは、TCPソケットのTIME_WAIT状態が原因です。この状態は、ソケットのクローズ後、2分から4分間続きます。TIME_WAIT状態の終了後、ソケットは削除されて、問題なくアドレスを再バインドできるようになります。
ソケット・サーバーを開発していて、変更を加えるためにサーバーを停止して、その後、再起動する必要がある場合には特に、TIME_WAITの終了を待つのが煩わしいかもしれません。さいわい、TIME_WAIT状態を回避する方法があります。SO_REUSEADDRソケット・オプションをソケットに適用すると、ポートをすぐに再利用できます。
リスト3の例を見てみましょう。アドレスをバインドする前に、SO_REUSEADDRオプションを指定してsetsockoptを呼び出します。アドレスの再利用を有効にするために、整数引数(on)を1に設定します(アドレスの再利用を禁止するには、0に設定します)。
リスト3. SO_REUSEADDRソケット・オプションを使用して「アドレス使用中」エラーを回避する
int sock, ret, on;
struct sockaddr_in servaddr;
/* Create a new stream (TCP) socket */
sock = socket( AF_INET, SOCK_STREAM, 0 ):
/* Enable address reuse */
on = 1;
ret = setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
/* Allow connections to port 8080 from any available interface */
memset( &servaddr, 0, sizeof(servaddr) );
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
servaddr.sin_port = htons( 45000 );
/* Bind to the address (interface/port) */
ret = bind( sock, (struct sockaddr *)&servaddr, sizeof(servaddr) );
|
SO_REUSEADDRソケット・オプションを適用した後は、bind API関数は常にアドレスの即時再利用を許可します。
落とし穴4. 構造化データの送信
ソケットは、構造化されていないバイナリー・バイト・ストリームやASCIIデータ・ストリームを送信するには完璧な手段です(HTTPによるHTMLページの送信やSMTPによる電子メール送信)。しかし、バイナリー・データをソケット経由で送信しようとすると、はるかに複雑になります。
 |
バイト・スワップするか、しないか
エンディアンとは、メモリーでのバイト順序のことです。ビッグ・エンディアンは、最上位バイトから並べますが、リトル・エンディアンでは最下位バイトから並べます。 PowerPC®などのビッグ・エンディアン・アーキテクチャーは、Intel® Pentium®シリーズなどのリトル・エンディアン・アーキテクチャーに比べて、ネットワーク・バイト・オーダーがビッグ・エンディアンであるという点で有利です。このことは、TCP/IPスタック内の制御データが、ビッグ・エンディアン・マシンにとって自然な順序になっていることを意味します。リトル・エンディアン・アーキテクチャーの場合はバイト・スワッピングが必要です。これは、ネットワーク化アプリケーションにとって、わずかながら、パフォーマンス上の不利になります。
|
|
たとえば、整数をソケット経由で送信したいとします。受信者がその整数を同じように解釈するという保証はあるでしょうか。同じようなアーキテクチャーで動作しているアプリケーションは、共通のプラットフォームに依存して、型を同じように解釈できます。しかし、ビッグ・エンディアンのIBM PowerPC上で動作しているクライアントが、リトル・エンディアンのIntel x86に32ビット整数を送信しようとした場合はどうでしょうか。バイト・オーダーの違いから、値は正しく解釈されません。
C構造体をソケット経由で送信する場合はどうでしょうか。この場合もトラブルになります。なぜなら、すべてのコンパイラーが構造体の要素を同じように整列するわけではないからです。また、構造体は、無駄なスペースを少なくするために圧縮されたり、構造体の要素が誤って整列されることもあります。
さいわい、この問題には解決策があり、両方のエンドポイントで一貫したデータ解釈を確保することができます。その昔、リモート・プロシージャー・コール(RPC)ツールキットには、外部データ表現(External Data Representation:XDR)と呼ばれるものがありました。XDRは、異種混在のネットワーク・アプリケーションの通信の発展をサポートするために、データの標準表現を定義しました。
現在、いくつかの新しいプロトコルが同様の機能を提供しています。拡張可能マークアップ言語/リモート・プロシージャー・コール(XML/RPC)プロトコルは、HTTP経由でのプロシージャー・コールをXML形式で整列します。データとメタデータはXML内にエンコードされて、ASCII文字列として送信されるので、値とホスト・アーキテクチャーによる値の物理表現とが分離されます。XML-RPCに続いて、SOAPがさらに多くの機能によってそのアイデアを拡張しています。これらのプロトコルのそれぞれについては、参考文献を参照してください。
落とし穴5. TCPにおけるフレーミング前提
TCPはフレーミングを提供しません。このことは、バイト・ストリーム指向のプロトコルにとって好都合です。これは、TCPとUDP(User Datagram Protocol)の主な違いの1つです。UDPはメッセージ指向のプロトコルであり、送信者と受信者の間でメッセージの境界を維持します。TCPはストリーム中心のプロトコルであり、図1に示されているように、送受信されるデータが構造化されていないことを前提としています。
図1. UDPのフレーミング機能とTCPのフレーミングの欠如
図1の上の図は、UDPクライアントとサーバーを示しています。左側のピアは、それぞれ100バイトの2つのソケット書き込みを行います。スタックのUDP層は、書き込みの数量を追跡して、右側の受信者がソケット経由でデータを受け取ったときに同じ数量が届いたことを確認します。言い換えると、書き手が与えたメッセージの境界が、読み手のために保持されます。
今度は、図1の下の図を見てください。これは、TCP層での同じ粒度の書き込みを示しています。ストリーム・ソケットに、それぞれ100バイトの2つの書き込みが行われます。しかし、今度は、ストリーム・ソケットの読み手が受け取るのは200バイトです。スタックのTCP層は、2つの書き込みを集約しています。この集約は、TCP/IPスタックの送信側と受信側のどちら側でも行われます。集約は必ず行われるわけではありません。TCPは、データが順序正しく届けられることしか保証しません。
この落とし穴が、多くの開発者を困惑させます。開発者は、TCPには信頼性を、UDPにはフレーミングを求めます。STCP(Stream Transmission Control Protocol)など、別の転送プロトコルに切り替える以外に、バッファリングおよびセグメント化機能を実装するかどうかは、アプリケーション層の開発者しだいです。
ソケット・アプリケーションのデバッグ・ツール
GNU/Linuxには、ソケット・アプリケーションの問題発見に役立つデバッグ・ツールがいくつかあります。さらに、これらのツールは、アプリケーションとTCP/IPスタックの動作を説明する教育目的にも使用できます。ここでは、いくつかのツールの概要だけを示しますが、詳細については下記の参考文献を参照してください。
ネットワーキング・サブシステムの詳細の表示
netstatツールは、GNU/Linuxネットワーキング・サブシステムに対する視認性を提供します。netstatでは、現在アクティブな接続を確認したり(プロトコル別に)、特定の状態の接続を確認したり(待機状態になっているサーバー・ソケットなど)できます。リスト4に、netstatのいくつかのオプションと、それらによって有効になる機能を示します。
リスト4. netstatユーティリティーの使用パターン
View all TCP sockets currently active
$ netstat --tcp
View all UDP sockets
$ netstat --udp
View all TCP sockets in the listening state
$ netstat --listening
View the multicast group membership information
$ netstat --groups
Display the list of masqueraded connections
$ netstat --masquerade
View statistics for each protocol
$ netstat --statistics
|
他にも多くのユーティリティーが存在しますが、netstatは、route、ifconfig、およびその他の標準的なGNU/Linuxツールの機能をカバーする万能選手です。
トラフィックの監視
GNU/Linuxでは、いくつかのツールを使用して、ネットワーク上の低レベル・トラフィックを検査することができます。tcpdumpツールは、ネットワークからのネットワーク・パケットを"sniff"して、標準出力に書き出すか、ファイルに記録する、昔からあるツールです。この機能によって、アプリケーションが生成するトラフィックを監視することができ、TCPが生成する低レベル・フロー制御メカニズムを監視することもできます。より新しいtcpflowという名前のツールは、tcpdumpを補完して、プロトコル・フロー分析を行って、パケットの順序や再送信に関係なく、データ・ストリームを正しく再構築する手段を提供します。tcpdumpのいくつかの使用パターンをリスト5に示します。
リスト5. tcpdumpツールの使用パターン
Display all traffic on the eth0 interface for the local host
$ tcpdump -l -i eth0
Show all traffic on the network coming from or going to host plato
$ tcpdump host plato
Show all HTTP traffic for host camus
$ tcpdump host camus and (port http)
View traffic coming from or going to TCP port 45000 on the local host
$ tcpdump tcp port 45000
|
tcpdumpおよびtcpflowツールには、複雑なフィルター式の作成など、膨大な数のオプションがあります。これらのツールの詳細については、下記の参考文献を参照してください。
tcpdumpとtcpflowは、どちらもテキスト・ベースのコマンドライン・ツールです。グラフィカル・ユーザー・インターフェース(GUI)の方が好きな場合は、Etherealというオープン・ソース・ツールがよいでしょう。Etherealは、アプリケーション層のプロトコルのデバッグに役立つ本格的なプロトコル・アナライザーです。プラグイン・アーキテクチャーなので、HTTPなどのプロトコルや、考え付く限りのその他の任意のプロトコル(この記事の執筆時点で637プロトコル)を分析することができます。
まとめ
ソケット・プログラミングは、簡単で楽しいものです。標準的な防衛的プログラミング実践に加えて、この記事で述べた5つのよくある落とし穴に気をつけることによって、バグが紛れ込むのを避けたり、少なくとも、バグの発見を容易にすれば、さらに簡単で楽しいものになります。GNU/Linuxのツールとユーティリティーも、プログラムの問題を明らかにする助けとなります。忘れないでください。ユーティリティーのマニュアル・ページを見るときには、関連ツールや"see also"ツールも見てください。なくてはならない新しいツールを見つけられるかもしれません。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | M. Tim Jonesは、埋め込みソフトウェアのエンジニアであり、GNU/Linux Application Programming, AI Application Programming と BSD Sockets Programming from a Multilanguage Perspective の著者でもあります。エンジニアとして経歴は幅広く、静止衛星用のカーネル開発から埋め込みシステム・アーキテクチャー、そしてネットワーク・プロトコル開発まで経験しています。現在はEmulex Corp.のシニア主席エンジニアです。 |
記事の評価
|