ネットワークインタフェース

第 2 章 ソケットインタフェース

この章では、ソケットインタフェースについて、プログラム例を示して具体的に説明します。

ソケットはマルチスレッドに対して安全

この章で説明するインタフェースは、マルチスレッドに対して安全です。ソケット関数の呼び出しを含むアプリケーションは、マルチスレッド対応のアプリケーションで自由に使用できます。しかし、アプリケーションに有効な多重度は指定されていません。

SunOS 4 のバイナリ互換性

SunOS 4 以降の主な変更は、SunOS 5 リリースにも継承しています。パッケージにバイナリ互換性があるため、動的にリンクされた SunOS 4 ベースのソケットアプリケーションは SunOS 5 でも実行できます。

  1. コンパイル行で、ソケットライブラリ (-lsocket または libsocket) を明示的に指定する必要があります。

  2. 場合によっては libnsl もリンクする必要があります (-lnsl -lsocket ではなく -lsocket -lnsl と指定する)。

  3. SunOS 5 で実行するには、ソケットライブラリを使用して SunOS 4 のソケットベースアプリケーションをすべてコンパイルし直す必要があります。

ソケットの概要

ソケットは、ネットワークプロトコルに対してもっとも一般的に使用される低レベルインタフェースです。ソケットは、1981 年以来 SunOS リリースに不可欠な部分となっています。ソケットは通信の終端であり、名前をバインドできます。ソケットにはタイプがあり、関連プロセスが 1 つ存在します。ソケットは、次のようなプロセス間通信のためのクライアントサーバーモデルを実装するために設計されました。

ソケットは、UNIX ファイルのように動作し、ネットワークプロトコルが使用できるように処置します。アプリケーションは、必要に応じてソケットを作成します。ソケットは、close(2)read(2)write(2)ioctl(2)、および fcntl(2) インタフェースと連携して動作します。オペレーティングシステムは、ファイルのファイル記述子とソケットのファイル記述子を区別します。

ソケットライブラリ

ソケットインタフェースルーチンは、アプリケーションとリンクが必要なライブラリ内に存在します。ライブラリ libsocket.so は、ほかのシステムサービスライブラリと共に /usr/lib に入っています。libsocket.so は、動的リンクに使用されます。

ソケットタイプ

ソケットタイプは、ユーザーにもわかる通信プロパティを定義します。インターネットファミリソケットは、TCP/IP トランスポートプロトコルへのアクセスを提供します。インターネットファミリは、IPv6 と IPv4 の両方で通信できるソケットの場合、AF_INET6 という値で識別されます。また、古いアプリケーションとのソース互換、および IPv4 に対する raw (生の) アクセスを目的とした値 AF_INET もサポートされています。

サポートされる 3 つのタイプのソケットを次に示します。

  1. ストリームソケットは、プロセスが TCP を使用して通信を行えるようにします。ストリームソケットは、信頼性の高い、順序付けされた、重複のない双方向データフローをレコード境界なしで提供します。接続が確立されたあと、これらのソケットからのデータの読み取り、およびこれらのソケットに対するデータの書き込みがバイトストリームとして行えます。ソケットタイプは SOCK_STREAM です。

  2. データグラムソケットは、プロセスが UDP を使用して通信を行えるようにします。データグラムソケットは、メッセージの双方向フローをサポートします。データグラムソケット側のプロセスは、送信シーケンスから順序を変えてメッセージを受信でき、重複したメッセージを受信できます。データ内のレコード境界は保持されます。ソケットタイプは SOCK_DGRAM です。

  3. raw ソケットは、ICMP へのアクセスを提供します。このタイプのソケットは、通常、データグラム型ですが、実際の特性はプロトコルが提供するインタフェースに依存します。raw ソケットは、ほとんどのアプリケーションには使用されません。このタイプは、新しい通信プロトコルの開発をサポートしたり、既存プロトコルの難解な機能にアクセスしたりするために提供されています。raw ソケットを使用できるのは、スーパーユーザープロセスだけです。ソケットタイプは SOCK_RAW です。

詳細については、「特定のプロトコルの選択」を参照してください。

インタフェースセット

SunOS 5.8 には、2 セットのソケットインタフェース、BSD ソケットインタフェースと XNS 5 (Unix98) ソケットインタフェースが付属しています (XNS 5 ソケットは SunOS 5.7 以降)。XNS 5 インタフェースは、BSD インタフェースとわずかに異なります。

XNS 5 ソケットインタフェースについては、次のマニュアルページで説明されています。accept(3XNET)bind(3XNET)connect(3XNET)endhostent(3XNET)endnetent(3XNET)endprotoent(3XNET)endservent(3XNET)gethostbyaddr(3XNET)gethostbyname(3XNET)gethostent(3XNET)gethostname(3XNET)getnetbyaddr(3XNET)getnetbyname(3XNET)getnetent(3XNET)getpeername(3XNET)getprotobyname(3XNET)getprotobynumber(3XNET)getprotoent(3XNET)getservbyname(3XNET)getservbyport(3XNET)getservent(3XNET)getsockname(3XNET)getsockopt(3XNET)htonl(3XNET)htons(3XNET)inet_addr(3XNET)inet_lnaof(3XNET)inet_makeaddr(3XNET)inet_netof(3XNET)inet_network(3XNET)inet_ntoa(3XNET)listen(3XNET)ntohl(3XNET)ntohs(3XNET)recv(3XNET)recvfrom(3XNET)recvmsg(3XNET)send(3XNET)sendmsg(3XNET)sendto(3XNET)sethostent(3XNET)setnetent(3XNET)setprotoent(3XNET)setservent(3XNET)setsockopt(3XNET)shutdown(3XNET)socket(3XNET)、および socketpair(3XNET)

従来の SunOS 5 BSD ソケットの動作については、対応する 3N のマニュアルページに説明されています。次のセクション 3N マニュアルページには、多くの新しいインタフェースが追加されました。freeaddrinfo(3SOCKET)freehostent(3SOCKET)getaddrinfo(3SOCKET)getipnodebyaddr(3SOCKET)getipnodebyname(3SOCKET)getnameinfo(3SOCKET)inet_ntop(3SOCKET)、および inet_pton(3SOCKET)。XNS 5 (Unix98) ソケットインタフェースを使用するアプリケーションの構築については、standards(5) のマニュアルページを参照してください。

ソケットの基本的な使用

この節では、基本的なソケットインタフェースの使用について説明します。

ソケットの作成

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_STREAMSOCK_DGRAM、または SOCK_RAW) は、AF_INET6AF_INET、および AF_UNIX でサポートされます。次の文は、インターネットファミリでストリームソケットを作成します。

s = socket(AF_INET6, SOCK_STREAM, 0);

この呼び出しの結果、基本的な通信を提供する TCP プロトコルを使用したストリームソケットが作成されます。通常は、デフォルトプロトコル (引数 protocol0) を使用してください。「拡張機能」で説明しているように、デフォルト以外のプロトコルも指定できます。

ローカル名のバインド

ソケットは、名前のない状態で作成されます。ソケットにアドレスがバインドされるまでは、遠隔プロセスはソケットを参照できません。通信プロセスは、アドレスを介して接続されます。インターネットファミリでは、接続はローカルアドレス、リモートアドレス、ローカルポート、およびリモートポートから構成されます。protocollocal addresslocal portforeign addressforeign port のような重複した順序セットは指定できません。

bind(3SOCKET) 呼び出しを使用すると、プロセスはソケットのローカルアドレスを指定できます。これは、local addresslocal 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 ファミリでは、fromstruct 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 で定義されている以下のフラグをゼロ以外の値として指定できます。

MSG_OOB

帯域外データを送受信する

MSG_PEEK

読み取らずにデータの確認だけを行う

MSG_DONTROUTE

パケットの経路を指定せずにデータを送信する

帯域外データは、ストリームソケットに固有のものです。recv(3SOCKET) 呼び出しで MSG_PEEK が指定される場合、存在するすべてのデータがユーザーに返されますが、データは読み取られていないものとして扱われます。ソケット上の次の read(2) または recv(3SOCKET) 呼び出しは、同じデータを返します。発信パケットに適用されるパケット経路を指定しないデータ送信オプションは、現在、ルーティングテーブルの管理プロセスにだけ使用されており、一般にはほとんど使用されません。

ソケットを閉じる

SOCK_STREAM ソケットは、close(2) 関数呼び出しで破棄できます。close(2) のあとで信頼できる配信が見込まれるソケットにデータが待ち行列化されている場合、プロトコルは継続してそのデータの転送を試みます。期限が来てもデータが配信されない場合は、データが破棄されます。

shutdown(3SOCKET) は、SOCK_STREAM ソケットを正常に閉じます。両方のプロセスで送信が行われなくなっていることを認識できます。この呼び出しの書式は次のとおりです。

shutdown(s, how);

how は次のように定義されています。

それ以上の受信を許可しない 

それ以上の送信を許可しない 

それ以上の受信と送信を許可しない 

ストリームソケットの接続

図 2-1 と次の 2 つの例は、インターネットファミリのストリーム接続の開始と受け入れを示しています。

図 2-1 ストリームソケットを使用したコネクション型の通信

Graphic

例 2-1 はサーバー側のプログラムです。このプログラムは、ソケットを作成してそのソケットに名前をバインドし、続いてポート番号を表示します。このプログラムは listen(3SOCKET) を呼び出して、ソケットが接続要求を受け入れる用意ができていることをマークし、要求の待ち行列を初期化します。プログラムの残り部分は無限ループです。ループの各パスは、新しいソケットを作成することによって新しい接続を受け入れ、待ち行列からその接続を削除します。サーバーは、ソケットからのメッセージを読み取って表示し、メッセージを閉じます。in6addr_any の使用については、「アドレスのバインド」で説明しています。


