プロセス間通信(1)/クライアント側

システム・プログラム

                                       電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/syspro-2002/2002-05-27
あるいは、次のページから手繰っていくこともできます。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/
http://www.is.tsukuba.ac.jp/~yas/index-j.html
先週のプリントに修正がある。

■今日の重要な話

■捕捉

◆プリント、WWWの資料

WWW上の資料やそれを印刷したものは、不完全である。次のような情報が抜け 落ちている。

あとでまとめて理解するより、授業中に理解するようにする。

◆教官、TA、回りの友だちの使い方

教官、TA、回りの友だちをうまく使って、自分の技を高めるとよい。 一人でプリントを見るより、100倍くらい効率がよい。 電子メールで質問するより、10倍くらい効率がよい。

■TCP/IPの基本的な考え方

インターネット上のアプリケーションの多くは、TCP/IPという仕組みを用いて 通信を行っている。

◆ストリーム

TCP/IPは、信頼性のある(reliable)双方向のストリーム転送サービス (stream transport service)を提供する通信プロトコルである(図1)。ス トリームは、次のような性質がある転送サービスである。

UNIXのパイプは、双方向ではなく単 方向であるが、同じストリームに分類される転送サービスを提供するものであ る。

図1 TCP/IPによりより提供されるストリーム
図1 TCP/IPによりより提供されるストリーム

なお、C言語のライブラリ関数である fopen(), fgets(), fputs() なども、 ストリームと呼ばれることがある。これは、もともとランダム・アクセス可能 で、メモリ中の配列と同じようにアクセスするすることもできるファイルを、 まるでプロセス間通信のストリームと同じように扱うことができることにも 関係している。

◆層(プロトコル・スタック)

TCP/IPによる通信では、図2に示すように、4つのプロトコル(規約、約束事) の層が使われる。TCP/IP自身は、TCP層と IP層という2つのプロトコルに分解 される。このようにさまざまなプロトコルが決められ、全体として層をなして いる。この様子を、プロトコル・スタックと呼ぶ。

図2 TCP/IPにおけるプロトコル・スタック

図2 TCP/IPにおけるプロトコル・スタック

TCP は、IP という通信プロトコルを利用して実現されている。IPは、(信頼 性がない)データグラム(datagram)転送サービスを提供する通信プロトコル である。データグラムとは、次のような性質を持つ。

データグラムは、書留めではない郵便に似ている。 データグラムは、 ぱけっと と呼ばれることもある。(パケットという言葉は、データの送り手と受けての 間に結合が作られる作られる時にも使われることがある。データグラムは、結 合が作られない通信で使われるひと固まりのデータで、電報(telegram)との類 推から名前が付けらた。)

IPのデータグラムが配達されるときに使われる番号が、 IPアドレス である。IPアドレスとしては、現在、32ビットの整数が 使われてる。IPアドレスを表現する時には、普通、8ビットずつ区切って、 次のように4つ部分に分けて書かれる。たとえば、


12.34.56.78
は、

(((12 * 256)+34)*256+56)*256+78
という数を表する。

TCPで通信をする時に、通信相手を識別するにはIPアドレスと ポート番号(port number) が必要になる。ポート番号は、同じホストの中で提供されている様々なサービスを 区別するために使われる。 ポート番号は、16ビットの整数であり、よく使われる アプリケーション では、あらかじめどの番号を使うかが決められている。これを well-knownポート番号(well-known port number) という。Unix では、1024 番より小さいポート番号を使うには、 スーパー・ユーザ(後述)の権限が必 要であり、このようなポート番号は、 特権ポート番号(privileged port number) と呼ばれる。

TCP層の上には、応用層が定義されている。この層では、ftp, rlogin, WWW, mnews,sendmail などの、TCP/IP を利用するプログラムの間の会話の方法が定 義される。TCP/IPを使った通信は、まるでプロセス同士が電話で会話するよう に進められる。普通の電話では、日本語を話す人と英語を話す人は、電話で情 報交換を行うことができない。同様に、同じTCP/IPを使っていても、会話の方 法が違うと、まったく情報交換を行うことができない。ゆえに、TCP/IPの上に さらに、情報交換のためにさまざまなプロトコルが取り決められている。

