クライアント側プログラムに続いて、サーバ側プログラムの作成方法を説明し ます。サーバ側プログラムは、電話による会話でいうと電話がかかってくるの を待っている側になります。このため、電話をかける側、つまり、クライアン ト側とは電話がつながるまでの手順が異なります。ただし、電話と同様に、一 度つながってしまえば違いはありません(電話では通話料金を支払うのはどち らか一方ですが)。そのサーバ側プログラムは(1)ソケットの生成、(2)ソケッ トの登録、(3)ソケットの接続準備、(4)ソケットの接続待機、(5)データの送 信/受信、(6)ソケットの切断の6つのフェーズから構成されます。サーバ側プ ログラムで利用するシステムコールにはsocket()、bind()、 listen()、accept()、read()、 write()、close()があります。
はじめ | ||
↓ | ||
socket() | (1)ソケット生成 | |
↓ | ||
bind() | (2)ソケット登録 | |
↓ | ||
listen() | (3)ソケット接続準備 | |
↓ | ||
accept() | (4)ソケット接続待機 | ←接続要求 |
↓ | ||
read()/write() | (5)受信/送信 | ←データ→クライアント側プログラム |
↓ | ||
close() | (6)ソケット切断 | |
↓ | ||
終わり |
(1)のソケット生成ではsocket()システムコールを使ってソケットを 生成します。socket()を呼び出すときは、その引数にはTCP通信に利 用するプロトコルの種類を与えます。ここでプロトコルの種類とはインターネッ ト用のものであることや、TCPやUDPの種別も含まれます。また、socket() の返値はソケット番号という整数値を返します。これは各ソケットの識別 子となります。
socket()を実行した段階ではソケットが作られただけであり、ポー ト番号などは未確定です。そこで、(2)ではbind()システムコールを 使い、(1)で生成したソケットにポート番号など割り当てます。
TCP通信のサーバ側はクライアントから通信接続を待つ側になります。このた め、通信接続を待つための準備作業が必要になります。これには(3)の listen()システムコールを利用します。
ここで準備が整いました。(4)ではaccept()システムコールを使い クライアント側からの通信接続を待ちます。サーバ側プログラムが accept()を実行すると、クライアント側からの通信接続要求が来るまで プログラムが停止し、接続語にプログラムを再開します。つまり、通信接続要求 が来るとaccept()が終了し、次の処理に移ることができるように なります。
通信接続後は、(5)のread()システムコールによりクライアントから 送られてきたデータの受信ができ、また、write()システムコールに よりサーバにクライアントにデータを送信できます。read()と write()の使い方はファイルにおけるread()と write()と同じです。
通信が完了したら、ソケットの接続を断たなければいけません。これには ファイルのクローズと同じようにclose()システムコールを利用して ソケットを閉じさせます。接続されたソケットはサーバ側からもクライアント側 からも閉じることができますが、どちらか一方がclose()により ソケットを閉じた時点で、通信接続は断たれることになります。
サーバ側プログラム例をリスト?に示します。また、図?はそれの大まかな流 れを示したものです。電話にたとえると、ステップ1のソケット生成というの は電話機を買ってくることに相当します。、ステップ2のソケット登録は電話 会社、ここではオペレーティングシステムにソケットの届け出ます。ステップ 3のソケット接続準備は電話機を電話回線に接続することに相当し、ステップ 4のソケット接続待機は文字通り電話がかかってくるのを待つことに相当し、 つまりクライアントからのソケット接続要求を待つための処理です。そして、 ステップ5以降はクライアントのデータ送信・受信とソケットクローズと同じ です。
#include <sys/fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> main() { int sockfd int new_sockfd; int writer_len; struct sockaddr_in reader_addr; struct sockaddr_in writer_addr; /* ソケットの生成 */ if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("reader: socket"); exit(1); } /* 通信ポート・アドレスの設定 */ bzero((char *) &reader_addr, sizeof(reader_addr)); reader_addr.sin_family = PF_INET; reader_addr.sin_addr.s_addr = htonl(INADDR_ANY); reader_addr.sin_port = htons(8000); /* ソケットにアドレスを結びつける */ if (bind(sockfd, (struct sockaddr *)&reader_addr, sizeof(reader_addr)) < 0) { perror("reader: bind"); exit(1); } /* コネクト要求をいくつまで待つかを設定 */ if (listen(sockfd, 5) < 0) { perror("reader: listen"); close(sockfd); exit(1); } /* コネクト要求を待つ */ if ((new_sockfd = accept(sockfd,(struct sockaddr *)&writer_addr, &writer_len)) < 0) { perror("reader: accept"); exit(1); } close(sockfd); /* ソケットを閉鎖 */ } void simpe_server(int sockfd) { char buf[1]; int buf_len; while((buf_len = read(new_sockfd, buf, 1)) > 0) { write(1, buf, buf_len); } close(new_sockfd); /* ソケットを閉鎖 */ }
ソケット生成はクライアント同じくsocket()というシステムコール を使います。ソケット生成部分だけを抜き出します。
int sockfd; /* 略 */ if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) > 0) { perror("client: socket"); exit(1); }
socket()システムコールはクライアントと同じく、第一引数を PF_INETとしてインターネットを利用することとし、第二引数を SOCK_STREAMとしてコネクション型のソケットであることを宣言しま す。そして、第三引数は0としておきます。socket()は成 功したときはソケットの番号を返し、失敗したときは負(-1)を返します。この 例の場合は、error()関数によりエラー内容を表示して、プログラム を終了します。
socket()システムコールで作られたばかりのソケットはポート番号 と結びついていません。 bind()というシステムコールを使って、オ ペレーティングシステムに使用するポート番号や通信方式を登録して、その ポート番号宛に届いたデータを渡してくれるように依頼します。
bind()はソケットのアドレスやポート番号、通信方式を登録します。 その引数はconnect()システムコールの引数と同じです。
int bind(int socket, struct scokaddr *addr, int length)
第一引数socketには登録するソケットの番号を与えます。第二引数 のaddrはそのソケット自体のアドレスやポート番号を定義する sockaddr_in構造体へのポインタとなります。lengthは第 二引数で与えたsockaddr_in構造体のサイズを与えます。プログラム リストでソケットの登録に関わるのは次の部分になります。
struct sockaddr_in reader_addr; /* 略 */ bzero((char *) &reader_addr, sizeof(reader_addr)); reader_addr.sin_family = PF_INET; reader_addr.sin_addr.s_addr = htonl(INADDR_ANY); reader_addr.sin_port = htons(8000); if (bind(sockfd, (struct sockaddr *)&reader_addr, sizeof(reader_addr)) < 0) { perror("reader: bind"); exit(1); }
はじめにsockaddr_in構造体の中身をbzero()関数を使って 0に埋めます。次にsockaddr_in構造体のメンバ変数を設定 していきます。メンバ変数のsin_familyはプロトコルファミリとして、 socket()のときと同じようにPF_INETとしてください。メ ンバ変数のsin_addr.s_addrはhtonl()を通じて INADDR_ANYを指定する。メンバ変数のsin_portは通信に利用するポー ト番号を与えます。ただし、ポート番号はhtons()関数を使って与え るようにしてください。ここではポート番号を8000にしています。
コラム:IPアドレスは何の番号?
bind()システムコールでは、ポート番号や通信方式の他にIPアドレ スも登録情報を加えました。IPアドレスの登録は不要に思われるかもしれませ んが、IPアドレスはコンピュータ自体の番号ではなく、コンピュータがもって いるネットワーク用インターフェースの番号なのです。このため、2つ以上の ネットワーク用インターフェースをもっているコンピュータは二つ以上のIPア ドレスをもっています。bind()システムコールでは、複数あるIPア ドレスのうち、どのIPアドレスとソケットを結びつけるのかを指定するもので す。一方、通常のコンピュータはネットワーク用インターフェースの数は一つ のためIPアドレスは一つです。このため、INADDR_ANYとしておけば 十分です。(コラム終わり)
コラム:クライアントがbind()を使わないのは
クライアントではサーバ側のIPアドレスやポート番号の設定を connect()システムコールで行いましたが、生成したソケット自体の IPアドレスやポート番号の登録作業はありませんでした。これは connect()システムコールが未登録ソケットに対してIPアドレスやポー ト番号の登録してくれるため、bind()システムコールを呼び出す必 要がないからです。ただし、クライアントでも特定のアドレスをソケットに割 り当てる場合にはconnect()システムコールを呼び出す前に bind()で登録します。(コラム終わり)
ソケットはbind()システムコールに登録を終了すると、クライアン トとの接続ができるようになります。ただし、クライアントと異なり、サーバ のソケット接続は、listen()とaccept()という二つの システムコールを使って2段階で行われます。
listen()システムコールはサーバがクライアントから接続要求への 受け入れる用意ができていることを示します。
int listen(int scokfd, int backlog)
第一引数sockfdは接続準備する対象となるソケットの番号です。第 二引数backlogは待ち行列の最大長を指定します。これは接続要求の ための待ち行列の最大数を指定します。複数の接続要求が来るような場合、サー バが何らかの処理をしていると接続要求を失う可能性があります。処理しきれ ない接続要求を一時的に待ち状態にしておく必要があり、その最大数が backlogとなります。ただし、その最大数はオペレーティングシステ ムに制限され、5以下になることが多いため、第二引数は5としておくことが普 通です。
プログラムリストでは以下の部分がlisten()に関わる部分です。
if (listen(sockfd, 5) < 0) { perror("reader: listen"); close(sockfd); exit(1); }
失敗した場合は-1を返しますが、その原因のほとんどは 第一引数sockfdがソケット番号でない場合や、 ソケットの生成・準備が失敗している場合です。
listen()まで完了したら、後はクライアントからの接続要求を待つ だけです。接続要求の待機はaccept()システムコールを使います。 accept()は接続要求があるまで終了しません。つまり、サーバは accept()をを実行した時点で、一旦停止し、接続要求があると、 accept()直後から再開します。
accept()システムコールはクライアントから接続要求を待ち、 接続要求があるとその接続のソケット番号を返す。
int accept(int sockfd, struct scokaddr *addr, int *addrlen)
第一引数sockfdはソケット番号です。第二引数addrは sockaddr構造体へのポインタで、インターネットでは、 sockaddr_in構造体をsockaddr構造体にキャストしたもの を与えます。この第二引数には接続してきたクライアントのアドレス等が格納 されます。sockaddr_in構造体として、その中身を取り出すと、クラ イアント側のIPアドレスやポート番号などの情報を調べることができます。な お、第二引数にNULLを与えた場合はクライアント側の情報を返すこ とはありません。
if ((new_sockfd = accept(sockfd,(struct sockaddr *)&writer_addr, &writer_len)) < 0) { perror("reader: accept"); exit(1); }
accept()はクライアントから接続要求が来ると、接続処理を行うと 同時に新しいソケットを生成します。この新しいソケットはその接続に使われ るソケットであり、元々のソケットは次の接続要求の受け付けのために使われ ます。なお、返値はその新しいソケットの番号となります。失敗した場合は-1 を返します。
accept()によりソケットの接続ができれば、クライアントと データ送信や受信ができるようになります。クライアントと同様に、 送信にはファイル出力に使うwrite()システムコールを、受信 にはファイル入力に使うread()を使って実現できます。
通信が終了したらソケットをクローズしなければいけません。
ソケットのクローズはファイルのクローズと同じシステムコール、つまり close()を利用します。
int close(int sockfd)
引数はクローズするソケットの番号を指定します。クローズに失敗した場合は- 1を返します。
コラム:inetd
サーバプログラムが通信接続を待つには、そのサーバプログラムが起動されて いてメモリ上で実行されている必要があります。しかし、一つのコンピュータ で複数のサーバが稼働している場合は、それらもすべてメモリ上で実行されて いなければなりません。そこで、UNIX系のオペレーティングシステムでは inetdという機構をもっています。これは、あらゆるポート番号宛の通信接続を 待っているサーバプログラムです。そして、あるポート番号宛の通信接続要求 が到着すると、そのポート番号に対応したサーバプログラムを起動し、通信 データをそのサーバプログラムに渡します。
inetdとサーバプログラムは標準入出力を使って通信をすることができますの で、サーバプログラム自体はTCP/IPではなく標準入出力を使ったプログラムと して実現することができるようになります。(コラム終わり)