この節では、基本的なソケットインタフェースの使用について説明します。
socket(3SOCKET) 呼び出しは、指定されたファミリに、指定されたタイプのソケットを作成します。
s = socket(family, type, protocol);プロトコルが指定されないと (値が 0)、システムは要求されたソケットタイプをサポートするプロトコルを選択します。ソケットハンドル (ファイル記述子) が返されます。
ファミリは、sys/socket.h で定義されている定数の 1 つで指定されます。AF_suite という名前の定数は、表 2-1 に示すように、名前を解釈する場合に使用するアドレス書式を指定します。
表 2-1 プロトコルファミリ
AF_APPLETALK |
Apple Computer, Inc. の Appletalk ネットワーク |
AF_INET6 |
IPv6 と IPv4 用のインターネットファミリ |
AF_INET |
IPv4 専用のインターネットファミリ |
AF_PUP |
Xerox Corporation の PUP インターネット |
AF_UNIX |
Unix ファイルシステム |
ソケットタイプは、sys/socket.h で定義されています。これらのタイプ (SOCK_STREAM、SOCK_DGRAM、または SOCK_RAW) は、AF_INET6、AF_INET、および AF_UNIX でサポートされます。次の文は、インターネットファミリでストリームソケットを作成します。
s = socket(AF_INET6, SOCK_STREAM, 0);
この呼び出しの結果、基本的な通信を提供する TCP プロトコルを使用したストリームソケットが作成されます。通常は、デフォルトプロトコル (引数 protocol が 0) を使用してください。「拡張機能」で説明しているように、デフォルト以外のプロトコルも指定できます。
ソケットは、名前のない状態で作成されます。ソケットにアドレスがバインドされるまでは、遠隔プロセスはソケットを参照できません。通信プロセスは、アドレスを介して接続されます。インターネットファミリでは、接続はローカルアドレス、リモートアドレス、ローカルポート、およびリモートポートから構成されます。protocol、local address、local port、foreign address、foreign port のような重複した順序セットは指定できません。
bind(3SOCKET) 呼び出しを使用すると、プロセスはソケットのローカルアドレスを指定できます。これは、local address、local port というセットになります。connect(3SOCKET) と accept(3SOCKET) は、アドレスの対のリモート側を決定することにより、ソケットの関連付けを完了します。bind(3SOCKET) 呼び出しは次のように使用します。
bind (s, name, namelen);
ソケットハンドルは s です。バインドされる名前は、サポートするプロトコルによって解釈されるバイト列です。インターネットファミリ名には、インターネットアドレスとポート番号が含まれます。
次の例は、インターネットアドレスのバインドを示しています。
#include <sys/types.h> #include <netinet/in.h> ... struct sockaddr_in6 sin6; ... s = socket(AF_INET6, SOCK_STREAM, 0); bzero (&sin6, sizeof (sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_addr.s6_addr = in6addr_arg; sin6.sin6_port = htons(MYPORT); bind(s, (struct sockaddr *) &sin6, sizeof sin6);
アドレス sin6 の内容は、インターネットアドレスのバインドについて説明した 「アドレスのバインド」で説明しています。
接続の確立は、通常、クライアントの役割を果たすプロセスと、サーバーの役割を果たすプロセスによって非同期で行われます。サーバーは、関連付けられた既知のアドレスと、接続要求のためのソケット上のブロックにソケットをバインドします。すると、関連のないプロセスがサーバーに接続できるようになります。クライアントは、サーバーがソケットに対する接続を開始すると、サーバーからサービスを要求します。クライアント側では、connect(3SOCKET) 呼び出しで接続を開始します。インターネットファミリの場合の例を次に示します。
struct sockaddr_in6 server; ... connect(s, (struct sockaddr *)&server, sizeof server);
接続呼び出しの時点でクライアントのソケットがバインドされていない場合、システムは自動的に名前を選択し、ソケットにバインドします。「アドレスのバインド」を参照してください。これは、ローカルアドレスがクライアント側のソケットにバインドされる一般的な方法です。
クライアントの接続を受信するには、サーバーはそのソケットをバインドしたあとに 2 つの処理を行う必要があります。まず、待ち行列に入れることができる接続要求の数を示します。続いて接続を受け入れます。
struct sockaddr_in6 from; ... listen(s, 5); /* 5 つの接続待ち行列を許可する */ fromlen = sizeof(from); newsock = accept(s, (struct sockaddr *) &from, &fromlen);
ソケットハンドル s は、接続要求の送信先であるアドレスにバインドされるソケットです。listen(3SOCKET) の 2 つ目のパラメータは、待ち行列に入れることができる未処理の接続の最大数を指定します。from は、クライアントのアドレスを指定する構造体です。場合によって NULL ポインタが渡されます。fromlen は構造体の長さです (UNIX ファミリでは、from は struct sockaddr_un として宣言される)。
accept(3SOCKET) は一般にブロックします。accept(3SOCKET) は、要求側クライアントに接続されている新しいソケット記述子を返します。fromlen の値は、アドレスの実際のサイズに変更されます。
サーバーは、特定のアドレスからのみ接続を受け入れます。しかしこのことを示すことができません。サーバーは、accept(3SOCKET) が返す from アドレスをチェックし、受け入れ不可能なクライアントとの接続を閉じることができます。サーバーは、複数のソケット上の接続を受け入れることも、あるいは accept 呼び出しのブロックを避けることもできます。これらの手法については、「拡張機能」で説明しています。
接続が失敗する場合はエラーが返されます (しかし、システムによってバインドされたアドレスは残る)。成功すると、ソケットがサーバーと関連付けられ、データ転送を開始できます。
表 2-2 は、接続が失敗する場合に返される一般的なエラーをいくつか示しています。
表 2-2 ソケット接続エラー
ソケットエラー |
エラーの説明 |
---|---|
ENOBUFS |
呼び出しをサポートするためのメモリーが足りない |
EPROTONOSUPPORT |
不明なプロトコルの要求 |
EPROTOTYPE |
サポートされないソケットタイプの要求 |
ETIMEDOUT |
指定された時刻に接続が確立されていない。これは、宛先ホストがダウンしているか、あるいはネットワーク内の障害で伝送が中断される場合に発生する |
ECONNREFUSED |
ホストがサービスを拒否した。これは、要求されたアドレスにサーバープロセスが存在しない場合に発生する |
ENETDOWN または EHOSTDOWN |
これは、基本通信インタフェースが配信するステータス情報によって起こる |
ENETUNREACH または EHOSTUNREACH |
このオペレーションエラーは、ネットワークまたはホストに対する経路がないため、あるいは中間ゲートウェイまたは切り替えノードが返すステータス情報が原因で起こる。返されるステータスが十分でないために、ダウンしているネットワークとダウンしているホストが区別できない場合もある |
この節では、データの送信と受信のための関数について説明します。メッセージの送受信は、次のように通常の read(2) インタフェースと write(2) インタフェースを使用して行います。
write(s, buf, sizeof buf); read(s, buf, sizeof buf);
また、次のように send(3SOCKET) 呼び出しと recv(3SOCKET) 呼び出しも使用できます。
send(s, buf, sizeof buf, flags); recv(s, buf, sizeof buf, flags);
send(3SOCKET) と recv(3SOCKET) は read(2) と write(2) に非常によく似ていますが、flags 引数が重要です。必要に応じて、sys/socket.h で定義されている以下のフラグをゼロ以外の値として指定できます。
帯域外データは、ストリームソケットに固有のものです。recv(3SOCKET) 呼び出しで MSG_PEEK が指定される場合、存在するすべてのデータがユーザーに返されますが、データは読み取られていないものとして扱われます。ソケット上の次の read(2) または recv(3SOCKET) 呼び出しは、同じデータを返します。発信パケットに適用されるパケット経路を指定しないデータ送信オプションは、現在、ルーティングテーブルの管理プロセスにだけ使用されており、一般にはほとんど使用されません。
SOCK_STREAM ソケットは、close(2) 関数呼び出しで破棄できます。close(2) のあとで信頼できる配信が見込まれるソケットにデータが待ち行列化されている場合、プロトコルは継続してそのデータの転送を試みます。期限が来てもデータが配信されない場合は、データが破棄されます。
shutdown(3SOCKET) は、SOCK_STREAM
ソケットを正常に閉じます。両方のプロセスで送信が行われなくなっていることを認識できます。この呼び出しの書式は次のとおりです。
shutdown(s, how);
how は次のように定義されています。
0 |
それ以上の受信を許可しない |
1 |
それ以上の送信を許可しない |
2 |
それ以上の受信と送信を許可しない |
図 2-1 と次の 2 つの例は、インターネットファミリのストリーム接続の開始と受け入れを示しています。
例 2-1 はサーバー側のプログラムです。このプログラムは、ソケットを作成してそのソケットに名前をバインドし、続いてポート番号を表示します。このプログラムは listen(3SOCKET) を呼び出して、ソケットが接続要求を受け入れる用意ができていることをマークし、要求の待ち行列を初期化します。プログラムの残り部分は無限ループです。ループの各パスは、新しいソケットを作成することによって新しい接続を受け入れ、待ち行列からその接続を削除します。サーバーは、ソケットからのメッセージを読み取って表示し、メッセージを閉じます。in6addr_any の使用については、「アドレスのバインド」で説明しています。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #define TRUE 1 /* * このプログラムは、ソケットを作成したあと無限ループを開始します。 * ループごとに接続を受け入れ、その接続からのデータを出力します。 * 接続が遮断するか、あるいはクライアントが接続を閉じる時点で * プログラムは新しい接続を受け入れます。 */ main() { int sock, length; struct sockaddr_in6 server; int msgsock; char buf[1024]; int rval; /* ソケットを作成する。*/ sock = socket(AF_INET6, SOCK_STREAM, 0); if (sock == -1) { perror("opening stream socket"); exit(1); } /* ワイルドカードを使用してソケットをバインドする。*/ bzero (&server, sizeof(server)); bzero (&sin6, sizeof (sin6)); server.sin6_family = AF_INET6; server.sin6_addr.s6_addr = in6addr_any; server.sin6_port = 0; if (bind(sock, (struct sockaddr *) &server, sizeof server) == -1) { perror("binding stream socket"); exit(1); } /* 割り当てられたポート番号を調べ、それを出力する。*/ length = sizeof server; if (getsockname(sock,(struct sockaddr *) &server,&length) == -1) { perror("getting socket name"); exit(1); } printf("Socket port #%d¥n", ntohs(server.sin6_port)); /* 接続の受け入れを開始する。*/ listen(sock, 5); do { msgsock = accept(sock,(struct sockaddr *) 0,(int *) 0); if (msgsock == -1 perror("accept"); else do { memset(buf, 0, sizeof buf); if ((rval = read(msgsock,buf, 1024)) == -1) perror("reading stream message"); if (rval == 0) printf("Ending connection¥n"); else /* データが出力可能であると想定する */ printf("-->%s¥n", buf); } while (rval > 0); close(msgsock); } while(TRUE); /* * このプログラムには無限ループが含まれるため、ソケット "sock" は * 明示的に閉じられることはありません。しかし、プロセスが中断されるか * 正常に終了する場合は自動的に閉じます。 */ exit(0); }
接続を開始するため、例 2-2 のクライアント側プログラムは、ストリームソケットを作成し、接続用のソケットのアドレスを指定して connect(3SOCKET) を呼び出します。宛先ソケットが存在し、要求が受け入れられる場合は、接続が完了し、プログラムはデータを送信できます。データは、メッセージ境界なしで順番に配信されます。接続は、一方のソケットが閉じられた時点で遮断されます。このプログラム内のデータ表現ルーチン (ntohl(3SOCKET)、ntohs(3SOCKET)、 htons(3SOCKET)、htonl(3XNET) など) の詳細については、byteorder(3SOCKET) のマニュアルページを参照してください。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #define DATA "Half a league, half a league . . ." /* * このプログラムはソケットを作成し、コマンド行で指定 * されるソケットを使用して接続を開始します。この接続で * データがいくらか送信されたあとソケットが閉じられ、 * 接続が終了します。 * コマンド行の書式: streamwrite hostname portnumber * 使用法: pgm host port */ main(argc, argv) int argc; char *argv[]; { int sock, errnum h_addr_index; struct sockaddr_in6 server; struct hostent *hp; char buf[1024]; /* ソケットを作成する。*/ sock = socket( AF_INET6, SOCK_STREAM, 0); if (sock == -1) { perror("opening stream socket"); exit(1); } /* コマンド行で指定される名前を使用してソケットを接続する。*/ bzero (&sin6, sizeof (sin6)); server.sin6_family = AF_INET6; hp = getipnodebyname(AF_INET6, argv[1], AI_DEFAULT, &errnum); /* * getinodebyname が、指定されたホストのネットワーク * アドレスを含む構造体を返す。 */ if (hp == (struct hostent *) 0) { fprintf(stderr, "%s: unknown host¥n", argv[1]); exit(2); } h_addr_index = 0; while (hp->h_addr_list[h_addr_index] != NULL) { bcopy(hp->h_addr_list[h_addr_index], &server.sin6_addr, hp->h_length); server.sin6_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *) &server, sizeof (server)) == -1) { if (hp->h_addr_list[++h_addr_index] != NULL) { /* 次のアドレスを試みる */ continue; } perror("connecting stream socket"); freehostent(hp); exit(1); } break; } freehostent(hp); if (write( sock, DATA, sizeof DATA) == -1) perror("writing on stream socket"); close(sock); freehostent (hp); exit(0); }
データグラムソケットは、同期データ交換インタフェースを提供します。接続を確立するための必要条件はありません。各メッセージには、宛先アドレスが含まれます。図 2-2 は、サーバーとクライアント間の通信の流れを示しています。
次の図のサーバー側で示されている bind(3SOCKET) の手順は省略できます。
データグラムソケットは、「ソケットの作成」で説明しているように作成されます。特定のローカルアドレスが必要な場合、bind(3SOCKET) オペレーションが最初のデータ伝送よりも先行しなければなりません。それ以外の場合、データが最初に送信される際にシステムがローカルアドレスまたはポート (あるいはこの両方) を設定します。データの送信には、sendto(3SOCKET) を使用します。
sendto(s, buf, buflen, flags, (struct sockaddr *) &to, tolen);
s、buf、buflen、および flags パラメータは、コネクション型のソケットの場合と同じです。to と tolen の値は、意図するメッセージ受信者のアドレスを示します。ローカルにエラー条件 (到達できないネットワークなど) が検出されると、-1 が戻り、errno がエラー番号に設定されます。
データグラムソケット上のメッセージを受信するには、recvfrom(3SOCKET) を使用します。
recvfrom(s, buf, buflen, flags, (struct sockaddr *) &from, &fromlen);
呼び出しの前に、fromlen が from バッファーのサイズに設定されます。fromlen は、戻り時には、データグラムの配信元であるアドレスのサイズに設定されます。
データグラムソケットは、ソケットを特定の宛先アドレスと関連付けるために connect(3SOCKET) 呼び出しを使用することもできます。続いて、send(3SOCKET) 呼び出しを使用できます。宛先アドレスの明示的な指定がない状態でソケット上で送信されるデータはすべて、接続されたピアにアドレス指定され、そのピアから受信されるデータだけが配信されます。1 つのソケットで一度に許可されるのは、接続された 1 つのアドレスだけです。2 つ目の connect(3SOCKET) 呼び出しは、宛先アドレスを変更します。データグラムソケット上の接続要求は、ただちに返されます。システムは、ピアのアドレスを記録します。accept(3SOCKET) と listen(3SOCKET) は、データグラムソケットでは使用されません。
データグラムソケットが接続されている間、前の send(3SOCKET) 呼び出しからのエラーは非同期に返すことができます。これらのエラーは、そのソケットの後続のオペレーションで報告できます。また、getsockopt(3SOCKET) のオプションである SO_ERROR を使用してそのエラーステータスを問い合わせることもできます。
例 2-3 は、ソケットの作成、ソケットへの名前のバインド、およびソケットへのメッセージ送信によってインターネット呼び出しを送信する方法を示しています。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #define DATA "The sea is calm, the tide is full . . ." /* * ここで、コマンド行引数から得られる名前を持つ受信者に * データグラムを送信する。 * コマンド行の書式: dgramsend hostname portnumber */ main(argc, argv) int argc, errnum; char *argv[]; { int sock; struct sockaddr_in6 name; struct hostent *hp; /* 送信を行うソケットを作成する。*/ sock = socket(AF_INET6,SOCK_DGRAM, 0); if (sock == -1) { perror("opening datagram socket"); exit(1); } /* * 「送信」先ソケットの、ワイルドカードを使用しない構造名。 * getinodebyname は、指定されたホストのネットワークアドレスを * 含む構造体を返します。ポート番号は、コマンド行から取得され * ます。 */ hp = getipnodebyname(AF_INET6, argv[1], AI_DEFAULT, &errnum); if (hp == (struct hostent *) 0) { fprintf(stderr, "%s: unknown host¥n", argv[1]); exit(2); } bzero (&sin6, sizeof (sin6)); bzero (&name, sizeof (name)); memcpy((char *) &name.sin6_addr, (char *) hp->h_addr, hp->h_length); name.sin6_family = AF_INET6; name.sin6_port = htons(atoi(argv[2])); /* メッセージを送信する。*/ if (sendto(sock,DATA, sizeof DATA ,0, (struct sockaddr *) &name,sizeof name) == -1) perror("sending datagram message"); close(sock); exit(0); }
例 2-4 は、ソケットの作成、ソケットへの名前のバインド、およびソケットからの読み取りによってインターネット呼び出しを読み取る方法を示しています。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> /* * このプログラムは、データグラムソケットを作成し、そのソケッ * トに名前をバインドし、続いてそのソケットから読み取ります。 */ main() { int sock, length; struct sockaddr_in6 name; char buf[1024]; /* 読み取られるソケットを作成する。*/ sock = socket(AF_INET6, SOCK_DGRAM, 0); if (sock == -1) { perror("opening datagram socket"); exit(1); } /* ワイルドカードを使用して名前を作成する。*/ bzero (&sin6, sizeof (sin6)); name.sin6_family = AF_INET6; name.sin6_addr.s6_addr = in6addr_any; name.sin6_port = 0; if (bind(sock,(struct sockaddr *)&name, sizeof name) == -1) { perror("binding datagram socket"); exit(1); } /* 割り当てられたポート値を確認し、それを出力する。*/ length = sizeof(name); if (getsockname(sock,(struct sockaddr *) &name, &length) == -1) { perror("getting socket name"); exit(1); } printf("Socket port #%d¥n", ntohs(name.sin6_port)); /* ソケットから読み取りを行う。*/ if (read(sock, buf, 1024) == -1 ) perror("receiving datagram packet"); /* データが出力可能であると想定する。*/ printf("-->%s¥n", buf); close(sock); exit(0); }
要求は、複数のソケットまたはファイルの間で多重化できます。このためには select(3C) を使用します。
#include <sys/time.h> #include <sys/types.h> #include <sys/select.h> ... fd_set readmask, writemask, exceptmask; struct timeval timeout; ... select(nfds, &readmask, &writemask, &exceptmask, &timeout);
select(3C) の最初の引数は、続く 3 つの引数によって示されるリスト内のファイル記述子の数です。
select(3C) の 2 つ目、3 つ目、4 つ目の引数は、3 つのファイル記述子セット (読み取りを行う記述子セット、書き込みを行うセット、および例外条件が認められるセット) を指します。帯域外データは、唯一の例外条件です。これらのポインタはどれも、適切にキャストされた NULL です。各セットは、ロング整数ビットマスクの配列を含む構造体です。配列のサイズは、FD_SETSIZE (select.h で定義されている) で設定します。配列には、各 FD_SETSIZE ファイル記述子のための 1 ビットを保持するだけの長さがあります。
マクロ FD_SET(fd, &mask) はセット mask 内のファイル記述子 fd を追加し、マクロ FD_CLR(fd, &mask) はこの記述子を削除します。セット mask は、使用前にゼロにする必要があります。マクロ FD_ZERO(&mask) は、セット mask を消去します。
select(3C) の 5 つ目の引数によって、タイムアウト値を指定できます。timeout ポインタが NULL の場合、select(3C) は、記述子が選択できるようになるか、あるいはシグナルが受信されるまでブロックします。timeout 内のフィールドが 0 に設定されると、select(3C) はただちにポーリングして戻ります。
select(3C) は、通常、選択されたファイル記述子の数を返します。select(3C) は、タイムアウトの期限が過ぎると 0 を返します。select(3C) は、ファイル記述子マスクが変更されず、エラーまたは割り込みが発生した場合、そこで指定されたエラー番号 errno に対し -1 を返します。成功した場合に返される 3 つのセットは読み取り可能なファイル記述子、書き込み可能なファイル記述子、または例外条件が保留されたファイル記述子を示します。
FD_ISSET(fd, &mask) マクロを使用して、選択マスク内のファイル記述子のステータスをテストしてください。セット mask 内に fd が存在する場合はゼロ以外の値が返され、存在しない場合は 0 が返されます。読み取りセットで FD_ISSET(fd, &mask) マクロの前に select(3C) を使用して、待ち行列に入っている、ソケット上の接続要求を確認します。
例 2-5 は、accept(3SOCKET) 呼び出しによって新しい接続をピックアップするタイミングを決定するために、読み取り用の「リスニング (待機) 」ソケットで select を使用する方法を示しています。このプログラムは、接続要求を受け入れ、データを読み取り、単一のソケットで切断します。
#include <sys/types.h> #include <sys/socket.h> #include <sys/time.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #define TRUE 1 /* * このプログラムは、accept を呼び出す前に、select を使用 * して、他のだれかが接続を試みていないかチェックします。 */ main() { int sock, length; struct sockaddr_in6 server; int msgsock; char buf[1024]; int rval; fd_set ready; struct timeval to; /* ソケットを開き、そのソケットを先の例と同様にバインドする。*/ /* 接続の受け入れを開始する。*/ listen(sock, 5); do { FD_ZERO(&ready); FD_SET(sock, &ready); to.tv_sec = 5; to.tv_usec = 0; if (select(sock + 1, &ready, (fd_set *)0, (fd_set *)0, &to) == -1) { perror("select"); continue; } if (FD_ISSET(sock, &ready)) { msgsock = accept(sock, (struct sockaddr *)0, (int *)0); if (msgsock == -1) perror("accept"); else do { memset(buf, 0, sizeof buf); if ((rval = read(msgsock, buf, 1024)) == -1) perror("reading stream message"); else if (rval == 0) printf("Ending connection¥n"); else printf("-->%s¥n", buf); } while (rval > 0); close(msgsock); } else printf("Do something else¥n"); } while (TRUE); exit(0); }
以前のバージョンの select(3C) ルーチンでは、引数は fd_sets のポインタではなく、整数のポインタでした。ファイル記述子の数が整数内のビット数よりも小さい場合は、現在でもこのような呼び出しを使用できます。
select(3C) は、同期多重スキーマを提供します。SIGIO と SIGURG シグナル (「拡張機能」を参照) は、出力の完了、入力の有効性、および例外条件の非同期通知を提供します。