TCP/IPの上に構築されているプロトコルの例を、表1に示す。

表1 TCP/IPの上に構築されているプロトコルの例

--------------------------------------------------------------------
ポート番号	プロトコルの名前	目的
--------------------------------------------------------------------
21	FTP(File Transfer Protocol)		ファイル転送
22	SSH (Secure Shell)			暗号通信路によるログイン
23	Telnet					遠隔ログイン(telnet)
25	SMTP(Simple Mail Transfer Protocol)	電子メールの転送
79	finger					fingerコマンド
80	HTTP(HyperText Transfer Protocol)	WWWのデータ転送
119	NNTP(Network News Transfer Protocol)	ネットワーク・ニュース
						の記事の転送
513	login	遠隔ログイン(rlogin)
--------------------------------------------------------------------

/etc/services に、他のポート番号が掲載されている。

IPのデータグラムを転送するためには、さまざまな物理的な媒体が使われる。 現在LANでは、イーサネットやFDDIがよく使われいる。イーサネットは、同軸 ケーブルやより対線(Twisted Pair Cable)を使ってデータを転送する。FDDIは、 光ケーブルを使っている。モデムなどを使ったシリアル回線では、PPP(Point to Point Protocol)というプロトコルの上に、IPデータグラムが流される。

データグラムは、ネットワーク通信では、最も基本的な転送サービスである。 IP上に構築された UDP(User Datagram Protocol)も、IPとほとんど同じ機能 を提供する。また、イーサネットやFDDIが提供する転送サービスも、データグ ラムである。

◆ホストとルータ

ネットワークに接続されている計算機の中で、ネットワークに1ヵ所の出入り 口(インタフェース)を持っているものは、ホストと呼ばれる。2ヵ所以上の 出入り口を持っている計算機は、ルータと呼ばれる。ルータは、ネットワーク とネットワークを接続するための計算機である。ルータは、入ってきたIPのパ ケットのIPアドレスを見て、どのネットワークに送ればよいかを判断する。

図1で、左端と右端にあり、4層全てそろっている部分がホストである。 TCP/IPの通信は、ホストとホストの間で行われる。中央の、2層しかない部分 は、ルータである。ルータの仕事は、IP層において行われる。

◆通信路の開設

TCP/IP では、プロセスとプロセスが、電話で会話をするように通信が行われ る。普通の電話で人間同士が話をするには、まず電話番号を指定して、話相手 に電話をとってもらわなければならない。TCP/IP においても同様である。 TCP/IPでは、電話を掛ける方をクライアント・プロセス、電話を待つ方をサー バ・プロセスと言いう。

TCP/IPにおいて、プロセス間に形成されたストリーム通信路のことを、計算機 間に張られた物理的な回線に似ていることから、仮想的回線(virtual circuit)とも言う。TCP/IP では、回線を接続する段階では、クライアント・ プロセスとサーバ・プロセスは非対称である。一度仮想回線が接続された後は、 両方のプロセスは、TCP/IPのレベルでは、まったく対称的になる。

TCP/IPにおいてプロセス間に仮想回線を開設するには、IPアドレスとポート番 号が必要である。ポート番号は、同じIPアドレスを持つホスト上で動いている プロセスを区別するために使われる。

以下に、通信路が開設される手順を示す。

  1. サーバ・プロセスがポート番号を指定して、接続要求受付用ポートを作る。 サーバ・プロセスは、クライアント・プロセスからの接続要求を待つ(図3 (a))。(注意:要求受付用ポートでは、データの送受信はできない。)
  2. クライアント・プロセスが通信用ポートを作る。このポートを、サーバ・ プロセスが動いているホストのIPアドレスと、サーバ・プロセスが作った接 続要求受付用ポートのポート番号を使って、接続要求を行う(図3(b))。
  3. 接続要求が受け付けられると、サーバ・プロセスには、新たに通信用ポー トが作られる(図3(c))。これは、特定のクライアントとの通信のために 使われる。

こうして一度通信路が開設されると、クライアントとサーバは、どちらからで もデータを送り始めることができる。

図3(a) TCP/IP通信路の開設(1)

図3(a) TCP/IP通信路の開設(1)

図3(b) TCP/IP通信路の開設(2)