例 2-1 インターネットストリーム接続の受け入れ (サーバー)

#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) のマニュアルページを参照してください。


例 2-2 インターネットファミリのストリーム接続 (クライアント)

#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) の手順は省略できます。


図 2-2 データグラムソケットを使用したコネクションレス型の通信

Graphic

データグラムソケットは、「ソケットの作成」で説明しているように作成されます。特定のローカルアドレスが必要な場合、bind(3SOCKET) オペレーションが最初のデータ伝送よりも先行しなければなりません。それ以外の場合、データが最初に送信される際にシステムがローカルアドレスまたはポート (あるいはこの両方) を設定します。データの送信には、sendto(3SOCKET) を使用します。

sendto(s, buf, buflen, flags, (struct sockaddr *) &to, tolen);

sbufbuflen、および flags パラメータは、コネクション型のソケットの場合と同じです。totolen の値は、意図するメッセージ受信者のアドレスを示します。ローカルにエラー条件 (到達できないネットワークなど) が検出されると、-1 が戻り、errno がエラー番号に設定されます。

データグラムソケット上のメッセージを受信するには、recvfrom(3SOCKET) を使用します。

recvfrom(s, buf, buflen, flags, (struct sockaddr *) &from, &fromlen);

呼び出しの前に、fromlenfrom バッファーのサイズに設定されます。fromlen は、戻り時には、データグラムの配信元であるアドレスのサイズに設定されます。

データグラムソケットは、ソケットを特定の宛先アドレスと関連付けるために connect(3SOCKET) 呼び出しを使用することもできます。続いて、send(3SOCKET) 呼び出しを使用できます。宛先アドレスの明示的な指定がない状態でソケット上で送信されるデータはすべて、接続されたピアにアドレス指定され、そのピアから受信されるデータだけが配信されます。1 つのソケットで一度に許可されるのは、接続された 1 つのアドレスだけです。2 つ目の connect(3SOCKET) 呼び出しは、宛先アドレスを変更します。データグラムソケット上の接続要求は、ただちに返されます。システムは、ピアのアドレスを記録します。accept(3SOCKET)listen(3SOCKET) は、データグラムソケットでは使用されません。

データグラムソケットが接続されている間、前の send(3SOCKET) 呼び出しからのエラーは非同期に返すことができます。これらのエラーは、そのソケットの後続のオペレーションで報告できます。また、getsockopt(3SOCKET) のオプションである SO_ERROR を使用してそのエラーステータスを問い合わせることもできます。

例 2-3 は、ソケットの作成、ソケットへの名前のバインド、およびソケットへのメッセージ送信によってインターネット呼び出しを送信する方法を示しています。


例 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 は、ソケットの作成、ソケットへの名前のバインド、およびソケットからの読み取りによってインターネット呼び出しを読み取る方法を示しています。


例 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 を使用する方法を示しています。このプログラムは、接続要求を受け入れ、データを読み取り、単一のソケットで切断します。


例 2-5 select(3C) を使用して保留状態の接続を確認する

#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) は、同期多重スキーマを提供します。SIGIOSIGURG シグナル (「拡張機能」を参照) は、出力の完了、入力の有効性、および例外条件の非同期通知を提供します。

標準のルーチン

ネットワークアドレスを見つけ、構成しなければならない場合があります。この節では、ネットワークアドレスを操作するルーチンについて説明します。特に明記しない限り、この節に示す関数はインターネットファミリにだけ適用されます。

リモートホスト上のサービスを見つけるには、クライアントとサーバーが通信を行う前にさまざまなレベルの割り当てを行う必要があります。サービスには、人間が使用するための名前が付いています。サービス名とホスト名は、ネットワークアドレスに変換しなければなりません。最後に、そのアドレスを使用してホストを見つけ、ホストへの経路を定めます。割り当ての細部は、ネットワークアーキテクチャによって異なります。望ましいのは、ホストに名前が付いていることをネットワークが要求せず、ホストの物理位置の同一性を保護できることです。ホストのアドレスが指定されていると、ホストの位置をより柔軟に見つけることができます。

標準ルーチンは、ホスト名をネットワークアドレスに、ネットワーク名をネットワーク番号に、プロトコル名をプロトコル番号に、サービス名をポート番号に、適切なプロトコルをサーバープロセスとの通信における使用にそれぞれ割り当てます。標準ルーチンのどれかを使用する場合は、ファイル netdb.h を含めなければなりません。

ホスト名とサービス名

インタフェース getaddrinfo(3SOCKET)getnameinfo(3SOCKET)、および freeaddrinfo(3SOCKET) を使用すると、ホスト上のサービスの名前とアドレスを簡単に変換できます。IPv6 の場合、getipnodebyname(3SOCKET)getservbyname(3SOCKET) を呼び出してアドレスの結合方法を決定する代わりに、これらのインタフェースを使用できます。同様に IPv4 では、これらのインタフェースを gethostbyname(3NSL)getservbyname(3SOCKET) の代わりに使用できます。IPv6 アドレスと IPv4 アドレスは、どちらも透過的に処理されます。

getaddrinfo(3SOCKET) は、指定されたホストの結合アドレスとポート番号、およびサービス名を返します。getaddrinfo(3SOCKET) が返す情報はすべて動的に割り当てられるため、メモリーリークが防止されるように freeaddrinfo(3SOCKET) を使用して解放しなければなりません。getnameinfo(3SOCKET) は、指定されたアドレスとポート番号に関連付けられたホスト名とサービス名を返します。getaddrinfo(3SOCKET)getnameinfo(3SOCKET) が返す EAI_xxx コードに基づくエラーメッセージを出力するには、gai_strerror(3SOCKET) を呼び出します。

getaddrinfo(3SOCKET) の使用例を次に示します。

    struct addrinfo         *res, *aip;
    struct addrinfo         hints;
    int                     sock = -1;
    int                     error;

    /* ホストアドレスを取得する。アドレスのタイプは問わない。*/
    bzero(&hints, sizeof (hints));
    hints.ai_flags = AI_ALL|AI_ADDRCONFIG;
    hints.ai_socktype = SOCK_STREAM;

    error = getaddrinfo(hostname, servicename, &hints, &res);
    if (error != 0) {
        (void) fprintf(stderr, "getaddrinfo: %s for host %s service %s¥n",
                 gai_strerror(error), hostname, servicename);
        return (-1);
    }
 

res が指す構造体の getaddrinfo(3SOCKET) が返す情報を処理したあと、次の文によって記憶領域を解放する必要があります。

        freeaddrinfo(res);

次の例に示すように、getnameinfo(3SOCKET) はエラー原因を調べるのに特に便利です。

    struct sockaddr_storage faddr;
    int                     sock, new_sock;
    socklen_t               faddrlen;
    int                     error;
    char                    hname[NI_MAXHOST];
    char                    sname[NI_MAXSERV];

     ...
         faddrlen = sizeof (faddr);
         new_sock = accept(sock, (struct sockaddr *)&faddr, &faddrlen);
         if (new_sock == -1) {
             if (errno != EINTR && errno != ECONNABORTED) {
                 perror("accept");
             }
             continue;
         }        
         error = getnameinfo((struct sockaddr *)&faddr, faddrlen, hname, 
                     sizeof (hname), sname, sizeof (sname), 0);
         if (error) {
             (void) fprintf(stderr, "getnameinfo: %s¥n",
                         gai_strerror(error));
         } else {
             (void) printf("Connection from %s/%s¥n", hname, sname);
         }

hostent - ホスト名

インターネットホスト名からアドレスへの割り当ては、次のように hostent 構造体で示されます。

struct hostent {
	   char  *h_name;            /* ホストの正式名称 */
	   char  **h_aliases;        /* 別名リスト */
	   int   h_addrtype;         /* ホストアドレスのタイプ (AF_INET6 など) */
	   int   h_length;           /* アドレスの長さ */
	   char  **h_addr_list;      /* NULL で終わるアドレスのリスト */
};
/* 最初のアドレス、ネットワークバイトオーダー */
#define h_addr h_addr_list[0]

getipnodebyname(3SOCKET) は、インターネットホスト名を hostent 構造体に割り当てます。getipnodebyaddr(3SOCKET) は、インターネットホストアドレスを hostent 構造体に割り当てます。freehostent(3SOCKET) は、hostent 構造体のメモリーを解放します。inet_ntop(3SOCKET) は、インターネットホストアドレスを表示可能な文字列に割り当てます。

このルーチンは、ホストの名前、その別名、アドレスタイプ (アドレスファミリ)、および NULL で終わる可変長アドレスのリストを含む hostent 構造体を返します。このアドレスリストが必要なのは、ホストが多くのアドレスを持つことができるためです。h_addr 定義は下位互換性のためであり、この定義は hostent 構造体のアドレスリストの最初のアドレスです。

netent - ネットワーク名

次に、ネットワーク名を番号に割り当て、netent 構造体を戻すルーチンを示します。

/*
 * ネットワーク番号が 32 ビットに収まると想定します。
 */
struct netent {
   char     *n_name;      /* ネットの正式名称 */
   char     **n_aliases;  /* 別名リスト */
   int      n_addrtype;   /* ネットアドレスのタイプ */
   int      n_net;        /* ネット番号、ホストバイトオーダー */
};

getnetbyname(3SOCKET)getnetbyaddr_r(3SOCKET)、および getnetent(3SOCKET) は、上記のホストルーチンに対するネットワーク側のルーチンです。

protoent - プロトコル名

protoent 構造体は、getprotobyname(3SOCKET)getprotobynumber(3SOCKET)、および getprotoent(3SOCKET) で使用されるプロトコル名マッピングを定義します。

