もっとも一般的な分散型アプリケーションは、クライアントサーバーモデルです。このスキーマでは、クライアントプロセスはサーバープロセスからのサービスを要求します。
代替スキーマとして、休止しているサーバープロセスを削除できるサービスサーバーがあります。たとえば、inetd(1M) というインターネットサービスデーモンです。inetd(1M) はさまざまなポートで待機しますが、起動時に構成ファイルを読み取ることによって使用するポートを決定します。inetd(1M) のサービスを受けるポートでコネクションが要求されると、inetd(1M) はクライアントにサービスを行うために適切なサーバーを生成します。クライアントは、そのコネクションで中間媒体が何らかの役割を果たすことは意識しません。inetd(1M) の詳細については、inetd デーモンを参照してください。
ほとんどのサーバーには、既知のインターネットポート番号または UNIX ファミリ名でアクセスします。既知の UNIX ファミリ名の例には、rlogin サービスがあります。例 7–6 に、リモートログインサーバーのメインループを示します。
DEBUG モードで動作していない限り、サーバーはその呼び出し元の制御端末との関連付けを解除します。
(void) close(0); (void) close(1); (void) close(2); (void) open("/", O_RDONLY); (void) dup2(0); (void) dup2(0); setsid();
関連付けを解除することによって、サーバーは制御端末のプロセスグループからシグナルを受信しません。制御端末との関連付けを解除したあと、サーバーはエラーレポートを制御端末に送信できません。したがって、サーバーは syslog(3C) でエラーを記録する必要があります。
サービスの定義を取得するために、サーバーは getaddrinfo(3SOCKET) を呼び出します。
bzero(&hints, sizeof (hints)); hints.ai_flags = AI_ALL|AI_ADDRCONFIG; hints.ai_socktype = SOCK_STREAM; error = getaddrinfo(NULL, "rlogin", &hints, &aip);
aip に返される結果は、プログラムがサービス要求を待機するインターネットポートを定義します。標準のポート番号の一部は /usr/include/netinet/in.h で定義されています。
次に、サーバーはソケットを作成して、サービス要求を待機します。bind(3SOCKET) ルーチンを使用すると、サーバーは必ず指定された場所で待機します。リモートログインサーバーが待機するポート番号は制限されているため、サーバーはスーパーユーザーとして動作します。次のループに、サーバーのメインループ (本体) を示します。
/* コネクション要求を待機する */ for (;;) { faddrlen = sizeof (faddr); new_sock = accept(sock, (struct sockaddr *)&faddr, &faddrlen); if (new_sock == -1) { if (errno != EINTR && errno != ECONNABORTED) { perror("rlogind: accept"); } continue; } if (fork() == 0) { close (sock); doit (new_sock, &faddr); } close (new_sock); } /*NOTREACHED*/
accept(3SOCKET) は、クライアントがサービスを要求するまでメッセージをブロックします。さらに、SIGCHLD などのシグナルによる割り込みを受けた場合、accept(3SOCKET) は失敗を示す値を返します。accept(3SOCKET) からの戻り値を調べて、エラーが発生している場合は syslog(3C) でエラーを記録します。
次に、サーバーは子プロセスをフォークし、リモートログインプロトコル処理の本体を呼び出します。コネクション要求を待ち行列に入れるために親プロセスが使用するソケットは、子プロセスで閉じられます。accept(3SOCKET) が作成したソケットは、親プロセスで閉じられます。クライアントのアドレスがサーバーアプリケーションの doit() ルーチンに渡され、クライアントが認証されます。
この節では、クライアントリモートログインプロセスで行われる処理について説明します。サーバー側と同様に、まずリモートログインのサービス定義の位置を確認します。
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); }
getaddrinfo(3SOCKET) は、res にあるアドレスの一覧の先頭を返します。希望のアドレスを見つけるには、ソケットを作成し、一覧に返される各アドレスに接続して、動作するアドレスが見つかるまで繰り返します。
for (aip = res; aip != NULL; aip = aip->ai_next) { /* * ソケットを開く。アドレスタイプは、提供される * getaddrinfo() によって変わる。 */ sock = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol); if (sock == -1) { perror("socket"); freeaddrinfo(res); return (-1); } /* ホストに接続する */ if (connect(sock, aip->ai_addr, aip->ai_addrlen) == -1) { perror("connect"); (void) close(sock); sock = -1; continue; } break; }
ソケットが作成され、希望のサービスに接続されます。sock はバインド解除されているので、connect(3SOCKET) ルーチンは暗黙的に sock をバインドします。
サービスの中にはデータグラムソケットを使用するものがあります。rwho(1) サービスは、LAN に接続されたホストについての状態情報を提供します。ネットワークトラフィックが重くなるため、in.rwhod(1M) は実行しないでください。rwho サービスは、特定のネットワークに接続されたすべてのホストに情報をブロードキャストします。rwho サービスは、データグラムソケットを使用する例の 1 つです。
rwho(1) サーバープロセスを実行するホスト上のユーザーは、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) サーバープロセスによって状態情報が周期的にブロードキャスト送信されます。このサーバープロセスも状態情報を受信します。このサーバープロセスはまた、データベースを更新します。このデータベースは、各ホストの状態のために解釈されます。サーバーはそれぞれ個別に動作し、ローカルネットワークとそのブロードキャスト機能によってのみ結合されます。
大量のネットトラフィックが生成されるため、ブロードキャストを使用することは非効率的です。サービスが広範囲に渡り、頻繁に使用されない限り、周期的なブロードキャストに手間がかかり簡潔さが失われます。
次に、rwho(1) サーバープロセスの簡単な例を示します。このコードは、まず、ネットワーク上のほかのホストからブロードキャストされた状態情報を受信し、次に、自分が動作しているホストの状態情報を提供します。最初のタスクは、プログラムのメインループで行われます。rwho(1) ポートで受信したパケットが別の 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) サーバープロセスに知らせる必要があります。このタスクはタイマーで実行されます。このタスクはシグナルによって起動されます。
状態情報は、ローカルネットワーク上でブロードキャスト送信されます。ブロードキャストをサポートしないネットワークでは、マルチキャストを使用してください。