図3(b) TCP/IP通信路の開設(2)

図3(c) TCP/IP通信路の開設(3)

図3(c) TCP/IP通信路の開設(3)

TCP/IPにおける通信路開設において、クライアントは、サーバ側の接続要求受 付用ポートのポート番号を、事前に知っている必要がある。表1に、いくつか の応用層のプロトコルについて、公に利用目的が決められているポート番号を 示す。

クライアント側の通信用ポートのポート番号は、通常は、オペレーティング・ システムにより自動的に割り当てられる。サーバ側の通信用ポートのポート番 号も、同様である。

◆プロセス間通信におけるクライアント・サーバ・モデル

プロセス間通信は、本来自由に行うことができる。どのプロセスも自由にメッ セージを送信する権利がある。プロセス間通信におけるクライアント・サーバ・ モデルは、本来対称的なプロセスを最初にメッセージを送る方(クライアント・ プロセス)と受ける方(サーバ・プロセス)に分類することで、プロセス間通 信を構造化し、わかりやすくするものである。TCP/IPの通信路開設時における クライアントとサーバの役割は、このプロセス間通信におけるクライアント・ サーバ・モデルの1つの例になっている。

プロセス間通信におけるクライアント・サーバ・モデルにおける意味の他に、 クライアントとサーバという言葉は、サービスを受けるプロセスとサービスを 提供するプロセスの意味で使われることがある。インターネットにおけるプロ セス間通信では、多くの場合、サービスの授受の関係におけるクライアントと サーバと、プロセス間通信におけるクライアントとサーバが一致している(稀 に一致していないこともあるので、注意しなさい)。

◆ソケット

UNIX オペレーティング・システム上で動作するプログラムがTCP/IPの機能を 使う場合、UNIXオペレーティング・システムが提供するソケットというインタ フェースを通じて利用することになる。ソケットは、TCP/IP をはじめとして、 XNS, OSI などさまざまな通信プロトコルを UNIX オペレーティング・システ ム上で使うために設計されたものである。TCP/IP だけを考えると、ソケット のインタフェースは、繁雑であり、使いにくくなっている。

UNIXでは、ソケットをドメイン(Protocol Family)と型で区別する。 下の表は、socket() システム・コールに与えるドメインと型である。

----------------------------------------------------------------------
ドメイン 型		option	プロトコル
----------------------------------------------------------------------
PF_INET	SOCK_STREAM	0	TCP
PF_INET	SOCK_DGRAM	0	UDP
PF_INET	SOCK_RAW	?	IP,ICMPなど
PF_UNIX	SOCK_STREAM	0	同一ホスト内(UNIXドメイン)のストリーム
PF_UNIX	SOCK_DGRAM	0	同一ホスト内(UNIXドメイン)のデータグラム
PF_NS	SOCK_SEQPACKET	?	XEROX NS protocol の順序付きパケット
PF_NS	SOCK_RDM	?	XEROX NS protocol の信頼性のあるデータグラム
----------------------------------------------------------------------
これ以外の組み合わせ、使えない。たとえば、PF_INETとSOCK_SEQPACKET を socket システム・コールで指定しても、うまくいかない。

◆DNS(Domain Name Service)

TCP/IPによる通信は、通信相手のIPアドレス(32ビットの整数)とポート番号 (16ビットの整数)さえわかれば、可能である。IPアドレスやポート番号は、 計算機にとって扱いやすいものであるが、人間にとって扱いやすいものではな い。人間にとってわかりやすい記号の名前から、IPアドレスに変換するサービ スがあれば便利である。このサービスを、名前サービス、それを行うプログラ ムを名前サーバという。

インターネットにおける名前サービスは、名前空間をドメイン(領域)に分割 して、階層的に管理することで実現されている。これを、ドメイン・ネーム・ サービス(Domain Name Service, DNS)という。DNSという言葉は、名前サー ビスを提供するプログラム(名前サーバ, Domain Name Server)を意味するこ ともある。

DNSでは、主に名前をIPアドレスへ変換するサービスが使われている。その他 に、名前から電子メールの配送先、名前から名前サーバが動いているホストの 名前、名前から名前サーバ自身の管理情報、逆にIPアドレスから名前を引くた めにも使われる。