struct protoent {
   char     *p_name;          /* プロトコルの正式名称 */
   char     **p_aliases       /* 別名リスト */
   int      p_proto;          /* プロトコル番号 */
};

servent - サービス名

インターネットファミリサービスは、特定の既知のポートに常駐し、特定のプロトコルを使用します。サービス名からポート番号へのマッピングは、servent 構造体で表現されます。

struct servent {
   char     *s_name;         /* サービスの正式名称 */
   char     **s_aliases;     /* 別名リスト */
   int      s_port;          /* ポート番号、ネットワークバイトオーダー */
   char     *s_proto;        /* 使用するプロトコル */
};
getservbyname(3SOCKET) は、サービス名と修飾プロトコルを servent 構造体に割り当てます (修飾プロトコルは省略可能)。次の呼び出しは、任意のプロトコルを使用する Telnet サーバーのサービス仕様を返します。
sp = getservbyname("telnet", (char *) 0);
次の呼び出しは、TCP プロトコルを使用する Telnet サーバーを返します。
sp = getservbyname("telnet", "tcp");

getservbyport(3SOCKET)getservent(3SOCKET) も提供されます。getservbyport(3SOCKET) には、getservbyname(3SOCKET) のインタフェースに似たインタフェースがあります (ルックアップを修飾するためオプションのプロトコル名を指定可能)。

その他のルーチン

名前とアドレスの操作を簡易化するルーチンには、アドレスに関連するデータベースルーチンのほかにもいくつかあります。表 2-3 は、可変長のバイト列、およびバイトスワッピングのネットワークアドレスと値を要約したものです。

表 2-3 実行時ライブラリルーチン

呼び出し 

機能説明 

memcmp(3C)

バイト列を比較する。同じ場合は 0、異なる場合は 0 以外の値を返す

memcpy(3C)

s2n バイトを s1 にコピーする

memset(3C)

base の最初の n バイトの領域に値 value を割り当てる

htonl(3SOCKET)

ホストからネットワークバイトオーダーへの 32 ビット量の変換 

htons(3SOCKET)

ホストからネットワークバイトオーダーへの 16 ビット量の変換 

ntohl(3SOCKET)

ネットワークからホストバイトオーダーへの 32 ビット量の変換 

ntohs(3SOCKET)

ネットワークからホストバイトオーダーへの 16 ビット量の変換 

バイトスワッピングルーチンが提供されているのは、アドレスはネットワークオーダーで供給されるとオペレーティングシステムが考えるためです。一部のアーキテクチャでは、ホストバイトオーダーがネットワークバイトオーダーと異なるため、プログラムは必要に応じて値のバイトスワップを行わなければなりません。ネットワークアドレスを返すルーチンは、ネットワークオーダーで返します。バイトスワッピング問題が発生するのは、ネットワークアドレスを解釈する場合だけです。たとえば、次のコードは TCP ポートまたは UDP ポートをフォーマットします。

printf("port number %d¥n", ntohs(sp->s_port));

これらのルーチンを必要としないマシンでは、アドレスは NULL マクロとして定義されます。

クライアントサーバープログラム

もっとも一般的な分散型アプリケーションは、クライアントサーバーモデルです。この方式では、クライアントプロセスはサーバープロセスからのサービスを要求します。

代替方式として、休止しているサーバープロセスを削除できるサービスサーバーがあります。たとえば、インターネットサービスデーモン inetd(1M) は、構成ファイルを読み取ることにより、起動時に決定されるさまざまなポートで待機します。inetd(1M) のサービスを受けるポートで接続が要求されると、inetd(1M) はクライアントにサービスを行うために適切なサーバーを生成します。クライアントは、その接続で中間媒体が何らかの役割を果たしたことを認識しません。inetd(1M) については、inetd(1M) デーモン」で詳しく説明しています。

サーバー

ほとんどのサーバーには、既知のインターネットポート番号または UNIX ファミリ名でアクセスします。例 2-6 は、リモートログインサーバーのメインループを示しています。


例 2-6 リモートログインサーバー

main(argc, argv)
   int argc;
   char *argv[];
{
   int f;
   struct sockaddr_in6 from;
   struct sockaddr_in6 sin;
   struct servent *sp;
 
   sp = getservbyname("login", "tcp");
 
   if (sp == (struct servent *) NULL) {
      fprintf(stderr, "rlogind: tcp/login: unknown service");
      exit(1);
   }
   ...
   #ifndef DEBUG
   /* サーバーを制御端末から分離する。*/
   ...
   #endif
   	sin.sin6_port = sp->s_port;	/* 限られたポート */
      sin.sin6_addr.s6_addr = in6addr_any;
      ...
      f = socket(AF_INET6, SOCK_STREAM, 0);
      ...
      if (bind( f, (struct sockaddr *) &sin, sizeof sin ) == -1) {
      ...
      }
      ...
      listen(f, 5);
      while (TRUE) {
         int g, len = sizeof from;
         g = accept(f, (struct sockaddr *) &from, &len);
         if (g == -1) {
            if (errno != EINTR)
               syslog(LOG_ERR, "rlogind: accept: %m");
            continue;
         }
         if (fork() == 0) {
            close(f);
            doit(g, &from);
         }
         close(g);
      }
      exit(0);
}

例 2-7 は、サーバーがそのサービス定義を取得する方法を示しています。


例 2-7 リモートログインサーバー : 手順 1

sp = getservbyname("login", "tcp");
if (sp == (struct servent *) NULL) {
		fprintf(stderr, "rlogind: tcp/login: unknown service¥n");
		exit(1);
}

getservbyname(3SOCKET) の結果は、プログラムがサービス要求を待機するインターネットポートを定義するためにあとで使用されます。標準のポート番号の一部は、/usr/include/netinet/in.h に入っています。

例 2-8 は、オペレーションの非 DEBUG モードで、サーバーがその呼び出し側の制御端末との関連付けを解除する方法を示しています。


例 2-8 制御端末との関連付けを解除する

   (void) close(0);
   (void) close(1);
   (void) close(2);
   (void) open("/", O_RDONLY);
   (void) dup2(0, 1);
   (void) dup2(0, 2);
   setsid();

このコードは、サーバーが制御端末のプロセスグループからのシグナルを受信することを防ぎます。関連付けを解除したあと、サーバーはエラーレポートを端末に送信できないため、syslog(3C) を使用してエラーをログに記録する必要があります。

サーバーは次にソケットを作成し、サービス要求を待機します。bind(3SOCKET) が使用されると、サーバーは指定された位置で待機します (限られたポート番号で待機するリモートログインサーバーはスーパーユーザーとして動作する)。

例 2-9 は、ループの本体を示しています。


例 2-9 リモートログインサーバー : 本体

while(TRUE) {
		int g, len = sizeof(from);
		if (g = accept(f, (struct sockaddr *) &from, &len) == -1) {
			if (errno != EINTR)
				syslog(LOG_ERR, "rlogind: accept: %m");
			continue;
		}
		if (fork() == 0) {		/* 子 */
			close(f);
			doit(g, &from);
		}
		close(g);					/* 親 */
}

accept(3SOCKET) は、クライアントがサービスを要求するまでメッセージをブロックします。accept(3SOCKET) は、SIGCHLD のようなシグナルで割り込みを受ける場合、失敗を示すメッセージを返します。accept(3SOCKET) からの戻り値がチェックされ、エラーが発生している場合は syslog(3C) によってエラーが記録されます。

続いてサーバーは子プロセスの fork(2) を行い、リモートログインプロトコル処理の本体を呼び出します。接続要求を待ち行列に入れるために親プロセスが使用するソケットは、子プロセスで閉じられます。accept(3SOCKET) が作成するソケットは、親プロセスで閉じられます。クライアントのアドレスは、クライアントの認証を行うためにサーバーアプリケーションの doit() ルーチンに渡されます。このルーチンは、クライアントと共に実際のアプリケーションプロトコルを実行します。

クライアント

この節では、クライアントリモートログインプロセスで行われる処理について説明します。サーバー側と同様に、まずリモートログインのサービス定義の位置を確認します。

sp = getservbyname("login", "tcp");
if (sp == (struct servent *) NULL) {
		fprintf(stderr,"rlogin: tcp/login: unknown service");
		exit(1);
}

次に、getipnodebyname(3SOCKET) 呼び出しで宛先ホストを調べます。

hp = getipnodebyname (AF_INET6, argv[1], AI_DEFAULT, &errnum);
if (hp == (struct hostent *) NULL) {
		fprintf(stderr, "rlogin: %s: unknown host", argv[1]);
		exit(2);
}

次に、要求されたホストでサーバーに接続し、リモートログインプロトコルを開始します。アドレスバッファーはクリアされ、外部ホストのインターネットアドレスと、ログインサーバーが待機するポート番号が書き込まれます。

memset((char *) &server, 0, sizeof server);
bzero (&sin6, sizeof (sin6));
memcpy((char*) &server.sin6_addr,hp->h_addr,hp->h_length);
server.sin6_family = hp->h_addrtype;
server.sin6_port = sp->s_port;

ソケットが作成され、接続が開始されます。s のバインドが解除されるため、connect(3SOCKET) は暗黙に bind(3SOCKET) を実行します。

s = socket(hp->h_addrtype, SOCK_STREAM, 0);
if (s < 0) {
		perror("rlogin: socket");
		exit(3);
}
 ...
if (connect(s, (struct sockaddr *) &server, sizeof server) < 0) {
		perror("rlogin: connect");
		exit(4);
}

コネクションレス型のサーバー

