システム・プログラムI 電子・情報工学系 新城 靖 <yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/syspro1-1998/1998-06-02
あるいは、次のページから手繰っていくこともできます。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/
http://www.hlla.is.tsukuba.ac.jp/~yas/index-j.html
---------------------------------------------------------------------- 1: /* 2: socketpair-rw.c -- socketpair() による双方向通信 3: ~yas/syspro1-1998/ipc/socketpair-rw.c 4: $Header: /home/lab2/OS/yas/syspro1-1998/ipc/RCS/socketpair-rw.c,v 1.2 1998/06/01 15:18:20 yas Exp $ 5: Start: 1997/05/26 21:29:34 6: */ 7: 8: #include <stdio.h> 9: #include <sys/types.h> /* fork(), socketpair() */ 10: #include <unistd.h> /* fork() */ 11: #include <sys/socket.h> /* socketpair() */ 12: 13: void parent( int fd ); 14: void child( int fd ); 15: 16: main() 17: { 18: int fildes[2] ; 19: pid_t pid ; 20: 21: if( socketpair(AF_UNIX,SOCK_STREAM,0,fildes) == -1) 22: { 23: perror("socketpair"); 24: exit( 1 ); 25: } 26: /* fildes[0] -- その1 27: * fildes[1] -- その2 28: */ 29: if( (pid=fork()) == 0 ) 30: { 31: close( fildes[1] ); 32: child( fildes[0] ); 33: } 34: else if( pid > 0 ) 35: { 36: close( fildes[0] ); 37: parent( fildes[1] ); 38: } 39: else 40: { 41: perror("fork"); 42: exit( 1 ); 43: } 44: } 45: 46: void parent( int fd ) 47: { 48: char *p,c,ret ; 49: p = "hello,world\n" ; 50: while( c = *p++ ) 51: { 52: if( write( fd, &c, 1 ) != 1 ) 53: perror("write"); 54: if( read( fd, &ret, 1 ) != 1 ) 55: perror("read"); 56: putchar( ret ); 57: } 58: close( fd ); 59: } 60: 61: void child( int fd ) 62: { 63: char c ; 64: while( read(fd,&c,1) == 1 ) 65: { 66: c = toupper( c ); 67: if( write( fd,&c,1 ) != 1 ) 68: perror("read"); 69: } 70: close( fd ); 71: } ----------------------------------------------------------------------実行例。
---------------------------------------------------------------------- % ./socketpair-rwHELLO,WORLD %
----------------------------------------------------------------------
◆ストリーム
TCP/IPは、信頼性のある(reliable)双方向のストリーム転送サービス (stream transport service)を提供する通信プロトコルである(図1)。ス トリームとは、通信する2つのプロセス間に結合(connection,通信路)が形成 され、複数回に分けて送り出したデータでも順番が入れ替わらないがデータの 区切りがわからなくなるような転送サービスである。UNIXのパイプは、双方向 ではなく単方向であるが、同じストリームに分類される転送サービスを提供す るものである。
なお、C言語のライブラリ関数である fopen(), fgets(), fputs() なども、 ストリームと呼ばれることがある。これは、もともとランダム・アクセス可能 で、メモリ中の配列と同じようにアクセスするすることもできるファイルを、 まるでプロセス間通信のストリームと同じように扱うことができることに由来 する。ストリームの元の意味は、プロセス間通信である。
◆層(プロトコル・スタック)
TCP/IPによる通信では、図2に示すように、4つのプロトコル(規約、約束事) の層が使われる。TCP/IP自身は、TCP層と IP層という2つのプロトコルに分解 される。このようにさまざまなプロトコルが決められ、全体として層をなして いる。この様子を、プロトコル・スタックと呼ぶ。
TCP は、IP という通信プロトコルを利用して実現されている。IPは、(信頼 性がない)データグラム(datagram)転送サービスを提供する通信プロトコル である。データグラムでは、データの送り手と受けての間に結合(通信路)が 形成されず、送出したデータの順番が途中で変ることや送出したデータが失わ れることがある。データグラムは、書留めではない郵便に似ている。IPのデー タグラムが配達されるときに使われる番号が、IPアドレスである。IPアドレス としては、現在32ビットの整数が使われている。
TCP層の上には、応用層が定義されている。この層では、ftp, rlogin, WWW, mnews,sendmail などの、TCP/IP を利用するプログラムの間の会話の方法が定 義される。TCP/IPを使った通信は、まるでプロセス同士が電話で会話するよう に進められる。普通の電話では、日本語を話す人と英語を話す人は、電話で情 報交換を行うことができない。同様に、同じTCP/IPを使っていても、会話の方 法が違うと、まったく情報交換を行うことができない。ゆえに、TCP/IPの上に さらに、情報交換のためにさまざまなプロトコルが取り決められている。
TCP/IPの上に構築されているプロトコルの例を、表1に示す。ポート番号につ いては、後述する。
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に、いくつか の応用層のプロトコルについて、公に利用目的が決められているポート番号を 示す。1024以上のポート番号は、特に利用目的が決められていない。よって、 利用されていなければ、利用者が自由に使ってもよい。
クライアント側の通信用ポートのポート番号は、通常は、オペレーティング・ システムにより自動的に割り当てられる。サーバ側の通信用ポートのポート番 号も、同様である。
◆プロセス間通信におけるクライアント・サーバ・モデル
プロセス間通信は、本来自由に行うことができる。どのプロセスも自由にメッ セージを送信する権利がある。プロセス間通信におけるクライアント・サーバ・ モデルは、本来対称的なプロセスを最初にメッセージを送る方(クライアント・ プロセス)と受ける方(サーバ・プロセス)に分類することで、プロセス間通 信を構造化し、わかりやすくするものである。TCP/IPの通信路開設時における クライアントとサーバの役割は、このプロセス間通信におけるクライアント・ サーバ・モデルの1つの例になっている。
プロセス間通信におけるクライアント・サーバ・モデルにおける意味の他に、 クライアントとサーバという言葉は、サービスを受けるプロセスとサービスを 提供するプロセスの意味で使われることがある。インターネットにおけるプロ セス間通信では、多くの場合、サービスの授受の関係におけるクライアントと サーバと、プロセス間通信におけるクライアントとサーバが一致している(稀 に一致していないこともあるので、注意しなさい)。
◆ソケット
UNIX オペレーティング・システム上で動作するプログラムがTCP/IPの機能を 使う場合、UNIXオペレーティング・システムが提供するソケットというインタ フェースを通じて利用することになる。ソケットは、TCP/IP をはじめとして、 XNS, OSI などさまざまな通信プロトコルを UNIX オペレーティング・システ ム上で使うために設計されたものである。TCP/IP だけを考えると、ソケット のインタフェースは、繁雑であり、使いにくくなっている。
UNIXでは、ソケットをドメイン(Address Family)と型で区別する。 下の表は、socket() システム・コールに与えるドメインと型である。
---------------------------------------------------------------------- ドメイン 型 option プロトコル ---------------------------------------------------------------------- AF_INET SOCK_STREAM 0 TCP AF_INET SOCK_DGRAM 0 UDP AF_INET SOCK_RAW ? IP AF_INET SOCK_RAW ? ICMP AF_UNIX SOCK_STREAM 0 (UNIXドメインのストリーム) AF_UNIX SOCK_DGRAM 0 (UNIXドメインのデータグラム) AF_NS SOCK_SEQPACKET ? XEROX NS protocol の順序付きパケット AF_NS SOCK_RDM ? XEROX NS protocol の信頼性のあるデータグラム ----------------------------------------------------------------------これ以外の組み合わせ、使えない。たとえば、AF_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アドレスから名前を引くた めにも使われる。
これは、次の省略形である。-------------------------------------------------------------------- % 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 コマンドに次のようIPアドレスを与えてもよい。
-------------------------------------------------------------------- % telnet XXXXXXXXXX 23--------------------------------------------------------------------
-------------------------------------------------------------------- % telnet localhost echoTrying... Connected to localhost. Escape character is '^]'. hello
hello exit
exit quit
quit aaa
aaa ^] telnet> quit
Connection closed. %
--------------------------------------------------------------------
---------------------------------------------------------------------- % egrep echo /etc/servicesecho 7/tcp echo 7/udp % hostname
adonis1 % telnet adonis1 7
Trying 130.158.86.1... Connected to adonis1. Escape character is '^]'. hello
hello aaa
aaa ^] telnet> quit
Connection closed. %
----------------------------------------------------------------------
---------------------------------------------------------------------- 1: 2: /* 3: echo-client.c -- 文字列を送受信するクライアント(TCP/IP版) 4: ~/syspro1-1998/ipc/echo-client.c 5: $Header: /home/lab2/OS/yas/syspro1-1998/ipc/RCS/echo-client.c,v 1.2 1998/06/01 15:34:50 yas Exp $ 6: <-- ipc/RCS/rdaytime.c,v 1.4 1997/06/02 22:27:08 yas Exp $ 7: Start: 1998/06/01 23:48:29 8: */ 9: #include <stdio.h> 10: #include <sys/types.h> /* socket() */ 11: #include <sys/socket.h> /* socket() */ 12: #include <netinet/in.h> /* struct sockaddr_in */ 13: #include <netdb.h> /* gethostbyname() */ 14: 15: extern int tcp_connect( char *hostname, int portno ); 16: 17: main( int argc, char *argv[] ) 18: { 19: if( argc != 3 ) 20: { 21: fprintf( stdout,"Usage: %s host port\n",argv[0] ); 22: exit( -1 ); 23: } 24: echo_client( argv[1],atoi(argv[2]) ); 25: } 26: 27: echo_client( char *hostname, int portno ) 28: { 29: int sock ; 30: int rcount ; 31: int slen ; 32: char sbuf[BUFSIZ+1]; 33: char rbuf[BUFSIZ+1]; 34: 35: sock = tcp_connect( hostname, portno ); 36: if( sock<0 ) 37: exit( -1 ); 38: 39: strncpy( sbuf,"hello",BUFSIZ ); 40: sbuf[BUFSIZ] = 0 ; 41: slen = strlen( sbuf ) + 1 ; 42: printf("sending: %s\n",sbuf ); 43: if( write( sock, sbuf, slen ) != slen ) 44: { 45: perror("write"); 46: exit( 1 ); 47: } 48: if( (rcount = read( sock, rbuf, BUFSIZ ))<0 ) 49: { 50: perror("read"); 51: exit( 1 ); 52: } 53: printf("received: ",rbuf ); fflush( stdout ); 54: write( 1, rbuf, rcount ); 55: printf("\n"); 56: close( sock ); 57: } 58: 59: int tcp_connect( char *hostname, int portno ) 60: { 61: struct hostent *hostent ; 62: struct sockaddr_in addr ; 63: int addr_len ; 64: int s ; 65: 66: addr.sin_family = AF_INET ; 67: if( (hostent = gethostbyname( hostname )) == NULL ) 68: { 69: fprintf(stderr,"unknown host %s\n",hostname ); 70: return( -1 ); 71: } 72: bcopy( hostent->h_addr, &addr.sin_addr, hostent->h_length ); 73: addr.sin_port = htons( portno ); 74: if( (s = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) 75: { 76: perror("socket"); 77: return( -1 ); 78: } 79: if( connect(s, &addr, sizeof(addr)) < 0 ) 80: { 81: perror( hostname ); 82: close( s ); 83: return( -1 ); 84: } 85: return( s ); 86: } ----------------------------------------------------------------------実行例。
その他の hostent 構造体(/etc/hosts, NIS の hosts マップ, DNS)をアクセ スする関数。---------------------------------------------------------------------- % ./echo-client adonis1 7sending: hello received: hello %
----------------------------------------------------------------------
---------------------------------------------------------------------- struct hostent *gethostbyname(const char *name); struct hostent *gethostbyaddr(const char *addr, int len, int type); ----------------------------------------------------------------------/etc/services をアクセスするには、次のような関数を使う。
---------------------------------------------------------------------- struct servent *getservbyname(const char *name, const char *proto); struct servent *getservbyport(int port, const char *proto); ----------------------------------------------------------------------
ヒント:dup(), dup2(), close() を使って標準入出力を切り替えるか、 fdopen() で socketpair() の結果のファイル記述子を FILE * に変換する。
fdopen() を使う時に注意することは、socketpair() の結果が read() でも write() でも使えるのに対して、高水準入出力では、1つの FILE * では、入 力と出力のうち一方しかできないことである。よって、fdopen() の前に、 dup() により、入力と出力に別々のファイル記述子を割り当てておく必要があ る。
次の通信プロトコルのクライアントを1つ以上作りなさい。(以下の項目には、 各プロトコルを説明したページへのリンクが埋め込まれています。)
まず、telnet で、これらのサーバに接続しなさい。そして、それぞれのプロ トコルに従って、要求を打ち込み、どのような結果が返ってくるかを調べなさ い。次に、telnet で行った要求の送信と結果の受信を行うようなプログラムを 作りなさい。このとき、必要なパラメタは、main() の引数から取りなさい。
注意。それぞれのプロトコルでは、接続先として次のホストを使いなさい。