■TCP/IP の汎用クライアント・プログラムとしての telnet コマンド

◆ telnet コマンドの使い方

telnet コマンドは、通常、次のように使われる。
--------------------------------------------------------------------
% telnet host1 [←]
--------------------------------------------------------------------
これは、次の省略形である。
--------------------------------------------------------------------
% telnet host1 telnet [←]
--------------------------------------------------------------------
ここで、第2引数の "telnet" という文字列は、TCP/IP のポート番号を示す 記号である。この記号は、/etc/services というファイルに次のように格納さ れている。
--------------------------------------------------------------------
telnet		23/tcp
--------------------------------------------------------------------

telnet コマンドは、/etc/services ファイルを検索し、与えられた記号から ポート番号(この例では23)を得る。また、host1 の IP アドレスを、 /etc/hosts や DNS から得る。telnet コマンドは、この IP アドレスとポー ト番号の2つを使って、TCP/IP の通信路を開設する。(注意:NISが動いてい る場合には、/etc/services や /etc/hosts の代わりに、NISのデータベース (マップ)が検索される。)

telnet コマンドでは、次のように、ホストのIPアドレスとポート番号を数字 で打ち込むこともできる。

--------------------------------------------------------------------
% telnet a.b.c.d 23 [←]
--------------------------------------------------------------------
ここで、a.b.c.d とは、32ビットのIPアドレスを8ビットずつに区切り、 それぞれの8ビットの整数を10進数で表記したものである。よって、実際のIP アドレスは、次のようしてに計算できる。
((((a*256)+b)*256)+c)*256+d == XXXXXXXXXX
よって、telnet コマンドに「.」を含まない10進数次のようにIPアドレスを 与えてもよい。
--------------------------------------------------------------------
% telnet XXXXXXXXXX 23  [←]
--------------------------------------------------------------------

◆ telnet コマンドの使い方(^])

^] を打つと、ローカルの telnet コマンドを制御することができる。 ここで、quit などのコマンドが使える。
--------------------------------------------------------------------
% telnet localhost echo [←]
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello[←]
hello
exit[←]
exit
quit[←]
quit
aaa[←]
aaa
^]
telnet> quit[←]
Connection closed.
% []
--------------------------------------------------------------------
注意:セキュリティ上の理由から、echo などの、システムプログラムの講義 くらいでしか役に立たないようなサービスを停止することが、最近では一般的 である。

■echoサービスのクライアント

TCP/IP のポート番号 7 では、送られてきた文字をそのまま返すサービスを提 供している。

以下は、telnet を使って、adonis9.coins.tsukuba.ac.jp 上で動作している echo サーバを利用している(adonis9 上のサービスは、情報学類内のコンピュー タからしかつながらないような設定をしている)。


----------------------------------------------------------------------
% egrep echo /etc/services [←]
echo            7/tcp
echo            7/udp
at-echo         204/tcp                         # AppleTalk echo
at-echo         204/udp
echo            4/ddp                           # AppleTalk Echo Protocol
% telnet adonis9.coins.tsukuba.ac.jp 7 [←]
Trying 130.158.86.29...
Connected to adonis9.coins.tsukuba.ac.jp.
Escape character is '^]'.
hello[←]
hello
aaa[←]
aaa
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------

◆echo-client.c

以下のプログラムは、echo サービスを利用するクライアントである。実行例 を先に示す。

----------------------------------------------------------------------
% mkdir ipc [←]
% cd ipc [←]
% cp ~yas/syspro/ipc/echo-client.c . [←]
% make echo-client [←]
cc     echo-client.c   -o echo-client
% ./echo-client [←]
Usage: ./echo-client host port str1 ...
% ./echo-client adonis7.coins.tsukuba.ac.jp 7 hello world. [←]
sending: 6 bytes [hello
]...
waiting for reply: ...
received: 6 bytes [hello
]
sending: 7 bytes [world.
]...
waiting for reply: ...
received: 7 bytes [world.
]
% []
----------------------------------------------------------------------
このプログラムは、コマンドラインから3つ以上の引数をとる。第1引数で指 定されたホスト上の、第2引数で指定されたポートで動作しているサーバに接 続する。そして、第3引数以降に指定された文字列を送る。echo サービスは、 同じものを送り返してくるので、それを受け取る。