サービスの中にはデータグラムソケットを使用するものがあります。rwho(1) サービスは、LAN に接続されたホストのステータス情報を提供します (in.rwhod(1M) は大量のネットワークトラフィックを発生させるため、実行を避けてください)。rwho(1) サービスを利用するには、特定のネットワークに接続されたすべてのホストに情報をブロードキャスト送信できる環境が必要です。これは、データグラムソケットを使用する例の 1 つです。

rwho(1) サーバーを実行するホスト上のユーザーは、ruptime(1) を使用して別のホストの現在のステータスを取得できます。典型的な出力を例 2-10 に示します。


例 2-10 ruptime(1) プログラムの出力

itchy up 9:45, 5 users, load 1.15, 1.39, 1.31
scratchy up 2+12:04, 8 users, load 4.67, 5.13, 4.59
click up 10:10, 0 users, load 0.27, 0.15, 0.14
clack up 2+06:28, 9 users, load 1.04, 1.20, 1.65
ezekiel up 25+09:48, 0 users, load 1.49, 1.43, 1.41
dandy 5+00:05, 0 users, load 1.51, 1.54, 1.56
peninsula down 0:24
wood down 17:04
carpediem down 16:09
chances up 2+15:57, 3 users, load 1.52, 1.81, 1.86

各ホストには、rwho(1) サーバープロセスによってステータス情報が周期的にブロードキャスト送信されます。このサーバープロセスもステータス情報を受信し、データベースを更新します。このデータベースは、各ホストのステータスのために解釈されます。サーバーはそれぞれ個別に動作し、ローカルネットワークとそのブロードキャスト機能によってのみ結合されます。

大量のネットトラフィックが生成されるため、ブロードキャストの使用はかなり困難です。サービスが広範に、かつ頻繁に使用されない限り、周期的なブロードキャストのために手間がかかり簡潔さが失われることになります。

例 2-11 は、rwho(1) サーバーの簡潔な例を示しています。このコードは、ネットワーク上のほかのホストによるステータス情報ブロードキャストを受信し、そのホストのステータスを供給するという 2 つのタスクを行います。最初のタスクは、プログラムのメインループで行われます。別の rwho(1) サーバープロセスによって送信されたことを確認するために rwho(1) ポートで受信されたパケットがチェックされ、到着時刻が記録されます。パケットは、続いてホストのステータスを使用してファイルを更新します。一定の時間内にホストからの通信がない場合、データベースルーチンはホストがダウンしていると想定し、そのことを記録します。ホストが稼動している間サーバーがダウンしていることがあるため、このアプリケーションはエラーになりがちです。


例 2-11 rwho(1) サーバー

main()
{
   ...
   sp = getservbyname("who", "udp");
   net = getnetbyname("localnet");
   sin.sin6_addr = inet_makeaddr(net->n_net, in6addr_any);
   sin.sin6_port = sp->s_port;
   ...
   s = socket(AF_INET6, SOCK_DGRAM, 0);
   ...
   on = 1;
   if (setsockopt(s, SOL_SOCKET, SO_BROADCAST, &on, sizeof on)
         == -1) {
      syslog(LOG_ERR, "setsockopt SO_BROADCAST: %m");
      exit(1);
   }
   bind(s, (struct sockaddr *) &sin, sizeof sin);
   ...
   signal(SIGALRM, onalrm);
   onalrm();
   while(1) {
      struct whod wd;
	      int cc, whod, len = sizeof from;
      cc = recvfrom(s, (char *) &wd, sizeof(struct whod), 0,
         (struct sockaddr *) &from, &len);
      if (cc <= 0) {
      if (cc == -1 && errno != EINTR)
         syslog(LOG_ERR, "rwhod: recv: %m");
      continue;
      }
      if (from.sin6_port != sp->s_port) {
         syslog(LOG_ERR, "rwhod: %d: bad from port",
            ntohs(from.sin6_port));
         continue;
      }
      ...
      if (!verify( wd.wd_hostname)) {
         syslog(LOG_ERR, "rwhod: bad host name from %x",
            ntohl(from.sin6_addr.s6_addr));
         continue;
      }
      (void) sprintf(path, "%s/whod.%s", RWHODIR, wd.wd_hostname);
      whod = open(path, O_WRONLY|O_CREAT|O_TRUNC, 0666);
      ...
      (void) time(&wd.wd_recvtime);
      (void) write(whod, (char *) &wd, cc);
      (void) close(whod);
   }
   exit(0);
}

2 つ目のサーバータスクは、そのホストのステータスの供給です。このタスクでは、周期的にシステムステータス情報を取得し、その情報をメッセージとしてパッケージ化し、ほかの rwho(1) サーバーに知らせるためローカルネットワーク上でそれをブロードキャスト送信する必要があります。このタスクはタイマーで実行され、シグナルによって起動されます。システムステータス情報の位置確認が伴いますが、重要なものではありません。

ステータス情報が、ローカルネットワーク上でブロードキャスト送信されます。ブロードキャストをサポートしないネットワークでは、別の方式を使用してください。

拡張機能

分散型アプリケーションを構築する場合、通常は、これまでに説明したメカニズムで十分対応できます。この節に挙げた機能は必要に応じて参照してください。

帯域外データ

ストリームソケットの抽象化には、帯域外データが含まれます。帯域外データは、接続されたストリームソケットペア間の論理的に独立した伝送チャネルです。帯域外データは、通常のデータとは無関係に配信されます。帯域外データ機能が使用される場合、一度に 1 つ以上の帯域外メッセージが確実に配信されなければなりません。このメッセージには 1 バイト以上のデータを含むことができ、いつでも 1 つ以上のメッセージの配信を保留できます。

帯域内シグナリングだけをサポートする (つまり、緊急のデータが通常のデータと共に順序どおり配信される) 通信プロトコルでは、メッセージは通常のデータストリームから抽出され、個別に格納されます。これにより、ユーザーは間に入るデータをバッファリングする必要にせまられることなく、緊急データを順番に受信するか順不同で受信するかを選択できます。

帯域外データは、MSG_PEEK を使用して先読みできます。ソケットにプロセスグループがある場合は、その存在がプロトコルに通知される時に SIGURG シグナルが生成されます。SIGIO に関する 「割り込み方式のソケット入出力」で説明しているように、プロセスは適切な fcntl(2) 呼び出しを使用して、プロセスグループまたはプロセス ID が SIGURG によって通知されるように設定できます。複数のソケットに配信待ちの帯域外データがある場合は、select(3C) 呼び出しを使用してそのようなデータ保留のあるソケットを確認できます。

帯域外データが送信された位置のデータストリームには、論理マークが置かれます。リモートログインアプリケーションとリモートシェルアプリケーションは、この機能を使用してクライアントプロセスとサーバープロセス間にシグナルを伝えます。シグナルが受信された時点で、データストリームのそのマークまでのデータがすべて破棄されます。

帯域外メッセージを送信するには、MSG_OOB フラグを send(3SOCKET) または sendto(3SOCKET) に適用します。帯域外メッセージを受信するには、順に取得する場合を除き MSG_OOBrecvfrom(3SOCKET) または recv(3SOCKET) に指定します (順に取得する場合は、MSG_OOB フラグは不要)。SIOCATMARK ioctl(2) は、読み取りポインタが現在、データストリーム内のマークを指しているかどうかを示します。

int yes;
ioctl(s, SIOCATMARK, &yes);

戻り時に yes1 の場合、次の読み取りはマークのあとのデータを返します。yes が 1 でない場合は、帯域外データが到着したと仮定して、次の読み取りは帯域外シグナルを送信する前にクライアントによって送信されたデータを提供します。割り込みまたは停止のシグナルを受ける場合に出力をフラッシュするリモートログインプロセス内のルーチンを、例 2-12 に示します。このコードは、マークまでの通常のデータを (これを破棄するために) 読み取り、続いて帯域外バイトを読み取ります。

プロセスは、初めにマークまでを読み取らずに、帯域外データの読み取りまたは先読みを行うことができます。これは、プロトコルが緊急の帯域内データを通常のデータと共に配信し、前もってその存在の通知だけを行う場合には困難になります (例: インターネットファミリ内のソケットストリームの提供に使用されるプロトコル、TCP)。このようなプロトコルでは、MSG_OOB フラグによって recv(3SOCKET) が行われた時点で帯域外バイトが到着していないことがあります。このような場合、呼び出しはエラー EWOULDBLOCK を返します。また、入力バッファー内に十分な帯域内データが存在し、バッファーが空になるまでは通常のフロー制御がピアによる緊急データの送信を防止する場合もあります。この場合プロセスは、待ち行列に入ったデータを十分に読み取ってからでないと緊急データを配信できません。


例 2-12 帯域外データの受信時における端末入出力のフラッシュ

#include <sys/ioctl.h>
#include <sys/file.h>
...
oob()
{
		int out = FWRITE;
		char waste[BUFSIZ];
		int mark = 0;
 
		/* ローカル端末出力をフラッシュする */
		ioctl(1, TIOCFLUSH, (char *) &out);
		while(1) {
			if (ioctl(rem, SIOCATMARK, &mark) == -1) {
				perror("ioctl");
				break;
			}
			if (mark)
				break;
			(void) read(rem, waste, sizeof waste);
		}
		if (recv(rem, &mark, 1, MSG_OOB) == -1) {
			perror("recv");
			...
		}
		...
}

ソケットストリーム内の緊急のインラインデータの位置を保持する機能もあります。この機能は、ソケットレベルのオプション SO_OOBINLINE として提供されています。使用方法については、getsockopt(3SOCKET) のマニュアルページを参照してください。このオプションを使用すると緊急データの位置 (マーク) は保持されますが、緊急データは MSG_OOB フラグなしで返される通常のデータストリーム内のマークの直後に続きます。複数の緊急指示を受信するとマークは移動しますが、帯域外データが消失することはありません。

