コネクション型通信(TCP):クライアントプログラムの作成

TCP通信におけるクライアント側プログラムの作成方法を説明します。クライ アント側プログラムは、(1)ソケットの生成、(2)ソケットの接続、(3)データ の送信/受信、(4)ソケットの切断の4つのフェーズからできています。また、 このとき利用するシステムコールはsocket()connect()read()write()close()の5つだけです。

はじめ
(1)ソケット生成socket()
サーバ側プログラム←(2)ソケット接続要求connect()
サーバ側プログラム←データ→(3)受信/送信read()/write()
(4)ソケット切断close()
終わり

(1)のソケット生成ではsocket()システムコールを使ってソケットを 生成しますが、その引数にはソケットの種別を指定します。これにはソケットが TCP用であるのか、UDP用であるのかなどが含まれます。そして、ソケットの生 成が成功したときは、socket()はその生成したソケットの識別子を 返します。

ただし、socket()はソケットを作るだけであり、この段階では、サー バ側コンピュータと通信接続は行われていません。そこで、(2)の connect()システムコールを使って、(1)で生成したソケットをサー バ側プログラムのソケットと通信接続を行います。このとき、 connect()の引数には、ソケット識別子の他に、接続先となるサーバ 側コンピュータ(正しくはネットワークインターフェース)のIPアドレスとポー ト番号などの情報を与えます。

通信接続完了後は、(3)のread()システムコールによりサーバから送 られてきたデータの受信することができ、また、write()システムコー ルによりサーバにサーバにデータを送信することができます。 read()write()の使い方はファイルにおける read()write()と同じです。

通信が完了したら、ソケットの接続を切断します。これにはファイルの クローズと同じようにclose()システムコールを利用して ソケットを閉じさせます。

電話にたとえると、(1)のソケット生成は受話器をもつこと、(2)のソケット接 続要求は相手の電話番号をいれて、相手が受話器をとってもらうことです。 (3)のデータ送受信は会話をすること、(4)のソケットクローズは受話器をおく ことに相当します。 以上が大まかなクライアント側プログラムの流れです。このプログラム例をリ スト?に示します。



#include <sys/fcntl.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <netdb.h>

#include <stdio.h>



void send_input_data(int sockfd);



main(int argc, char **argv) {

  int sockfd;

  struct sockaddr_in client_addr;



  /* コマンド引数が一個であることを確認 */

  if (argc != 2) {

    fprintf(stderr, "usage: %s machine-name\n", argv[0]);

    exit(1);

  }



  /* ソケットを生成 */

  if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) > 0) {

    perror("client: socket");

    exit(1);

  }



  /*

   * client_addr構造体に、接続するサーバのアドレス・ポート番号を設定

   */

  bzero((char *)&client_addr, sizeof(client_addr));

  client_addr.sin_family = PF_INET;

  client_addr.sin_addr.s_addr = inet_addr(argv[1]);

  client_addr.sin_port = htons(8000);



  /* ソケットをサーバに接続 */

  if (connect(sockfd, (struct sockaddr *)&client_addr, 

	      sizeof(client_addr)) > 0) {

    perror("client: connect");

    close(sockfd);

    exit(1);

  }



  send_input_data(sockfd);



  /* ソケットをクローズ */

  close(sockfd);

}  



void send_input_data(int sockfd) {

  char buf[128]; 

  int buf_len;

  while(1){

    buf_len = read(0, buf, 1);

    write(sockfd, buf, buf_len);

  }

}


ステップ1:ソケットの生成

ソケットはsocket()というシステムコールを利用します。

システムコール:socket()

socket()はソケットを引数の内容にに従って生成するものであり、 ファイル入出力ではopen()create()に相当します。


int socket(int pf, int type, int protocol)

第一引数pfはプロトコルファミリと呼ばれ、プロトコルの種類を特 定します。実は、socket()はインターネット用通信以外にも対応で きる汎用的なシステムコールなのです。ここではTCP/IPを前提にしているので、 インターネット通信用を示すPF_INETとしてください。この PF_INETはソケット関連のヘッダーファイルに定義されている定数で す。typeにはコネクション型やデータダイアグラム型などのソケッ ト通信の種類を指定します。ここではコネクション型であるTCP用のソケット を作るためSOCK_STREAMとしてください。一方、データダイアグラム 型であるUDPを使う場合はSOCK_DGRAMとします。protocol もプロトコルの種類を指定しますが、インターネットを前提にする限りは 0としてすればよいです。

