システム・プログラム 電子・情報工学系 新城 靖 <yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~yas/classes/syspro-2003/2003-05-19
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~yas/
http://www.is.tsukuba.ac.jp/~yas/index-j.html
先週のプリントに修正がある。
インターネット上のアプリケーションの多くは、TCP/IPという仕組みを用いて 通信を行っている。
TCP/IPは、信頼性のある(reliable)双方向のストリーム転送サービス (stream transport service)を提供する通信プロトコルである(図1)。ス トリームは、次のような性質がある転送サービスである。
なお、C言語のライブラリ関数である fopen(), fgets(), fputs() なども、 ストリームと呼ばれることがある。これは、もともとランダム・アクセス可能 で、メモリ中の配列と同じようにアクセスするすることもできるファイルを、 まるでプロセス間通信のストリームと同じように扱うことができることにも 関係している。
TCP/IPによる通信では、図2に示すように、4つのプロトコル(規約、約束事) の層が使われる。TCP/IP自身は、TCP層と IP層という2つのプロトコルに分解 される。このようにさまざまなプロトコルが決められ、全体として層をなして いる。この様子を、プロトコル・スタックと呼ぶ。
TCP は、IP という通信プロトコルを利用して実現されている。IPは、(信頼 性がない)データグラム(datagram)転送サービスを提供する通信プロトコル である。データグラムとは、次のような性質を持つ。
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に示す。
/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アドレスを持つホスト上で動いている プロセスを区別するために使われる。
以下に、通信路が開設される手順を示す。
こうして一度通信路が開設されると、クライアントとサーバは、どちらからで もデータを送り始めることができる。
図3(a) TCP/IP通信路の開設(1)
図3(b) TCP/IP通信路の開設(2)
図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 システム・コールで指定しても、うまくいかない。
TCP/IPによる通信は、通信相手のIPアドレス(32ビットの整数)とポート番号 (16ビットの整数)さえわかれば、可能である。IPアドレスやポート番号は、 計算機にとって扱いやすいものであるが、人間にとって扱いやすいものではな い。人間にとってわかりやすい記号の名前から、IPアドレスに変換するサービ スがあれば便利である。このサービスを、名前サービス、それを行うプログラ ムを名前サーバという。
インターネットにおける名前サービスは、名前空間をドメイン(領域)に分割 して、階層的に管理することで実現されている。これを、ドメイン・ネーム・ サービス(Domain Name Service, DNS)という。DNSという言葉は、名前サー ビスを提供するプログラム(名前サーバ, Domain Name Server)を意味するこ ともある。
DNSでは、主に名前をIPアドレスへ変換するサービスが使われている。その他 に、名前から電子メールの配送先、名前から名前サーバが動いているホストの 名前、名前から名前サーバ自身の管理情報、逆にIPアドレスから名前を引くた めにも使われる。
これは、次の省略形である。-------------------------------------------------------------------- % telnet host1--------------------------------------------------------------------
ここで、第2引数の "telnet" という文字列は、TCP/IP のポート番号を示す 記号である。この記号は、/etc/services というファイルに次のように格納さ れている。-------------------------------------------------------------------- % telnet host1 telnet--------------------------------------------------------------------
-------------------------------------------------------------------- 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アドレスとポート番号を数字 で打ち込むこともできる。
ここで、a.b.c.d とは、32ビットのIPアドレスを8ビットずつに区切り、 それぞれの8ビットの整数を10進数で表記したものである。よって、実際のIP アドレスは、次のようしてに計算できる。-------------------------------------------------------------------- % telnet a.b.c.d 23--------------------------------------------------------------------
((((a*256)+b)*256)+c)*256+d == XXXXXXXXXXよって、telnet コマンドに「.」を含まない10進数次のようにIPアドレスを 与えてもよい。
-------------------------------------------------------------------- % telnet XXXXXXXXXX 23--------------------------------------------------------------------
注意:セキュリティ上の理由から、echo などの、システムプログラムの講義 くらいでしか役に立たないようなサービスを停止することが、最近では一般的 である。-------------------------------------------------------------------- % telnet localhost echoTrying 127.0.0.1... Connected to localhost. Escape character is '^]'. hello
hello exit
exit quit
quit aaa
aaa ^] telnet> quit
Connection closed. %
--------------------------------------------------------------------
以下は、telnet を使って、adonis9.coins.tsukuba.ac.jp 上で動作している echo サーバを利用している(adonis9 上のサービスは、情報学類内のコンピュー タからしかつながらないような設定をしている)。
---------------------------------------------------------------------- % egrep echo /etc/servicesecho 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. %
----------------------------------------------------------------------
このプログラムは、コマンドラインから2つ以上の引数をとる。第1引数で指 定されたホスト上の、第2引数で指定されたポートで動作しているサーバに接 続する。そして、キーボードから1行単位で入力し、そのサーバへ送る。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 % ./echo-client adonis7.coins.tsukuba.ac.jp 7
==> hello
sending: 6 bytes [hello ]... waiting for reply: ... received: 6 bytes [hello ] ==> world.
sending: 7 bytes [world. ]... waiting for reply: ... received: 7 bytes [world. ] ==> ^D %
----------------------------------------------------------------------
TCP/IPのクライアント側のプログラムで大事な標準のシステムコールとライブ ラリ関数は、次の通りである。
---------------------------------------------------------------------- 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> /* getaddrinfo() */ 12: 13: extern int echo_client( char *server, int portno ); 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\n",argv[0] ); 24: exit( -1 ); 25: } 26: server = argv[1] ; 27: portno = strtol( argv[2],0,10 ); 28: echo_client( server, portno ); 29: } ----------------------------------------------------------------------main() 関数は、コマンドラインの引数を調べて、echo_client() を読んでい る。strtol() で、引数で文字列として与えられた数を、int に変換している。
---------------------------------------------------------------------- 30: 31: #define BUFFERSIZE 1024 32: 33: int 34: echo_client( char *server, int portno ) 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: printf("==> "); fflush(stdout); 48: while( fgets(sbuf,BUFFERSIZE,stdin) ) 49: { 50: if( strchr(sbuf,'\n') == 0 ) 51: { 52: fprintf(stderr,"no \\n in the input line.\n"); 53: exit( 1 ); 54: } 55: slen = strlen( sbuf ); 56: printf("sending: %d bytes [%s]...\n",slen,sbuf ); 57: if( writen( sock, sbuf, slen ) != slen ) 58: { 59: perror("write"); 60: exit( 1 ); 61: } 62: printf("waiting for reply: ...\n" ); 63: if( (rcount = readline( sock, rbuf, BUFFERSIZE ))<0 ) 64: { 65: perror("read"); 66: exit( 1 ); 67: } 68: printf("received: %d bytes [%s]\n",rcount,rbuf ); 69: printf("==> "); fflush(stdout); 70: } 71: printf("\n"); 72: 73: close( sock ); 74: } 75: ----------------------------------------------------------------------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() システムコールでは、 付けてくれない。)
---------------------------------------------------------------------- 78: int tcp_connect( char *server, int portno ) 79: { 80: struct addrinfo hints, *ai; 81: struct sockaddr_in addr ; /* sockaddr_storage */ 82: int s ; 83: int err ; 84: 85: if( (s = socket(PF_INET, SOCK_STREAM, 0)) < 0 ) 86: { 87: perror("socket"); 88: return( -1 ); 89: } 90: 91: memset( &hints, 0, sizeof(hints) ); 92: hints.ai_family = AF_INET ; 93: hints.ai_socktype = SOCK_STREAM ; 94: if( (err = getaddrinfo( server, NULL, &hints, &ai )) ) 95: { 96: fprintf(stderr,"unknown host %s (%s)\n",server,gai_strerror(err) ); 97: return( -1 ); 98: } 99: if( ai->ai_addrlen > sizeof(addr) ) 100: { 101: fprintf(stderr,"sockaddr too large (%d) > (%d)\n", 102: ai->ai_addrlen,sizeof(addr) ); 103: freeaddrinfo( ai ); 104: return( -1 ); 105: } 106: memcpy( &addr, ai->ai_addr, ai->ai_addrlen ); 107: addr.sin_port = htons( portno ); 108: freeaddrinfo( ai ); 109: 110: if( connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) 111: { 112: perror( server ); 113: close( s ); 114: return( -1 ); 115: } 116: return( s ); 117: } ----------------------------------------------------------------------tcp_connect() は、通信路の開 設の仕事のうち、クライアント側の仕事をする関数である。まず、クライア ント側のソケットを、socket() システムコールで作成している。 PF_INET と SOCK_STREAMの組み合わせなので、 TCP を使うことを意味する。
socket() の引数で、PF_INET の変りに、AF_INET と書いてもよい。ここでは、 Protocol を選んでいるので、PF_ が正しいが、実際には、PF_INET と AF_INET は同じであり、また、多くのテキストで混在されて使われいる。
ソケットが作成できたら、connect() システムコールで接続する。その第2引 数と第3引数で、接続先のアドレスを指定しなければならない。まずそのため の準備を行う。
TCP/IP (IPv4) の場合、アドレスの指定には、sockaddr_in 構造体を使う。connect() システムコールのマニュアルには、sockaddr 構造体を使うようにと書かれて いるが、TCP/IP では、そのサブクラス(オブジェクト指向用語)である sockaddr_in を使う。それには、 IP アドレスとポート番号が必要である。 その他に、sockaddr 構造体のサブクラスの中でも sockaddr_in であることを 示す定数 AF_INET が先頭に現れる。
/usr/include/bits/sockaddr.h: ---------------------------------------------------------------------- /* POSIX.1g specifies this type name for the `sa_family' member. */ typedef unsigned short int sa_family_t; #define __SOCKADDR_COMMON(sa_prefix) \ sa_family_t sa_prefix##family ---------------------------------------------------------------------- /usr/include/bits/socket.h: ---------------------------------------------------------------------- /* Structure describing a generic socket address. */ struct sockaddr { __SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ }; ---------------------------------------------------------------------- /usr/include/netinet/in.h: ---------------------------------------------------------------------- typedef uint16_t in_port_t; /* Structure describing an Internet socket address. */ struct sockaddr_in { __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; }; ---------------------------------------------------------------------- /usr/include/sys/un.h: ---------------------------------------------------------------------- /* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket. */ struct sockaddr_un { __SOCKADDR_COMMON (sun_); char sun_path[108]; /* Path name. */ }; ----------------------------------------------------------------------
IP アドレスは、文字列で与えられた接続先のサーバのホスト名からライブラ リ関数 getaddrinfo() を使って調べている。(従来は、getaddrinfo() ではな く gethostbyname() がよく使われていた。)
getaddrinfo(3) Linux Programmer's Manual getaddrinfo(3) NAME getaddrinfo - network address and service translation int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *ai_next; };このライブラリ関数は、第一引数として、ホスト名、第2引数としてポート番 号に関連した(/etc/services に含まれている)、第3引数として、調べたいア ドレスの制約に関するヒントを与える。この例では、TCP/IP (IPv4) なので、 ai_family としてAF_INET, ai_socktype とし SOCK_STREAM を指定している。 (IPv4/IPv6 の両方に対応するには、AF_INET は指定しない方がよい。)
この関数は、与えられた引数から、IP アドレス(やポート番号)を調べ、結果 を構造体 struct addrinfo に格納して返す。その構造体は、ヒープ (malloc() されたメモリ)に置かれている。関数 getaddrinfo() は、構造体 へのポインタを第4引数で示された番地に保存する(第4引数は、ポインタを 保存するための番地)。
この構造体の中の ai_addr に目的の IP アドレスを組んだstruct sockaddr が含まれている。その構造体を、ai_addrlen バイト数分だけだけ addr 変数 にコピーしている。
1つのホストに複数の IP アドレスが割り当てられている場合もある。その場 合は、ai_next を手繰ればわかる。
ライブラリ関数 getaddrinfo() の内部的では、/etc/hosts というファイル や, NIS の hosts マップ, DNSの名前サーバなどが調べられる。どれがどの順 番で調べられるかは、システムに依存する。 /etc/nsswitch.conf があるシステムでは hosts: の行に書かれている。 /etc/host.conf というファイルがあるホストでは、そのファイルの order という 行による。
ポート番号は、引数で与えられたものをそのまま使う。ただし、「ネットワー ク・バイトオーダ」に変換する必要がある。ポート番号は、16 ビットなので、 htons() ライブラリ関数(host to network, short)を使っている。他に、 htonl(), ntohl(), ntohs() がある。
IP アドレスもネットワーク・バイトオーダになっているが、これは getaddrinfo() 関数で、struct addrinfo の ai_addr に保存それた段階そう なっている。
ポート番号として、整数(short)ではなく、/etc/services ファイルに含まれ た文字列で指定したいこともある。その場合は、getaddrinfo() 関数の第2引 数に文字列を渡す。
---------------------------------------------------------------------- 119: /* 120: W.リチャード・スティーブンス著、篠田陽一訳: 121: "UNIXネットワークプログラミング第2版 Vol.1 ネットワークAPI:ソケットとXTI", 122: ピアソン・エデュケーション, 1999年. ISBN 4-98471-205-9 123: 3.9節 readn, writen, および readline 関数 (p.76) 124: 125: Richard Stevens: "UNIX Network Programming, Volume 1, Second Edition: 126: Networking APIs: Sockets and XTI", Prentice Hall, 1998. 127: ISBN 0-13-490012-X. 128: Section 3.9 readn, writen, and readline Functions (p.77) 129: 130: http://www.kohala.com/start/ (http://www.kohala.com/~rstevens/) 131: http://www.kohala.com/start/unpv12e/unpv12e.tar.gz 132: 133: */ 134: 135: /* include writen */ 136: /*#include "unp.h"*/ 137: #include <errno.h> 138: 139: ssize_t /* Write "n" bytes to a descriptor. */ 140: writen(int fd, const void *vptr, size_t n) 141: { 142: size_t nleft; 143: ssize_t nwritten; 144: const char *ptr; 145: 146: ptr = vptr; 147: nleft = n; 148: while (nleft > 0) { 149: if ( (nwritten = write(fd, ptr, nleft)) <= 0) { 150: if (errno == EINTR) 151: nwritten = 0; /* and call write() again */ 152: else 153: return(-1); /* error */ 154: } 155: 156: nleft -= nwritten; 157: ptr += nwritten; 158: } 159: return(n); 160: } 161: ----------------------------------------------------------------------writen() は、write() システム・コールの代りに使うとよい。説明は、 echo_client() の所にある。
---------------------------------------------------------------------- 162: /* include readline */ 163: /*#include "unp.h"*/ 164: 165: ssize_t 166: readline(int fd, void *vptr, size_t maxlen) 167: { 168: ssize_t n, rc; 169: char c, *ptr; 170: 171: ptr = vptr; 172: for (n = 1; n < maxlen; n++) { 173: again: 174: if ( (rc = read(fd, &c, 1)) == 1) { 175: *ptr++ = c; 176: if (c == '\n') 177: break; /* newline is stored, like fgets() */ 178: } else if (rc == 0) { 179: if (n == 1) 180: return(0); /* EOF, no data read */ 181: else 182: break; /* EOF, some data was read */ 183: } else { 184: if (errno == EINTR) 185: goto again; 186: return(-1); /* error, errno set by read() */ 187: } 188: } 189: 190: *ptr = 0; /* null terminate like fgets() */ 191: return(n); 192: } 193: /* end readline */ ----------------------------------------------------------------------readline() は、\n 記号が現れるまでを一区切りとして、読込む。これは、 read() システムコールの代りには使えない。fgets() と同様に、最後に終端 の 0 を付けてくれる。(read() システムコールでは、付けてくれない。)
ただし、このように、1バイトずつ読む方法は、性能が悪い。上記の教科書で は、高速版やマルチスレッドで動作するプログラムも示してある。(dup() し てから、)、入力と出力にそれぞれ fdopen() でFILE * を割り当てる方法が使 えることがある。
FILE *fopen (const char *path, const char *mode); FILE *fdopen (int fildes, const char *mode); FILE *freopen (const char *path, const char *mode, FILE *stream);fdopen() を使うと、open() で開いたファイルやconnect() したソケットに対 して、fprintf() や fgets() といった高水準入出力ライブラリ関数が使える ようになる。
fdopen() を使って上の echo-client.c を書き 換えなさい。そして、TCP/IP によるプロセス間通信を、fgets() や fprintf() で行うようにしなさい。
fdopen() に渡すファイル記述子は、dup() でコピーしておいた方がよいであ ろう。dup() してあれば、2回 fclose() しても問題はない。
次の通信プロトコルのクライアントを1つ以上作りなさい。(以下の項目には、 各プロトコルを説明したページへのリンクが埋め込まれている。)
まず、telnet で、これらのサーバに接続しなさい。そして、それぞれのプロ トコルに従って、要求を打ち込み、どのような結果が返ってくるかを調べなさ い。次に、telnet で行った要求の送信と結果の受信を行うようなプログラムを 作りなさい。このとき、必要なパラメタは、main() の引数から取りなさい。
注意。それぞれのプロトコルでは、接続先として次のホストを使いなさい。
プログラムをつくる時には、行末の扱い(CR-LF)に注意しないさい。
HTTP, finger, SMTP, NNTP など、インターネットで使われている通信プロト コルの多くでは、行末を表す記号として、キャリッジ・リターン(carrige return,CR)とライン・フィード(Line feed, LF)の両方が必要であると定めら れている。Unix では、通常ライン・フィード(ニュー・ライン、New Line, NL と呼ばれることもある)だけが行末の記号として使われる。よって、画面 に文字列を表示し、改行したい場合は、次のようなプログラムが使われる。
しかしながら、インターネットの上のプログラムを作成する時には、次のよう にしなければならないことが多い。printf("Hello,world\n");
ここで、'\r' がキャリッジ・リターン、'\n' がライン・フィードである。い ずれも、C言語のソース・プログラム上では2文字に見えるが、Cコンパイラ により、1文字に変換される。それぞれ、アスキーでは、13(0x0d), 10(0x0a) である。printf("Hello,world\r\n");
規格上は、CR-LF が必要とされているが、実際にプログラムを作る時には、LF だけでも動くことがある。しかし、それはたまたまその特定のサーバのプログ ラムが良くできていて、LF だけでも動くようになっているからである。
-------------------------------------------------------------------- % ./time-client host 37Mon May 19 02:49:05 JST 2003 %
--------------------------------------------------------------------
このプログラムでは、TCP/IP でサーバに接続した後、何も送らずにサーバか ら4バイトの数を読み込む。その4バイトの数は、ネットワーク・バイト・オー ダになっているので、ntohl() で、ホストのバイト・オーダに変換する。この 値に、ある値で補正して、Unix で使われているtime_t に変換する。最後に、 strftime() や localtime() でカレンダーの形式に変換する。
RFC868 Time Protocol では、値は、1900年1月1日 0:00 (GMT) を基準にした 秒数を返す。time() システムコールや gettimeofday() システムコールでは、 1970 年を基準にしている。strftime() や localtime() を使う前に、差分を 補正する必要がある。
この課題では、ポート番号 37 の time を使いなさい。13 の daytime を使っ てはならない。
接続先のホストとしては、次のどれかを使いなさい。