非ブロックソケット

一部のアプリケーションは、ブロックしないソケットを必要とします。たとえば、すぐに完了できず (完了を待っている間に) プロセスを一時的に中断させてしまう要求は実行されません。エラーコードが返されます。ソケットが作成され、別のソケットに対する接続が確立されたあと、例 2-13 に示すように fcntl(2) 呼び出しを発行してこのソケットを非ブロックに設定できます。


例 2-13 非ブロックソケットの設定

#include <fcntl.h>
#include <sys/file.h>
...
int fileflags;
int s;
...
s = socket(AF_INET6, SOCK_STREAM, 0);
...
if (fileflags = fcntl(s, F_GETFL, 0) == -1)
		perror("fcntl F_GETFL");
		exit(1);
}
if (fcntl(s, F_SETFL, fileflags | FNDELAY) == -1)
		perror("fcntl F_SETFL, FNDELAY");
		exit(1);
}
...

非ブロックソケットで入出力を行う場合は、オペレーションが正常にブロックする場合に発生する、errno.h 内のエラー EWOULDBLOCK をチェックしてください。accept(3SOCKET)connect(3SOCKET)send(3SOCKET)recv(3SOCKET)read(2)、および write(2) はすべて、EWOULDBLOCK を返すことができます。send(3SOCKET) のようなオペレーションを完全には実行できないが、ストリームソケットを使用する場合などに部分的な書き込みは可能という場合には、すぐに送信できるデータは処理され、実際に送信された量が関数の戻り値となります。

非同期ソケット入出力

複数の要求を同時に処理するアプリケーションでは、プロセス間の非同期通信を必要とします。非同期ソケットは、SOCK_STREAM タイプでなければなりません。ソケットを非同期にするには、例 2-14 に示すように fcntl(2) 呼び出しを発行します。


例 2-14 ソケットを非同期にする

#include <fcntl.h>
#include <sys/file.h>
...
int fileflags;
int s;
...
s = socket(AF_INET6, SOCK_STREAM, 0);
...
if (fileflags = fcntl(s, F_GETFL ) == -1)
		perror("fcntl F_GETFL");
		exit(1);
}
if (fcntl(s, F_SETFL, fileflags | FNDELAY | FASYNC) == -1)
		perror("fcntl F_SETFL, FNDELAY | FASYNC");
		exit(1);
}
...

ソケットの初期化と接続が終わり、非同期に設定されると、ファイルを非同期で読み書きする場合のように通信が行われます。send(3SOCKET)write(2)recv(3SOCKET)read(2) は、データ転送を開始します。次の節で説明しているように、データ転送はシグナル方式の入出力ルーチンによって行われます。

割り込み方式のソケット入出力

SIGIO シグナルは、ソケット (実際には任意のファイル記述子) がデータ転送を終了した時点をプロセスに通知します。SIGIO を使用する手順は次のとおりです。

例 2-15 は、ソケットで要求が保留される場合にその保留要求に関する情報を特定のプロセスが受信する例を示しています。SIGURG のハンドラを追加すると、このコードは SIGURG シグナルを受信する目的でも使用できます。


例 2-15 入出力要求の非同期通知

#include <fcntl.h>
#include <sys/file.h>
 ...
signal(SIGIO, io_handler);
/* SIGIO または SIGURG シグナルを受信するプロセスを s に設定する。*/
if (fcntl(s, F_SETOWN, getpid()) < 0) {
		perror("fcntl F_SETOWN");
		exit(1);
}

シグナルとプロセスグループ ID

SIGURGSIGIO の場合、各ソケットにはプロセス番号とプロセスグループ ID があります。これらの値はゼロに初期化されますが、先の例で示しているように、F_SETOWN fcntl(2) を使用してあとから定義し直すことができます。fcntl(2) の 3 番目の引数が正の場合、ソケットのプロセス ID を設定します。fcntl(2) の 3 番目の引数が負の場合、ソケットのプロセスグループ ID を設定します。SIGURG シグナルと SIGIO シグナルの受信側として許可されるのは、呼び出し側のプロセスだけです。同様に、fcntl(2)F_GETOWN は、ソケットのプロセス番号を返します。

SIGURGSIGIO の受信は、ioctl(2) を使用してソケットをユーザーのプロセスグループに割り当てることによっても有効にできます。

/* oobdata はルーチンを処理する帯域外データ */
sigset(SIGURG, oobdata);
int pid = -getpid();
if (ioctl(client, SIOCSPGRP, (char *) &pid) < 0) {
		perror("ioctl: SIOCSPGRP");
}

サーバープロセスで便利なシグナルとして、ほかに SIGCHLD が挙げられます。このシグナルは、任意の子プロセスがその状態を変更した場合にプロセスに配信されます。通常、サーバーはこのシグナルを使用して、明示的に終了を待機せずに、あるいは終了ステータスを周期的にポーリングせずに、終了した子プロセスの「リープ (刈り取り)」を行います。たとえば、先に示したリモートログインサーバーループは、例 2-16 に示すように拡大できます。


例 2-16 SIGCHLD シグナル

int reaper();
...
sigset(SIGCHLD, reaper);
listen(f, 5);
while (1) {
		int g, len = sizeof from;
		g = accept(f, (struct sockaddr *) &from, &len);
		if (g < 0) {
			if (errno != EINTR)
				syslog(LOG_ERR, "rlogind: accept: %m");
			continue;
		}
		...
}
 
#include <wait.h>
 
reaper()
{
		int options;
		int error;
		siginfo_t info;
 
		options = WNOHANG | WEXITED;
		bzero((char *) &info, sizeof(info));
		error = waitid(P_ALL, 0, &info, options);
}

親サーバープロセスがその子のリープに失敗する場合、ゾンビプロセスが生じます。

特定のプロトコルの選択

socket(3SOCKET) 呼び出しの 3 番目の引数が 0 の場合、socket(3SOCKET) は要求されたタイプである戻されたソケットにデフォルトプロトコルを使用することを選択します。通常はデフォルトプロトコルで十分であり、ほかの選択肢はありません。raw ソケットを使用して低レベルのプロトコルやハードウェアインタフェースと直接通信を行う場合は、必要に応じてプロトコル引数で非多重化を設定してください。たとえば、インターネットファミリ内の raw ソケットを使用して IP 上に新しいプロトコルを実装できます。この場合、ソケットは指定されたプロトコルのパケットだけを受信します。特定のプロトコルを取得するには、プロトコルファミリで定義されているようにプロトコル番号を決定します。インターネットファミリの場合、「標準のルーチン」で説明しているライブラリルーチンの 1 つ (getprotobyname(3SOCKET) など) を使用してください。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
 ...
pp = getprotobyname("newtcp");
s = socket(AF_INET6, SOCK_STREAM, pp->p_proto);

このコードでは、ソケット s はストリームベースの接続を使用しますが、プロトコルタイプはデフォルトの tcp ではなく newtcp を使用します。

アドレスのバインド

TCP と UDP は、ローカル IP アドレスローカルポート番号外部 IP アドレス、 および外部ポート番号の 4 つを使用してアドレス指定を行います。TCP では、これらの 4 つの要素は一意でなければなりません。UDP にはこのような要求はありません。ホストは複数のネットワークに常駐でき、ユーザーは割り当てられているポート番号に直接アクセスできないため、ユーザープログラムがローカルアドレスとローカルポートに使用する適切な値を常に認識できるとは限りません。この問題を避けるため、アドレスの一部を指定せずにおき、必要時にシステムにこれらの部分を適切に割り当てることができます。これらの要素の各部は、ソケット API のさまざまな部分によって指定できます。

bind(3SOCKET)

ローカルアドレスまたはローカルポート (あるいはこの両方)

connect(3SOCKET)

外部アドレスと外部ポート

accept(3SOCKET) 呼び出しは、外部クライアントからの接続情報を検出します。そのため、この呼び出しによってローカルアドレスとローカルポートがシステムに指定され (accept(3SOCKET) の呼び出し側が何も指定しなくても)、外部アドレスと外部ポートが返されます。

listen(3SOCKET) を呼び出すと、ローカルポートが選択されます。ローカル情報を割り当てるために明示的な bind(3SOCKET) がなされていない場合に listen(3SOCKET) を呼び出すと、一時的なポート番号が割り当てられます。

特定のポートに常駐するが、どのローカルアドレスが選択されてもかまわないというサービスは、それ自体をそのポートに bind(3SOCKET) し、ローカルアドレスを指定しないままにしておくことができます。このためには、<netinet/in.h> 内の定数値を持つ変数 in6addr_any に設定します。ローカルポートを固定する必要がない場合、listen(3SOCKET) を呼び出すと、ポートが選択されます。アドレス in6addr_any またはポート番号 0 を指定することを、ワイルドカードの使用と呼びます (AF_INET の場合、in6addr_any ではなく INADDR_ANY を使用する)。

ワイルドカードアドレスは、インターネットファミリにおけるローカルアドレスのバインドを簡易化します。次のコード例は、特定のポート番号 MYPORT をソケットにバインドし、ローカルアドレスは指定しないままにします。


例 2-17 ソケットにポート番号をバインドする

#include <sys/types.h>
#include <netinet/in.h>
...
struct sockaddr_in6 sin;
...
		s = socket(AF_INET6, SOCK_STREAM, 0);
		bzero (&sin6, sizeof (sin6));
		sin.sin6_family = AF_INET6;
		sin.sin6_addr.s6_addr = in6addr_any;
		sin.sin6_port = htons(MYPORT);
		bind(s, (struct sockaddr *) &sin, sizeof sin);