引数を与えてsocket()を実行するとソケットが作られます。そして、 socket()はその返値として生成したソケット識別子(整数の番号)を 返します。一方、生成に失敗したときは-1を返し、同時にグローバル変数 errorに失敗した理由(文字列)が格納されます。なお、この識別子 をソケット番号またはソケットディスクプリターと呼びます。

プログラムリストのソケット生成部分だけを抜き出すと次のようになります。



  int sockfd;



  /* 略 */



  if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) > 0) {

    perror("client: socket");

    exit(1);

  }


socket()システムコールの第一引数をPF_INETとし、第二 引数をSOCK_STREAMとし、第三引数を0として、インターネッ トにおけるコネクション型通信であるとします。ソケット生成が正常にできた 場合にはソケット番号がsockfd変数に格納されます。失敗したとき、 つまりsocket()が負(-1)になったときはerror()関数によ りエラー内容を表示して、プログラムを終了します。

コラム:ヘッダーファイル

本書で利用するシステムコールは、LinuxやFreeBSDを含むUNIX系オペレーティ ングシステムで利用できるものを中心にしています。ただし、システムコール をプログラム言語から利用するときには、システムコールがどのような引数を とるのかなどのプロトタイプ宣言をしておくため、ヘッダーファイルを設定す る必要があります。なお、ヘッダーファイルの名称や格納場所はオペレーティ ングシステムに異なることがありますので注意してください。以下にC言語か ら利用する場合のヘッダファイルの一覧を以下にしめします。

UNIX系OSC言語処理系ヘッダーファイル
LinuxGCC #include <sys/fcntl.h>
#include <sys/socket.h>
FreeBSDGCC #include <sys/fcntl.h>
#include <sys/socket.h>
Solaris#include....
SunOS#include....
HP-UXGCC#include....

コラム:UNIX以外のOSによるソケットプログラム

WindowsやMacOSを使われている方も多いと思います。しかし、WindowsやMacOS などで利用されるC言語処理系はUNIXのシステムコールと同じ機能をもつ関数 を提供することが多いです。ヘッダーファイルの指定さえ行えばソケットを利 用したプログラムが書けるようになります。(コラム終わり)

コラム:ソケットアドレス構造体

sockaddr構造体、 in_addr構造体、 sockaddr_in構造体の相違、

ステップ2:ソケットの接続

ソケットが無事にできたら、connect()システムコールを利用して、 サーバと接続します。プログラムリストでは次の部分で接続を行います。



  struct sockaddr_in client_addr;



  /* 略 */



  bzero((char *)&client_addr, sizeof(client_addr));

  client_addr.sin_family = PF_INET;

  client_addr.sin_addr.s_addr = inet_addr(argv[1]);

  client_addr.sin_port = htons(8000);



  /* ソケットをサーバに接続 */

  if (connect(sockfd, (struct sockaddr *)&client_addr, 

	      sizeof(client_addr)) < 0) {

    /* 接続失敗時の処理 */

  }


コラム:ネットワークバイト順序

すべてのコンピュータが複数のバイトからなるデータを同じ順序で格納すると は限りません。例えば、4バイトつまり32ビットの整数データを考えてみてく ださい。【作成中】ビックエンディアンとリトルエンディアンの話を書く。

システムコール:connect()

connect()は引数として与えた情報をもとにしてサーバに仮想回線を 接続するシステムコールです。


int connect(int socket, struct scokaddr *destaddr, int destlen)

第一引数socketにはsocket()システムコールが返したソケッ ト番号をいれます。第二引数のdestaddrは接続先のサーバを指定す るsockaddr構造体へのポインタです。ただし、インターネットの通 信ではsockaddr構造体ではなく、sockaddr_in構造体を sockaddr構造体にキャストしてから与えます。 sockaddr_inは接続先のサーバの名前やポート番号を定義する構造体 で、ヘッダーファイル<netinet/in.h>で以下のように定義されている。 ください。


struct sockaddr_in { 

    short sin_family; 

    unsigned short sin_port; 

    struct in_addr sin_addr; 

    char sin_zero[8];

};

sockaddr_in構造体を利用するときは、前もって構造体の中身を 0に埋めなければいけません。これにはbzero(char *src, int bytes)関数を利用します。bzero()関数は第一引数のデータ構 造を第二引数分で与えた数分のバイトだけ0に埋めるものです。

sockaddr_in構造体は、メンバ変数のsin_familyはプロトコルファミリ として、socket()のときと同じようにPF_INETとしてくだ さい。メンバ変数のsin_portは通信に利用するポート番号を与えます。ただし、 ポート番号はhtons()関数を使って与えるようにしてください。 ここではポート番号を8000にしています。