TCP/IPのクライアント側のプログラムで大事な標準のシステムコールとライブ ラリ関数は、次の通りである。

その他に、tcp_connect() が出てくる。これは、新城が講義用に作ったもので ある。演習課題を解く時に、これを使ってもよい。


----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        echo-client.c -- 文字列を送受信するクライアント(TCP/IP版)
   4:	        ~yas/syspro/ipc/echo-client.c
   5:	        Start: 1998/06/01 23:48:29
   6:	*/
   7:	#include <stdio.h>
   8:	#include <sys/types.h>  /* socket() */
   9:	#include <sys/socket.h> /* socket() */
  10:	#include <netinet/in.h> /* struct sockaddr_in */
  11:	#include <netdb.h>      /* gethostbyname() */
  12:	
  13:	extern  int echo_client( char *server, int portno, int nstr, char *strv[] );
  14:	extern  ssize_t writen(int fd, const void *vptr, size_t n);
  15:	extern  ssize_t readline(int fd, void *vptr, size_t maxlen);
  16:	
  17:	main( int argc, char *argv[] )
  18:	{
  19:	     char *server ;
  20:	     int portno ;
  21:	        if( argc < 3 )
  22:	        {
  23:	            fprintf( stdout,"Usage: %s host port str1 ...\n",argv[0] );
  24:	            exit( -1 );
  25:	        }
  26:	        server = argv[1] ;
  27:	        portno = atoi( argv[2] );
  28:	        echo_client( server, portno, argc-3, &argv[3] );
  29:	}
----------------------------------------------------------------------

main() 関数は、コマンドラインの引数を調べて、echo_client() を読んでい る。atoi() で、文字列で与えられた数を、int に変換している。

----------------------------------------------------------------------
  30:	
  31:	#define BUFFERSIZE      1024
  32:	
  33:	int
  34:	echo_client( char *server, int portno, int nstr, char *strv[] )
  35:	{
  36:	    int sock ;
  37:	    int rcount ;
  38:	    int slen ;
  39:	    char sbuf[BUFFERSIZE];
  40:	    char rbuf[BUFFERSIZE];
  41:	    int i ;
  42:	
  43:	        sock = tcp_connect( server, portno );
  44:	        if( sock<0 )
  45:	            exit( -1 );
  46:	
  47:	        for( i=0 ; i<nstr ; i++ )
  48:	        {
  49:	            snprintf( sbuf,BUFFERSIZE,"%s\n",strv[i] );/* '\n' is important */
  50:	            slen = strlen( sbuf );
  51:	            printf("sending: %d bytes [%s]...\n",slen,sbuf );
  52:	            if( writen( sock, sbuf, slen ) != slen )
  53:	            {
  54:	                perror("write");
  55:	                exit( 1 );
  56:	            }
  57:	            printf("waiting for reply: ...\n" );
  58:	            if( (rcount = readline( sock, rbuf, BUFFERSIZE ))<0 )
  59:	            {
  60:	                perror("read");
  61:	                exit( 1 );
  62:	            }
  63:	            printf("received: %d bytes [%s]\n",rcount,rbuf );
  64:	        }
  65:	
  66:	        close( sock );
  67:	}
----------------------------------------------------------------------

echo_client() では、tcp_connect() という関数を呼び出している。この結果、 サーバとの間に TCP/IP通信路の開設され、通信可能なファイル記述子が返さ れる。このファイル記述子は、標準入出力(0,1,2)や open() システム・コー ルの結果と同じもので、不要になったら close() で解放する。それをwrite() システム・コールに与えると、データが送ることができる。read() システム・ コールで受けとることができる。

TCP/IP の通信では、行単位(最後に\n)でデータを送受信することが多い。 echo サービスでは、1行送り、1行受け取る。他のサービスでは、1行送っ て複数行受け取ったり、受け取る方では行の概念がなくなるもの(HTTPで画像 データを受け取る場合など)もある。