ホスト上の各ネットワークインタフェースは、通常、一意の IP アドレスを持ちます。ワイルドカードローカルアドレスを持つソケットは、指定されたポート番号にあてたメッセージと、ホストに割り当てられた可能性のあるアドレスに送信されたメッセージを受信できます。たとえば、ホストにアドレス 128.32.0.4 と 10.0.0.78 を持つ 2 つのインタフェースがあり、ソケットが例 2-17 のようにバインドされている場合、プロセスは 128.32.0.4 または 10.0.0.78 にアドレス指定された接続要求を受け入れることができます。特定のネットワーク上のホストだけに接続を許可するには、サーバーは適切なネットワーク上のインタフェースのアドレスをバインドします。

同様に、ローカルポート番号は指定しないままにしておくことができます (0 を指定)。この場合、システムがポート番号を選択します。次に、特定のローカルアドレスをソケットにバインドし、ローカルポート番号を指定しないままにする例を示します。

bzero (&sin, sizeof (sin));
(void) inet_pton (AF_INET6, ":ffff:127.0.0.1", sin.sin6_addr.s6_addr);
sin.sin6_family = AF_INET6;
sin.sin6_port = htons(0);
bind(s, (struct sockaddr *) &sin, sizeof sin);

システムは、次の 2 つの基準でローカルポート番号を選択します。

クライアントのポート番号と IP アドレスは、accept(3SOCKET) (from の結果) または getpeername(3SOCKET) で見つけます。

ポート番号を選択するためにシステムが使用するアルゴリズムがアプリケーションに適さない場合もあります。これは、関連付けが 2 段階のプロセスで作成されるためです。たとえば、インターネットファイル転送プロトコルでは、データ接続は常に同じローカルポートから発生しなければならないと定めています。しかし、異なる外部ポートに接続することによって、関連付けの重複が避けられます。この場合、前のデータ接続のソケットが存在していると、システムは同じローカルアドレスとポート番号をソケットにバインドすることを許可しません。デフォルトのポート選択アルゴリズムを無効にするには、次に示すようにアドレスをバインドする前にオプション呼び出しを行う必要があります。

 ...
int on = 1;
...
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
bind(s, (struct sockaddr *) &sin, sizeof sin);

この呼び出しを行うと、すでに使用されているローカルアドレスをバインドできます。同じローカルアドレスとローカルポートを持つほかのソケットが同じ外部アドレスと外部ポートを持たないことをシステムが接続時に検証するため、この呼び出しは一意性という条件に違反することはありません。関連付けがすでに存在する場合、エラー EADDRINUSE が返されます。

マルチキャストの使用

IP マルチキャストは、タイプが SOCK_DGRAM または SOCK_RAW であるソケット AF_INET6 または AF_INET およびインタフェースドライバがマルチキャストをサポートするサブネットワーク上でだけサポートされます。

IPv4 マルチキャストデータグラムの送信

マルチキャストデータグラムを送信するには、sendto(3SOCKET) 呼び出しで宛先アドレスとして 224.0.0.0 〜 239.255.255.255 の範囲の IP マルチキャストアドレスを指定します。

デフォルトでは、IP マルチキャストデータグラムは存続期間 (TTL) 1 で送信されます。この場合、単一のサブネットワーク外にデータグラムが転送されることはありません。ソケットオプション IP_MULTICAST_TTL を指定すると、後続のマルチキャストデータグラムの TTL を 0 〜 255 の範囲の任意の値に設定してマルチキャストの配信範囲を制御できます。

    u_char ttl;
    setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl,sizeof(ttl))

TTL 0 のマルチキャストデータグラムはいかなるサブネット上でも伝送されませんが、送信ホストが宛先グループに属しており、かつ送信側ソケットでマルチキャストループバックが無効になっていない場合は、ローカルに配信できます (下記を参照)。最初の配信先となるサブネットに 1 つ以上のマルチキャストルーターが接続されている場合は、1 を超える TTL を持つマルチキャストデータグラムを複数のサブネットに配信できます。意味のある配信範囲制御を提供するため、マルチキャストルーターは TTL に「しきい値」の概念をサポートします。この概念は、一定の TTL 未満のデータグラムが特定のサブネットを越えることを防止します。マルチキャストデータグラムの初期 TTL と、それらに対してしきい値が強制する規則を次に示します。

同じホストに制限される 

同じサブネットに制限される 

32 

同じサイトに制限される 

64 

同じ地域に制限される 

128 

同じ大陸に制限される 

255 

配信範囲内で制限されない 

「サイト」と「地域」は厳密には定義されません。サイトは、ローカルな事柄として、小さな管理単位にさらに分割できます。

アプリケーションは、上記の TTL 以外に初期 TTL を選択できます。たとえば、アプリケーションは、TTL シーケンス 0、1、2、4、8、16、32 を使用し、TTL 0 から開始して応答が得られるまでより大きな TTL のマルチキャスト照会を送ることによってネットワークリソースの「拡張リング検索」が行えます。

マルチキャストルーターは、224.0.0.0 〜 224.0.0.255 の範囲の宛先アドレスを持つマルチキャストデータグラムの転送を、TTL の値にかかわらず拒否します。この範囲のアドレスは、経路指定プロトコルとその他の低レベルトポロジの発見または保守プロトコル (ゲートウェイ発見、グループメンバーシップ報告など) の使用に予約されています。

ホストにマルチキャスト機能を持つ複数のインタフェースがある場合でも、各マルチキャスト伝送は単一のネットワークインタフェースから送信されます (ホストがマルチキャストルーターでもあり、TTL が 1 を超える場合には、発信インタフェース以外のインタフェースにマルチキャストを転送できる)。一定のソケットからの後続の伝送については、ソケットオプションを使用してデフォルトを無効にできます。

    struct in_addr addr;
    setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, &addr, sizeof(addr))
addr は、使用する発信インタフェースのローカル IP アドレスです。デフォルトインタフェースに戻すには、アドレス INADDR_ANY を指定します。インタフェースのローカル IP アドレスは、SIOCGIFCONF ioctl を使用して取得します。インタフェースがマルチキャストをサポートするかどうかを確認するには、SIOCGIFFLAGS ioctl を使用してインタフェースフラグを取り出し、IFF_MULTICAST フラグが設定されているかどうかをテストしてください。このオプションは、インターネットトポロジと明確な関係があるマルチキャストルーターなどのシステムサービスを主な対象としています。

(発信インタフェース上で) 送信ホスト自体が属しているグループにマルチキャストデータグラムが送信されると、ローカル配信の IP 層によってデータグラムのコピーがデフォルトでループバックされます。後続のデータグラムがループバックされるかどうかにかかわらず、別のソケットオプションが送信側に明示的な制御を与えます。

    u_char loop;
    setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop))  
このコードの loop の値は、ループバックを無効にする場合は 0、ループバックを有効にする場合は 1 です。単一のホストにインスタンスを 1 つしか持たないアプリケーション (ルーターやメールデーモンなど) では、このオプションを使用するとアプリケーション自体の伝送を受信するオーバーヘッドが排除されるため、パフォーマンスが向上します。このオプションは、単一のホストに複数のインスタンスを持つアプリケーション (会議システムプログラムなど) や送信側が宛先グループに属さないアプリケーション (時間照会プログラムなど) には通常使用しないでください。

送信ホストが別のインタフェースの宛先グループに属している場合、1 を超える TTL で送信されたマルチキャストデータグラムは、他方のインタフェース上の送信ホストに配信できます。このような配信には、ループバック制御オプションは何の効果もありません。

IPv4 マルチキャストデータグラムの受信

ホストは、IP マルチキャストデータグラムを受信する前に、1 つ以上の IP マルチキャストグループのメンバーになる必要があります。プロセスは、次のソケットオプションを使用して、マルチキャストグループに加わるようにホストに求めることができます。

    struct ip_mreq mreq;
    setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) 
mreq は次の構造体です。
    struct ip_mreq {
        struct in_addr imr_multiaddr;   /* 加わるマルチキャストグループ */
        struct in_addr imr_interface;   /* 加わるインタフェース */
    }  
各メンバーシップは単一のインタフェースに関連付けられ、複数のインタフェース上の同じグループに加わることができます。imr_interfacein6addr_any になるように 指定してデフォルトのマルチキャストインタフェースを選択するか、あるいはホストのローカルアドレスの 1 つを指定して特定の (マルチキャスト機能を持った) インタフェースを選択してください。

メンバーシップを取り消すには、次のコードを使用します。

    struct ip_mreq mreq;
    setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq))  
mreq には、メンバーシップの追加に使用したのと同じ値が入ります。ソケットが閉じられるか、あるいはソケットを保持しているプロセスが停止される時には、ソケットに関連付けられたメンバーシップも取り消されます。特定のグループ内で複数のソケットがメンバーシップを要求でき、ホストは最後の要求が取り消されるまでそのグループのメンバーにとどまります。

ソケットのどれかがデータグラムの宛先グループ内でメンバーシップを要求した場合、カーネル IP 層が受信マルチキャストパケットを受け入れます。特定のソケットに対するマルチキャストデータグラムの配信は、単一キャストデータグラムの場合と同様に、宛先ポートと、ソケットに関連付けられたメンバーシップ (raw ソケットの場合はプロトコルタイプ) に基づいています。特定のポートに送信されたマルチキャストデータグラムを受信するには、ローカルアドレスを未指定のまま (INADDR_ANY などを指定) ローカルポートにバインドします。

次の指定が bind(3SOCKET) の前にあると、複数のプロセスを同じ SOCK_DGRAM UDP ポートにバインドできます。

    int one = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))  
