Linux ソケット・プログラミングの 5 つの落とし穴

異種混在環境で信頼性の高いネットワーク対応アプリケーションを開発する

Sockets API は、ネットワーキング・アプリケーション開発のためのデファクト・スタンダードな API です。この API はシンプルですが、開発初心者が共通して経験する問題がいくつかあります。この記事では、このような最も一般的な落とし穴を説明して、それらを克服する方法を示します。

M. Tim Jones (mtj@mtjones.com), Consultant Engineer, Emulex

M. Tim JonesM. Tim Jones は、埋め込みソフトウェアのエンジニアであり、GNU/Linux Application Programming, AI Application Programming と BSD Sockets Programming from a Multilanguage Perspective の著者でもあります。エンジニアとして経歴は幅広く、静止衛星用のカーネル開発から埋め込みシステム・アーキテクチャー、そしてネットワーク・プロトコル開発まで経験しています。現在は Emulex Corp. のシニア主席エンジニアです。



2005年 9月 20日

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 のフレーミングの欠如
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" ツールも見てください。なくてはならない新しいツールを見つけられるかもしれません。

参考文献

学ぶために

  • TCP ステート・マシンには 7 つの状態があります。W. Richard Steven 著の illustration from TCP/IP Illustrated, Volume 1 を見てください。
  • エンディアンについて、その歴史や意味するところを Wikipedia で調べてください。
  • オープンでスケーラブル、そしてカスタム化が可能な IBM の Power Architectureについて学んでください。
  • RPC/XDR についての紹介を、Programming in C コースウェアで学んでください。
  • XML-RPC と、Java アプリケーションの中での使い方について、「XML-RPC in Java programming」(developerWorks, 2004年 1月)を読んでください。
  • SOAP は XML-RPC の持つ特徴の上に構築されています。その仕様や実装、チュートリアル、記事などが SoapWare.Org に用意されています。
  • SCTP は TCP と UDP の特徴を組み合わせており、さらに可用性と信頼性についても考慮されています。
  • チュートリアル「Programming Linux sockets, Part 1」(developerWorks, 2003年 10月)は、ソケットを使ったプログラミングの始め方と、TCP/IP で接続されるエコー・サーバーとクライアントの構築の仕方について解説しています。「Programming Linux sockets, Part 2」(developerWorks, 2004年 1月)は UDP に焦点を当て、C と Python による UDP ソケット・アプリケーションの書き方を解説しています(コードは、他の言語にも容易に変換することができます)。
  • developerWorks の Linux ゾーンには、Linux 開発者のための資料が他にも豊富に用意されています。

製品や技術を入手するために

  • tcpdump ユーティリティーと tcpflow ユーティリティーは、ネットワーク・トラフィックを監視するために使われます。
  • Ethereal network protocol analyzer は、tcpdump の機能に加えて、グラフィカルUI とスケーラブル・プラグイン・アーキテクチャーを備えています。
  • 皆さんの次期 Linux 開発プロジェクトを、IBM ソフトウェア評価版を使って構築してください。developerWorks から直接ダウンロードすることができます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=231396
ArticleTitle=Linux ソケット・プログラミングの 5 つの落とし穴
publish-date=09202005