このプログラムでは、write() システムコールの変りに writen() 関数(ソー ス・プログラムは同じファイルの下の方にある)を使っている。TCP/IP の通 信では、write(fd,buf,100) としても、100バイト送られずに、50 バイトしか 送られないことがある。残りの 50 バイトも送る必要があれば、ループして全 部送るようにする。システム・コール write() は、今は送る必要がない(後 で送ってもよい)場合、送らなくてもよい場合にも対応できるようになってい る。しかし、一般には送る方は全部送り終わるまでループして待った方がよい 場合が多い。writen() は、このような目的のための関数である。

データを読込む時には、readline() を使っている。これは、\n 記号が現れる までを一区切りとして、読込むものである。ただしバッファサイズ以上は、 読込まない。最後に終端の 0 を付けてくれる。(read() システムコールでは、 付けてくれない。)


----------------------------------------------------------------------
  68:	
  69:	/* 新城、システムプログラム講義用 */
  70:	
  71:	int tcp_connect( char *server, int portno )
  72:	{
  73:	    struct hostent *hostent ;
  74:	    struct sockaddr_in addr ;
  75:	    int addr_len ;
  76:	    int s ;
  77:	
  78:	        if( (s = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
  79:	        {
  80:	            perror("socket");
  81:	            return( -1 );
  82:	        }
  83:	
  84:	        memset( &addr, 0, sizeof(addr) );
  85:	        addr.sin_family = AF_INET ;
  86:	        addr.sin_port = htons( portno );
  87:	        if( (hostent = gethostbyname( server )) == NULL )
  88:	        {
  89:	            fprintf(stderr,"unknown host %s\n",server );
  90:	            return( -1 );
  91:	        }
  92:	        memcpy( &addr.sin_addr, hostent->h_addr, hostent->h_length );
  93:	
  94:	        if( connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0 )
  95:	        {
  96:	            perror( server );
  97:	            close( s );
  98:	            return( -1 );
  99:	        }
 100:	        return( s );
 101:	}
----------------------------------------------------------------------

tcp_connect() は、通信路の開 設の仕事のうち、クライアント側の仕事をする関数である。まず、クライア ント側のソケットを、socket() システムコールで作成している。 PF_INET と SOCK_STREAMの組み合わせなので、 TCP を使うことを意味する。

socket() の引数で、PF_INET の変りに、AF_INET と書いてもよい。ここでは、 Protocol を選んでいるので、PF_ が正しいが、実際には、PF_INET と AF_INET は同じであり、また、多くのテキストで混在されて使われいる。

ソケットが作成できたら、connect() システムコールで接続する。その第2引 数と第3引数で、接続先のアドレスを指定しなければならない。まずそのため の準備を行う。

TCP/IP の場合、アドレスの指定には、sockaddr_in 構造体を使う。connect() システムコールのマニュアルには、sockaddr 構造体を使うようにと書かれて いるが、TCP/IP では、そのサブクラス(オブジェクト指向用語)である sockaddr_in を使う。それには、 IP アドレスとポート番号が必要である。 その他に、先頭に、sockaddr_in であることを示す定数 AF_INET を置く。

IP アドレスは、文字列で与えられた接続先のサーバのホスト名からライブラ リ関数 gethostbyname() を使って調べている。

     struct hostent *gethostbyname(const char *name);

     struct  hostent {
             char    *h_name;        /* official name of host */
             char    **h_aliases;    /* alias list */
             int     h_addrtype;     /* host address type */
             int     h_length;       /* length of address */
             char    **h_addr_list;  /* list of addresses from name server */
     };
     #define h_addr  h_addr_list[0]  /* address, for backward compatibility */
このライブラリ関数は、結果として hostent 構造体を返す。この中の hostent->h_addr という番地に、IP アドレスが含まれている。(複数の IP ア ドレスがある場合もある。) そこから hostent->h_length バイトだけコピー する。IPv4 では、4 バイトである。

ライブラリ関数 gethostbyname() の内部的では、/etc/hosts というファイル や, NIS の hosts マップ, DNSの名前サーバなどが調べられる。どれがどの順 番で調べられるかは、システムに依存する。Solaris や Linux では、 /etc/nsswitch.conf の hosts: の行に書かれている。

ポート番号は、引数で与えられたものをそのまま使う。ただし、「ネットワー ク・バイトオーダ」に変換する必要がある。ポート番号は、16 ビットなので、 htons() ライブラリ関数(host to network, short)を使っている。他に、 htonl(), ntohl(), ntohs() がある。

IP アドレスもネットワーク・バイトオーダになっているが、これはhostent 構造体に入った段階では既にそうなっている。

ポート番号として、記号表現のものを /etc/services ファイルを検索して調 べるには、getservbyname() ライブラリ関数を使う方法がある。


----------------------------------------------------------------------
 102:	
 103:	/* 
 104:	W.リチャード・スティーブンス著、篠田陽一訳:
 105:	"UNIXネットワークプログラミング第2版 Vol.1 ネットワークAPI:ソケットとXTI",
 106:	ピアソン・エデュケーション, 1999年. ISBN 4-98471-205-9
 107:	  3.9節 readn, writen, および readline 関数 (p.76)
 108:	
 109:	Richard Stevens: "UNIX Network Programming, Volume 1, Second Edition:
 110:	Networking APIs: Sockets and XTI", Prentice Hall, 1998.
 111:	ISBN 0-13-490012-X.  
 112:	    Section 3.9 readn, writen, and readline Functions (p.77)
 113:	
 114:	http://www.kohala.com/start/ (http://www.kohala.com/~rstevens/)
 115:	http://www.kohala.com/start/unpv12e/unpv12e.tar.gz
 116:	
 117:	*/
 118:	
 119:	/* include writen */
 120:	/*#include      "unp.h"*/
 121:	#include <errno.h>
 122:	
 123:	ssize_t                 /* Write "n" bytes to a descriptor. */
 124:	writen(int fd, const void *vptr, size_t n)
 125:	{
 126:	        size_t          nleft;
 127:	        ssize_t         nwritten;
 128:	        const char      *ptr;
 129:	
 130:	        ptr = vptr;
 131:	        nleft = n;
 132:	        while (nleft > 0) {
 133:	                if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
 134:	                        if (errno == EINTR)
 135:	                                nwritten = 0;           /* and call write() again */
 136:	                        else
 137:	                                return(-1);                     /* error */
 138:	                }
 139:	
 140:	                nleft -= nwritten;
 141:	                ptr   += nwritten;
 142:	        }
 143:	        return(n);
 144:	}
 145:	
----------------------------------------------------------------------

writen() は、write() システム・コールの代りに使うとよい。説明は、 echo_client() の所にある。

----------------------------------------------------------------------
 146:	/* include readline */
 147:	/*#include      "unp.h"*/
 148:	
 149:	#define MAXLINE 4096
 150:	
 151:	static ssize_t
 152:	my_read(int fd, char *ptr)
 153:	{
 154:	        static int      read_cnt = 0;
 155:	        static char     *read_ptr;
 156:	        static char     read_buf[MAXLINE];
 157:	
 158:	        if (read_cnt <= 0) {
 159:	again:
 160:	                if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
 161:	                        if (errno == EINTR)
 162:	                                goto again;
 163:	                        return(-1);
 164:	                } else if (read_cnt == 0)
 165:	                        return(0);
 166:	                read_ptr = read_buf;
 167:	        }
 168:	
 169:	        read_cnt--;
 170:	        *ptr = *read_ptr++;
 171:	        return(1);
 172:	}
 173:	
 174:	ssize_t
 175:	readline(int fd, void *vptr, size_t maxlen)
 176:	{
 177:	        int             n, rc;
 178:	        char    c, *ptr;
 179:	
 180:	        ptr = vptr;
 181:	        for (n = 1; n < maxlen; n++) {
 182:	                if ( (rc = my_read(fd, &c)) == 1) {
 183:	                        *ptr++ = c;
 184:	                        if (c == '\n')
 185:	                                break;  /* newline is stored, like fgets() */
 186:	                } else if (rc == 0) {
 187:	                        if (n == 1)
 188:	                                return(0);      /* EOF, no data read */
 189:	                        else
 190:	                                break;          /* EOF, some data was read */
 191:	                } else
 192:	                        return(-1);             /* error, errno set by read() */
 193:	        }
 194:	
 195:	        *ptr = 0;       /* null terminate like fgets() */
 196:	        return(n);
 197:	}
 198:	