この場合、共有ポートに向けられた各受信マルチキャストまたは受信ブロードキャスト UDP データグラムは、そのポートにバインドされているすべてのソケットに配信されます。下位互換性の理由から、これは単一キャストの受信データグラムには適用されません。データグラムの宛先ポートにバインドされているソケットの数にかかわらず、単一キャストデータグラムが複数のソケットに配信されることはありません。SOCK_RAW ソケットは、SO_REUSEADDR オプションがなくても単一の IP プロトコルタイプを共有できます。

マルチキャストに関連する新しいソケットオプションの説明は、<netinet/in.h> を参照してください。IP アドレスはすべて、ネットワークバイトオーダーで渡されます。

IPv6 マルチキャストデータグラムの送信

マルチキャストデータグラムを送信するには、sendto(3SOCKET) 呼び出しで宛先アドレスとして ff00::0/8 の範囲の IP マルチキャストアドレスを指定します。

デフォルトでは、IP マルチキャストデータグラムはホップ制限 1 で送信されます。この場合、単一のサブネットワーク外にデータグラムが転送されることはありません。ソケットオプション IPV6_MULTICAST_HOPS を指定すると、後続のマルチキャストデータグラムのホップ制限を 0 から 255 の範囲の任意の値に設定してマルチキャストの配信範囲を制御できます。

    uint_l;
    setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops,sizeof(hops))

ホップ制限 0 のマルチキャストデータグラムはどのサブネットにも伝送されませんが、送信ホストが宛先グループに属しており、送信側ソケットでマルチキャストループバックが無効になっていない場合は、ローカルに配信できます (次を参照)。最初の配信先となるサブネットに 1 つ以上のマルチキャストルーターが接続されている場合は、1 を超えるホップ制限を持つデータグラムを複数のサブネットに配信できます。IPv4 マルチキャストアドレスと異なり、IPv6 マルチキャストアドレスには、アドレスの最初の部分にコード化された明示的な配信範囲情報が含まれます。定義されている配信範囲を次に示します (X は未指定)。

ffX1::0/16

ノード - ローカルな配信範囲 - 同じノードに制限される

ffX2::0/16

リンク - ローカルな配信範囲

ffX5::0/16

サイト - ローカルな配信範囲

ffX8::0/16

組織 - ローカルな配信範囲

ffXe::0/16

全世界的な配信範囲

アプリケーションは、マルチキャストアドレスの配信範囲とは個別に、異なるホップ制限値を使用できます。たとえば、アプリケーションは、ホップ制限シーケンス 0、1、2、4、8、16、32 を使用し、ホップ制限 0 から開始して応答が受信されるまでより大きなホップ制限のマルチキャスト照会を送信することにより、ネットワークリソースの「拡張リング検索」を実行する場合があります。

ホストにマルチキャスト機能を持つ複数のインタフェースがある場合でも、各マルチキャスト伝送は単一のネットワークインタフェースから送信されます (ホストがマルチキャストルーターでもあり、ホップ制限 が 1 を超える場合には、発信インタフェース以外のインタフェースにマルチキャストを転送できる)。一定のソケットからの後続の伝送については、ソケットオプションを使用してデフォルトを無効にできます。

    uint_t ifindex;

    ifindex = if_nametoindex )"hme3");
    setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex, sizeof(ifindex))
ifindex は、希望する発信インタフェースのインタフェースインデックスです。デフォルトインタフェースに戻すには、値 0 を指定します。

(発信インタフェース上で) 送信ホスト自体が属しているグループにマルチキャストデータグラムが送信されると、ローカル配信の IP 層によってデータグラムのコピーがデフォルトでループバックされます。後続のデータグラムがループバックされるかどうかにかかわらず、別のソケットオプションが送信側に明示的な制御を与えます。

    uint_t loop;
    setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &loop, sizeof(loop))  
loop の値は、ループバックを無効にする場合は 0、ループバックを有効にする場合は 1 です。単一のホストにインスタンスを 1 つしか持たないアプリケーション (ルーターやメールデーモンなど) では、このオプションを使用するとアプリケーション自体の伝送を受信するオーバーヘッドが排除されるため、パフォーマンスが向上します。このオプションは、単一のホストに複数のインスタンスを持つアプリケーション (会議システムプログラムなど) や送信側が宛先グループに属さないアプリケーション (時間照会プログラムなど) に通常は使用しないでください。

送信ホストが別のインタフェースの宛先グループに属している場合、1 以上のホップ制限で送信されたマルチキャストデータグラムは、他方のインタフェース上の送信ホストに配信できます。このような配信には、ループバック制御オプションは何の効果もありません。

IPv6 マルチキャストデータグラムの受信

ホストは、IP マルチキャストデータグラムを受信する前に、1 つ以上の IP マルチキャストグループのメンバーになる必要があります。プロセスは、次のソケットオプションを使用して、マルチキャストグループに加わるようにホストに求めることができます。

    struct ipv6_mreq mreq;
    setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) 
mreq は次の構造体です。
    struct ipv6_mreq {
        struct in6_addr ipv6mr_multiaddr;   /* IPv6 マルチキャストアドレス */
        unsigned int    ipv6mr_interface;   /* インタフェースインデックス */
    }  
各メンバーシップは単一のインタフェースに関連付けられ、複数のインタフェース上の同じグループに加わることができます。ipv6_interface0 になるように指定してデフォルトのマルチキャストインタフェースを選択するか、あるいはホストのインタフェースの 1 つのインタフェースインデックスを指定してその (マルチキャスト機能を持った) インタフェースを選択してください。

グループから抜けるには、次のコードを使用します。

    struct ipv6_mreq mreq;
    setsockopt(sock, IPPROTO_IPV6, IP_LEAVE_GROUP, &mreq, sizeof(mreq))  
mreq には、メンバーシップの追加に使用した同じ値が入ります。ソケットが閉じられるか、あるいはソケットを保持しているプロセスが停止する時には、ソケットに関連付けられたメンバーシップも取り消されます。特定のグループ内で複数のソケットがメンバーシップを要求でき、ホストは最後の要求が取り消されるまでそのグループのメンバーにとどまります。

ソケットのどれかがデータグラムの宛先グループ内でメンバーシップを要求した場合、カーネル IP 層が受信マルチキャストパケットを受け入れます。特定のソケットに対するマルチキャストデータグラムの配信は、単一キャストデータグラムの場合と同様に、宛先ポートと、ソケットに関連付けられたメンバーシップ (raw ソケットの場合はプロトコルタイプ) に基づいています。特定のポートに送信されたマルチキャストデータグラムを受信するには、ローカルアドレスを未指定のまま (INADDR_ANY などに指定) ローカルポートにバインドします。

次の指定が bind(3SOCKET) の前にあると、複数のプロセスを同じ SOCK_DGRAM UDP ポートにバインドできます。

    int one = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))  
この場合、共有ポートに向けられた各受信マルチキャスト UDP データグラムは、そのポートにバインドされているすべてのソケットに配信されます。下位互換性の理由から、これは単一キャストの受信データグラムには適用されません。データグラムの宛先ポートにバインドされているソケットの数にかかわらず、単一キャストデータグラムが複数のソケットに配信されることはありません。SOCK_RAW ソケットは、SO_REUSEADDR オプションがなくても単一の IP プロトコルタイプを共有できます。

マルチキャストに関連する新しいソケットオプションの説明は、<netinet/in.h> を参照してください。IP アドレスはすべて、ネットワークバイトオーダーで渡されます。

ゼロコピーとチェックサム負荷解除

SunOS 5.6 以降では、TCP/IP プロトコルスタックは、ゼロコピーと TCP チェックサム負荷解除という 2 つの新しい機能をサポートするように機能が拡張されています。


注 -

ゼロコピーとチェックサム負荷解除は互いに機能的には依存していませんが、最大のパフォーマンスを得るには連携して動作する必要があります。チェックサム負荷解除はネットワークインタフェースからのハードウェアサポートを必要とし、このハードウェアサポートがないとゼロコピーは有効になりません。


ゼロコピーを実行するためには VM ページの再マッピングを適用する前に、アプリケーションがページ型のバッファーを供給する必要があります。負荷が高い書き込み時コピーの失敗を避けるには、アプリケーションは伝送側に大きな循環バッファーを使用する必要があります。一般的なバッファー割り当ては 16 の 8K バッファーです。

ソケットオプション

送信バッファー領域または受信バッファー領域の変更などにより、setsockopt(3SOCKET)getsockopt(3SOCKET) を使用してソケットオプションのいくつかを設定、取得できます。呼び出しの一般的な書式を次に示します。

setsockopt(s, level, optname, optval, optlen);
および
getsockopt(s, level, optname, optval, optlen);

場合によっては (バッファーサイズを設定する場合など)、これらの指定がオペレーティングシステムに対する参考情報にしかならない場合があります。値を適切に調整する権限はオペレーティングシステムに与えられています。

表 2-4 に、これらの呼び出しの引数を示します。

表 2-4 setsockopt(3SOCKET)getsockopt(3SOCKET) の引数

引数 

説明 

s

オプションの適用先であるソケット 

level

sys/socket.h 内の 記号定数 SOL_SOCKET によって示されるプロトコルレベル (ソケットレベルなど) を指定する

optname

オプションを指定する、sys/socket.h で定義されている記号定数

optval

オプションの値を示す 

optlen

オプションの値の長さを示す 

getsockopt(3SOCKET) の場合、optlen は、初期状態では optval によって示される記憶領域のサイズに設定され、最終的に使用された記憶領域の長さに設定されます。

既存ソケットのタイプ (ストリームやデータグラムなど) を決定すると便利な場合があります。inetd(1M) によって呼び出されるプログラムは、SO_TYPE ソケットオプションと getsockopt(3SOCKET) 呼び出しを使用してこの決定を行うことができます。

#include <sys/types.h>
#include <sys/socket.h>
 
int type, size;
 