client_addr.sin_addr.s_addr = htons(8000);

sockaddr_in構造体のメンバ変数のsin_addrin_addr構造 体であり、ヘッダーファイル<netinet/in.h>で定義されます。


struct in_addr { 

  unsigned long s_addr;

};

in_addr構造体のsin_addrにはサーバのアドレスを書き込 みますが、その前にinet_addr()関数でアドレスの表記方法を変換し ます。


unsigned long inet_addr(char *address)

インターネットのアドレスは192.12.345.6のようにドットで区切って記述しま す。これは人間に読みやすい形式にしたものであり、実際のインターネットア ドレスは32ビットの数値で表されます。inet_addr()関数は引数とし て与えられたドット表記のアドレスを32ビット形式の数値に変換します。

ステップ3:データ送信・受信

ソケットの接続ができれば、サーバとのデータ送信や受信ができるようになり ます。送信にはファイル出力に使うwrite()システムコールを、受信 にはファイル入力に使うread()を使って実現できます。

システムコール:read()


int read(int scoket, char *buf, int length)

第一引数socketの返値であるソケット番号です。第二引数 bufは受信したデータを格納するための領域であり、第三引数 lengthは一回のread()の実行で受信するデータのバイトに なります。このため、第二引数buflengthバイト以上の 領域でなければいけません。システムコールread()の返値は実際に 受信したデータのバイト数となります。ここで、返値は第三引数 lengthより小さくなることに注意してください。これは第三引数 lengthバイト分を受信しようとしたのに、サーバ側からの送信デー タ量はlengthより小さく、返値分のバイト数しかなかった場合です。 なお、受信に失敗した場合は-1を返します。

システムコール:write()

次に write()システムコールについて説明します。 write()はファイルへの出力をする システムコールであり、通信ではソケットへの送信のために使われます。


int write(int scoket, char *buf, int length)

第一引数socketはソケット番号です。第二引数bufは送信 すべきデータを格納してある領域であり、第三引数lengthは送信デー タのバイト数を与えます。なお、第二引数buf領域サイズは lengthより大きくなければいけません。ステムコール write()の返値は実際に送信できたデータのバイト数となります。こ こで、返値は第三引数lengthより小さくなることに注意してくださ い。これは第三引数lengthバイト分を送信しようとしたのに、サー バ側がlength以下のバイト数しか受信しなかった場合です。なお、 送信に失敗した場合は-1を返します。

プログラムリストではsend_input(int sockfd)においてキーボー ドから入力された文字をサーバに送信します。



void send_input(int sockfd) {

  char buf[128]; 

  int buf_len;

  while(1){

    buf_len = read(0, buf, 1);

    write(sockfd, buf, buf_len);

  }

}


ステップ4:データ送信・受信

ファイル入出力では入出力処理が終了したらオープンしたファイルを クローズします。これと同様に、ソケットも通信が終了したらクローズ しなければいけません。

システムコール:write()

ソケットのクローズはファイルのクローズ と同じシステムコール、つまりclose()を利用します。


int close(int sockfd)

引数はクローズするソケットの番号を指定します。 クローズに失敗した場合は-1を返します。

コラム:バイト順序

htons()関数はバイト順序(の解説は未完)を変換する。

関数機能
unsigned short htons(unsigned short) コンピュータに依存した形式のshort型の整数を ネットワーク共通の形式のshort型の整数に変換
unsigned long htons(unsigned long) コンピュータに依存した形式のlong型の整数を ネットワーク共通の形式のlong型の整数に変換
unsigned short ntons(unsigned short) ネットワーク共通の形式のshort型の整数を コンピュータに依存した形式のshort型の整数に変換
unsigned long ntons(unsigned long) ネットワーク共通の形式のlong型の整数を コンピュータに依存した形式のlong型の整数に変換
(コラム終わり)

コラム:バイト操作

ネットワークにおける文字列の終わりは、C言語の文字列の終わりとは 一致するとは限りません。このため、通常の文字列関数では

関数機能
bcopy(char *src, char *dst, int bytes) bytesバイト分の srcの内容を dstにコピーする。
bzero()ゼロで埋める
bcmp()
(コラム終わり)

コラム:プロトコルファミリー

プロトコルファミリーは、これはサーバアドレスが与えられたときにどのよう に解釈するかを決めるものです。ソケットという概念を利用する通信は、イン ターネットなどのTCP/IP以外にも利用され、その種類によってアドレスの指定・ 解釈方法が違うのです。