----------------------------------------------------------------------

readline() は、\n 記号が現れるまでを一区切りとして、読込む。これは、 read() システムコールの代りには使えない。fgets() と同様に、最後に終端 の 0 を付けてくれる。(read() システムコールでは、付けてくれない。)

上のプログラムは、複雑である。意味としては、下のプログラムとだいたい同 じである。ただし、下のプログラムは、非常に遅い。


unpv12e/test/readline1.c:
----------------------------------------------------------------------
   1:	/* include readline */
   2:	#include        "unp.h"
   3:	
   4:	ssize_t
   5:	readline(int fd, void *vptr, size_t maxlen)
   6:	{
   7:	        ssize_t n, rc;
   8:	        char    c, *ptr;
   9:	
  10:	        ptr = vptr;
  11:	        for (n = 1; n < maxlen; n++) {
  12:	again:
  13:	                if ( (rc = read(fd, &c, 1)) == 1) {
  14:	                        *ptr++ = c;
  15:	                        if (c == '\n')
  16:	                                break;  /* newline is stored, like fgets() */
  17:	                } else if (rc == 0) {
  18:	                        if (n == 1)
  19:	                                return(0);      /* EOF, no data read */
  20:	                        else
  21:	                                break;          /* EOF, some data was read */
  22:	                } else {
  23:	                        if (errno == EINTR)
  24:	                                goto again;
  25:	                        return(-1);             /* error, errno set by read() */
  26:	                }
  27:	        }
  28:	
  29:	        *ptr = 0;       /* null terminate like fgets() */
  30:	        return(n);
  31:	}