size = sizeof (int);
if (getsockopt(s, SOL_SOCKET, SO_TYPE, (char *) &type, &size) <0) {
 	...
}

getsockopt(3SOCKET) のあと、sys/socket.h で定義しているように、type は ソケットタイプの値に設定されます。データグラムソケットの場合、typeSOCK_DGRAM です。

inetd(1M) デーモン

システムに装備されているデーモンの 1 つが inetd(1M) です。システムの起動時に呼び出され、 /etc/inet/inetd.conf ファイルを読み、その内容に従いサービスを取得します。デーモンは /etc/inet/inetd.conf ファイルに記述されている各サービスごとに 1 つのソケットを作成し、各ソケットに適切なポート番号の割り当てを行います。inetd(1M) に関するより詳しい情報は、マニュアルページを参照してください。

inetd(1M) は各ソケットのポーリングを行い、そのソケットに対するサービスの接続要求待ちを行います。SOCK_STREAM タイプのソケットの場合、inetd(1M) は待機ソケットに対し accept(3SOCKET) を実行、新しいファイル記述子 0 および 1 (stdin および stdout) において、新しいソケットに対して fork(2)dup(2) を実行し、その他の開かれているファイル記述子を終了させ適切なサーバーに対し exec(2) を実行します。

inetd(1M) の主な利点は、使用されていないサービスがシステム資源を使用しない点にあります。また、接続の確立に関する処理の大部分を inetd(1M) が請け負う点も大きな利点の 1 つです。inetd(1M) によって開始されたサーバーのソケットは、ファイル記述子 0 および 1 においてクライアントに接続され、ただちに read(2)write(2)send(3SOCKET)、または recv(3SOCKET) の実行が可能です。サーバーは stdio の取り決めに従い、必要な場合に fflush(3C) を利用する限りバッファー入出力の使用が可能です。

getpeername(3SOCKET) はソケットに接続されたピア (処理) のアドレスを戻り値として返します。これは inetd(1M) で開始されたサーバーにおいて、有効な手段です。たとえば、インターネットアドレス (たとえば、クライアントの IPv6 アドレスとしては定型的なアドレス fec0::56:a00:20ff:fe7d:3dd2 など) のログを記録する場合、inetd(1M) を使用しているサーバーならば以下が使用可能です。

    struct sockaddr_storage name;
    int namelen = sizeof (name);
    char abuf[INET6_ADDRSTRLEN];
    struct in6_addr addr6;
    struct in_addr addr;

    if (getpeername(fd, (struct sockaddr *)&name, &namelen) == -1) {
        perror("getpeername");
        exit(1);
    } else {
        addr = ((struct sockaddr_in *)&name)->sin_addr;
        addr6 = ((struct sockaddr_in6 *)&name)->sin6_addr;
        if (name.ss_family == AF_INET) {
                (void) inet_ntop(AF_INET, &addr, abuf, sizeof (abuf));
        } else if (name.ss_family == AF_INET6 && IN6_IS_ADDR_V4MAPPED(&addr6)) {
                /* これは IPv4 によりマップされた IPv6 アドレスです。*/
                IN6_MAPPED_TO_IN(&addr6, &addr);
                (void) inet_ntop(AF_INET, &addr, abuf, sizeof (abuf));
        } else if (name.ss_family == AF_INET6) {
                (void) inet_ntop(AF_INET6, &addr6, abuf, sizeof (abuf));

        }
        syslog("Connection from %s¥n", abuf);
    }

ブロ−ドキャストとネットワーク構成の判定

ブロードキャストは IPv6 ではサポートされていません。サポートしているのは IPv4 のみです。

データグラムソケットにより送信されたメッセージは、接続されているネットワークのすべてのホストに届くようブロードキャストを行うことができます。システムのソフトウェア上でのブロードキャストのシミュレーションは行われないため、ネットワークがブロードキャストをサポートしていなければなりません。ブロードキャストはネットワーク上のすべてのホストにサービスを強制するため、ネットワークメッセージはネットワークに大きい負荷をかける場合があります。ブロードキャストは主に 2 つの目的に使用されます。アドレスが不明なローカルネットワーク上の資源の検索、またアクセス可能なすべての隣接ホストへ送られる情報のルーティングを行う場合に使用されます。

ブロードキャストメッセージの送信を行うには、インターネットデータグラムソケットを作成します。

s = socket(AF_INET, SOCK_DGRAM, 0);
ソケットのポート番号を割り当てます。
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_port = htons(MYPORT);
bind(s, (struct sockaddr *) &sin, sizeof sin);

ネットワークのブロードキャストアドレス宛てに送信を行うことにより、データグラムは 1 つのネットワーク上のみでブロードキャストを行うことが可能です。また netinet/in.h 内で定義されている特別なアドレス INADDR_BROADCAST に送信することにより、接続されているすべてのネットワークに対し、データグラムのブロードキャストを行うことが可能です。

システムにはシステム上のネットワークインタフェースに関する情報の数 (IP アドレスおよびブロードキャストアドレスを含む) を判定するメカニズムが装備されています。SIOCGIFCONF ioctl(2) 呼び出しはホストのインタフェース構成を単一 ifconf 構造体において (戻り値として) 返します。この構造体にはホストが接続されている各ネットワークインタフェースによりサポートされる各アドレスファミリに対し個別の ifreq 構造体の配列が含まれます。例 2-18 ではこれらの構造体の net/if.h 内における定義を行っています。


例 2-18 net/if.h

struct ifreq {
#define IFNAMSIZ 16
char ifr_name[IFNAMSIZ]; /* たとえば名前が "en0" */
union {
		struct sockaddr ifru_addr;
		struct sockaddr ifru_dstaddr;
		char ifru_oname[IFNAMSIZ]; /* 名前の場合、その他 */
		struct sockaddr ifru_broadaddr;
		short ifru_flags;
		int ifru_metric;
		char ifru_data[1]; /* インタフェース依存データ */
		char ifru_enaddr[6];
} ifr_ifru;
#define ifr_addr ifr_ifru.ifru_addr
#define ifr_dstaddr ifr_ifru.ifru_dstaddr
#define ifr_oname ifr_ifru.ifru_oname
#define ifr_broadaddr ifr_ifru.ifru_broadaddr
#define ifr_flags ifr_ifru.ifru_flags
#define ifr_metric ifr_ifru.ifru_metric
#define ifr_data ifr_ifru.ifru_data
#define ifr_enaddr ifr_ifru.ifru_enaddr
};

インタフェース構成を取り出す呼び出しは以下の通りです。

/*
 * インタフェースの数を検索するため SIOCGIFNUM ioctl を実行。
 *
 * 発見されたインタフェースの数に相当する空間を割り当て。
 *
 * 割り当てられたバッファーに対し SIOCGIFCONF を実行。
 *
 */
if (ioctl(s, SIOCGIFNUM, (char *)&numifs) == -1) {
        numifs = MAXIFS;
}
bufsize = numifs * sizeof(struct ifreq);
reqbuf = (struct ifreq *)malloc(bufsize);
if (reqbuf == NULL) {
        fprintf(stderr, "out of memory¥n");
        exit(1);
}
ifc.ifc_buf = (caddr_t)&reqbuf[0];
ifc.ifc_len = bufsize;
if (ioctl(s, SIOCGIFCONF, (char *)&ifc) == -1) {
        perror("ioctl(SIOCGIFCONF)");
        exit(1);
}
...
}

この呼び出しの後、 buf にはホストが接続されている各ネットワークに対して個別の ifreq 構造体の配列が格納されます。これらの構造体の順序付けは、まずインタフェース名で、次にサポートするアドレスファミリによって行われます。 ifc.ifc_lenifreq 構造体に使用されるバイト数に合わせて設定されます。

各構造体は対応するネットワークの利用可能状況、ポイントツーポイント (point-to-point) またはブロードキャストなどを表示するインタフェースフラグのセットを持ちます。例 2-19SIOCGIFFLAGS ioctl(2)ifreq 構造体により指定されたインタフェースのフラグを返すコードの例です。


例 2-19 インタフェースフラグの取得

struct ifreq *ifr;
ifr = ifc.ifc_req;
for (n = ifc.ifc_len/sizeof (struct ifreq); --n >= 0; ifr++) {
   /*
    * 別の目的でアドレスファミリ用に使用されているインタフェースを
    * 使用しないよう注意する。
    */
   if (ifr->ifr_addr.sa_family != AF_INET)
      continue;
   if (ioctl(s, SIOCGIFFLAGS, (char *) ifr) < 0) {
      ...
   }
     if ((ifr->ifr_flags & IFF_UP) == 0 ||
      (ifr->ifr_flags & IFF_LOOPBACK) ||
      (ifr->ifr_flags & (IFF_BROADCAST | IFF_POINTOPOINT)) == 0)
      continue;
}

例 2-20 では SIOCGIFBRDADDR ioctl(2) によってインタフェースのブロードキャストが取得可能であることを表現しています。


例 2-20 インタフェースのブロードキャストアドレス

if (ioctl(s, SIOCGIFBRDADDR, (char *) ifr) < 0) {
		...
}
memcpy((char *) &dst, (char *) &ifr->ifr_broadaddr,
		sizeof ifr->ifr_broadaddr);

SIOCGIFBRDADDR ioctl(2) はポイントツーポイントインタフェースのアドレスを取得する目的にも使用できます。

インタフェースブロードキャストアドレスの取得が完了したら、sendto(3SOCKET) を使用してブロードキャストデータグラムの送信を行います。

sendto(s, buf, buflen, 0, (struct sockaddr *)&dst, sizeof dst);

ブロードキャストまたはポイントツーポイントアドレスをサポートするホストが接続されている各インタフェースに対し 1 つの sendto(3SOCKET) を使用します。