...
----------------------------------------------------------------------

高速版redline() は、複数の TCP/IP のストリームを扱うと問題がある。また、 マルチスレッドのプログラムでは動作しない。上記の教科書では、マルチスレッ ドで動作するプログラムも示してある。

■練習問題と課題

★練習問題 37 getservbyname()の利用

上の echo-client.c を、getservbyname() を 使うように書き換えなさい。

★練習問題 38 TCP/IPクライアントの作成

次の通信プロトコルのクライアントを1つ以上作りなさい。(以下の項目には、 各プロトコルを説明したページへのリンクが埋め込まれている。)

まず、telnet で、これらのサーバに接続しなさい。そして、それぞれのプロ トコルに従って、要求を打ち込み、どのような結果が返ってくるかを調べなさ い。

次に、telnet で行った要求の送信と結果の受信を行うようなプログラムを 作りなさい。このとき、必要なパラメタは、main() の引数から取りなさい。

注意。それぞれのプロトコルでは、接続先として次のホストを使いなさい。

HTTP
www (www.coins.tsukuba.ac.jp)
NNTP
orchid-c ($NNTPSERVER)
SMTP
orchid-a
finger
adonis1-adonis45, azalea1-azalea45, localhost

プログラムをつくる時には、行末の扱い(CR-LF)に注意しないさい。

HTTP, finger, SMTP, NNTP など、インターネットで使われている通信プロト コルの多くでは、行末を表す記号として、キャリッジ・リターン(carrige return,CR)とライン・フィード(Line feed, LF)の両方が必要であると定めら れている。Unix では、通常ライン・フィード(ニュー・ライン、New Line, NL と呼ばれることもある)だけが行末の記号として使われる。よって、画面 に文字列を表示し、改行したい場合は、次のようなプログラムが使われる。

printf("Hello,world\n");
しかしながら、インターネットの上のプログラムを作成する時には、次のよう にしなければならないことが多い。
printf("Hello,world\r\n");
ここで、'\r' がキャリッジ・リターン、'\n' がライン・フィードである。い ずれも、C言語のソース・プログラム上では2文字に見えるが、Cコンパイラ により、1文字に変換される。それぞれ、アスキーでは、13(0x0d), 10(0x0a) である。

規格上は、CR-LF が必要とされているが、実際にプログラムを作る時には、LF だけでも動くことがある。しかし、それはたまたまその特定のサーバのプログ ラムが良くできていて、LF だけでも動くようになっているからである。

★練習問題 39 ファイルや標準入力からデータを読み込む echo-client

上の echo-client.c を書き換えて、ファイル、または、 標準入力からデータを読み込むようにしなさい。