筑波大学 システム情報工学研究科 コンピュータサイエンス専攻, 電子・情報工学系 新城 靖 <yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2007/No8.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2007/
http://www.coins.tsukuba.ac.jp/~yas/
% telnet www 80このうち、1行目、Content-Type:、空行、本文はしっかり返したい。Trying 130.158.86.207... Connected to orchid-nwd.coins.tsukuba.ac.jp. Escape character is '^]'. GET /.. HTTP/1.0
![]()
HTTP/1.1 400 Bad Request Date: Tue, 05 Jun 2007 09:52:56 GMT Server: Apache/2.0.55 (Unix) PHP/4.4.2 Content-Length: 324 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>400 Bad Request</title> </head><body> <h1>Bad Request</h1> <p>Your browser sent a request that this server could not understand.<br /> </p> <hr> <address>Apache/2.0.55 (Unix) PHP/4.4.2 Server at www2.coins.tsukuba.ac.jp Port 80</address> </body></html> Connection closed by foreign host. %
![]()
次の場所に、作りかけの HTTP サーバがある。
~yas/syspro/ipc/http-server.c
~yas/syspro/ipc/http-server-error.c
int http_send_reply_bad_request( FILE *out ) { fprintf(out,"HTTP/1.0 400 Bad Request\r\nContent-Type: text/html\r\n\r\n"); fprintf(out,"<html><head></head><body>400 Bad Request</body></html>\n"); }実際の Web サーバでは、exit() することは許されない。攻撃が成功したとも 言える。
IP 層では、アドレスとしては、IP アドレスだけを使う。UDP/IP は、IP 層の 機能をほとんどそのままの形で利用可能にしたものである。違いは、ポート番 号を指定できることと、チェックサムでデータの内容を検査できることなどで ある。
UDP/IP でも、クライアント側のアドレスは、OSに任せ、自らは指 定しないことが多い(指定することもできる)。
UDP/IP でも、通信パタンによる
クライアント・サーバ・モデル
に基づくことが多い。
図? UDP/IPによるクライアント・サーバ型の通信
telnet は、TCP/IP の汎用のクライアント(文字列の送受信用)として使える。 しかし、UDP/IP 用のクライアントとしては使えない。
1: 2: /* 3: echo-client-udp.c -- 文字列を送受信するクライアント(UDP/IP版) 4: ~yas/syspro/ipc/echo-client-udp.c 5: Created on: 1997/06/16 21:22:26 6: */ 7: #include <stdio.h> 8: #include <stdlib.h> /* exit() */ 9: #include <sys/types.h> /* socket() */ 10: #include <sys/socket.h> /* socket() */ 11: #include <netinet/in.h> /* struct sockaddr_in */ 12: #include <netdb.h> /* getaddrinfo() */ 13: #include <string.h> /* strlen() */ 14: 15: extern void echo_client_udp( char *server, int portno ); 16: extern int udp_port_nobind(); 17: extern int sockaddr_in_init( struct sockaddr_in *addr, int addrlen, 18: char *hostname, int portno ); 19: extern void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len ); 20: extern void sockname_print( int s ); 21: 22: main( int argc, char *argv[] ) 23: { 24: char *server ; 25: int portno ; 26: if( argc != 3 ) 27: { 28: fprintf( stdout,"Usage: %% %s host port\n",argv[0] ); 29: exit( -1 ); 30: } 31: server = argv[1] ; 32: portno = strtol( argv[2],0,10 ); 33: echo_client_udp( server, portno ); 34: } 35:
main の部分は、TCP/IP 版の echo サービスのクライアント とほとんど同じである。
main() 関数は、コマンドラインの引数を調べて、echo_client() を読んでい る。strtol() で、文字列で与えられたポート番号を、int に変換している。
36: #define BUFFERSIZE 1024 37: 38: void 39: echo_client_udp( char *server, int portno ) 40: { 41: int sock ; 42: int slen,scount,rcount ; 43: char sbuf[BUFFERSIZE]; 44: char rbuf[BUFFERSIZE]; 45: int i ; 46: socklen_t fromlen ; 47: struct sockaddr_in to; struct sockaddr_storage from ; 48: 49: sock = udp_port_nobind(); 50: if( sock<0 ) 51: exit( -1 ); 52: printf("my port is "); sockname_print( sock ); printf("\n"); 53: 54: if( sockaddr_in_init( &to, sizeof(to), server, portno )<0 ) 55: { 56: perror("sockaddr_in_init"); 57: exit( -1 ); 58: } 59: printf("server is "); 60: sockaddr_print( (struct sockaddr *)&to, sizeof(to) ); printf("\n"); 61: 62: printf("==> "); fflush(stdout); 63: while( fgets(sbuf,BUFFERSIZE,stdin) ) 64: { 65: slen = strlen( sbuf ); 66: printf("sending [%s] (%d bytes) to ", sbuf, slen ); 67: sockaddr_print( (struct sockaddr *)&to, sizeof(to) ); printf("\n"); 68: 69: scount = sendto( sock, sbuf, slen, 0, 70: (struct sockaddr *)&to, sizeof(to) ); 71: if( scount != slen ) 72: { 73: perror("sendto()"); 74: exit( 1 ); 75: } 76: printf("after sendo(), my port is "); sockname_print( sock ); printf("\n"); 77: 78: fromlen = sizeof( from ); 79: rcount = recvfrom( sock, rbuf, BUFFERSIZE, 0, 80: (struct sockaddr *)&from, &fromlen); 81: if( rcount < 0 ) 82: { 83: perror("recvfrom()"); 84: exit( 1 ); 85: } 86: rbuf[rcount] = 0 ; 87: printf("received %d bytes [%s] from ",rcount, rbuf ); 88: sockaddr_print( (struct sockaddr *)&from, fromlen ); printf("\n"); 89: 90: printf("==> "); fflush(stdout); 91: } 92: printf("\n"); 93: 94: close( sock ); 95: } 96:echo_client_udp() では、udp_port_nobind() という関数を呼び出している。 この結果、UDP/IP の通信用ポートが作られ、通信可能なファイル記述子が返される。 ただし、サーバとの間には、通信路は確保されない。このファイル記述子は、 標準入出力(0,1,2)や open() システム・コールの結果と同じもので、不要に なったら close() で解放する。しかし、この状態では、write() や read() システムコールは使えない。その代りに、sendto() や recvfrom() システム コールを使う。
クライアント側では、bind() でアドレス(IPアドレスとポート番号)を設定し ないことが多い。通信に使う前には、アドレスは固定されていない。通信に使 うと、その瞬間に固定され、close() するまで有効である。(クライアント側 でも、bind() でアドレスを固定することもできるが、一般的ではない。)
sockaddr_in_init() は、構造体 to に送り先のアドレス(IPアドレスとポート 番号)をセットしている。
送るデータは、TCP/IP版の同じく文字列である。UDP/IP の通信では、データ の区切りは保存されるので、行末の区切りをつけずに構造体をそのまま送るも のも多い。
sendto() システムコールで、データを送信している。送り先は、第5引数に指 定されている。TCP/IP とは異なり、データの区切りは保存される。また、一 度に全データが送られるので、TCP/IP とは異なり、全デー タを送り出すためのループは不要である。ただし、一度に送ることができるデー タの大きさには上限がある。
途中、階層の通信媒体の上限を越えた場合、「フラグメント化」が行われ、実 際のデータとしては分割されることがある。たとえば、イーサネットでは、 1500バイトを越えた場合、フラグメント化される。フラグメント化されたデー タは、受信側のプロセスに送られる前に組み立てられる。
データを受け取るには、recvfrom() システムコールを使っている。この時、 データの送り手のアドレス(IPアドレスとポート番号)が&from番 地に保存される。普通は、sendto() と同じアドレスから返ってくるが、 違うアドレスから返ってくることもある。
recvfrom() では、最後に終端の 0 を付けてくれないので、自分で 0 を付け ている。
97: int 98: udp_port_nobind() 99: { 100: int s ; 101: if( (s = socket(PF_INET, SOCK_DGRAM, 0)) < 0 ) 102: { 103: perror("socket"); 104: return( -1 ); 105: } 106: return( s ); 107: } 108:
udp_port_nobind() は、クライアント側の UDP/IP のポートを作る関数である。 内部では、ソケットを socket() システムコールで作成しているだけである。 PF_INET と SOCK_DGRAMの組み合わせなので、 UDP を使うことを意味する。 socket() の引数で、PF_INET の変りに、AF_INET と書いてもよい。ここでは、 Protocol を選んでいるので、PF_ が正しいが、実際には、PF_INET と AF_INET は同じであり、また、多くのテキストで混在されて使われいる。
UDP/IP でも、connect() システムコールで接続先を固定する方法もあるが、 一般的ではない。接続先を固定した場合は、sendto() や recvfrom() ではな く、write() やread() を使って通信することもできる。その場合も、UDP の 性質は保たれるので、write() したデータのパケットが失われた時には、再転 送は行われない。
109: int 110: sockaddr_in_init( struct sockaddr_in *addr, int addrlen, 111: char *hostname, int portno ) 112: { 113: struct addrinfo hints, *ai; 114: int err ; 115: 116: if( addrlen < sizeof(struct sockaddr_in) ) 117: { 118: fprintf(stderr,"sockaddr_in, not enough space (%d) > (%d)\n", 119: addrlen, sizeof(struct sockaddr_in) ); 120: return( -1 ); 121: } 122: memset( &hints, 0, sizeof(hints) ); 123: hints.ai_family = AF_INET ; /* IPv4 */ 124: if( (err = getaddrinfo( hostname, NULL, &hints, &ai )) ) 125: { 126: fprintf(stderr,"unknown host %s (%s)\n",hostname, 127: gai_strerror(err) ); 128: return( -1 ); 129: } 130: if( ai->ai_addrlen > addrlen ) 131: { 132: fprintf(stderr,"sockaddr too large (%d) > (%d)\n", 133: ai->ai_addrlen,sizeof(addr) ); 134: freeaddrinfo( ai ); 135: return( -1 ); 136: } 137: memcpy( addr, ai->ai_addr, ai->ai_addrlen ); 138: addr->sin_port = htons( portno ); 139: freeaddrinfo( ai ); 140: 141: return( 0 ); 142: }sockaddr_in_init() は、 TCP/IPのechoクライアントで用いたものと同じものである。
UDP/IP の場合も、TCP/IP と同様に、アドレスの指定には、sockaddr_in 構造 体を使う。sendto() システムコールのマニュアルには、sockaddr 構造体を使 うようにと書かれているが、UDP/IP (IPv4) では、そのサブクラス(オブジェ クト指向用語)であるsockaddr_in を使う。この場合、IP アドレスとポート番 号が埋められている必要がある。その他に先頭に、sockaddr_in であることを 示す定数 AF_INET を置く。 getaddrinfo() は、ホスト名を IP アドレスし、さらに、先頭に定数 AF_INET を設定している。ポート番号は、第2引数が NULL なので、0 を指定している。 この例では、引数で与えられたポート番号を htons() を介してバイトオーダ を調整してから設定している。
143: 144: void 145: sockaddr_print( struct sockaddr *addrp, socklen_t addr_len ) 146: { 147: char host[BUFFERSIZE] ; 148: char port[BUFFERSIZE] ; 149: if( getnameinfo(addrp, addr_len, host, sizeof(host), 150: port, sizeof(port), NI_NUMERICHOST|NI_NUMERICSERV)<0 ) 151: return; 152: printf("%s:%s", host, port ); 153: } 154: 155: void 156: sockname_print( int s ) 157: { 158: struct sockaddr_storage addr ; 159: socklen_t len ; 160: len = sizeof( addr ); 161: if( getsockname( s, (struct sockaddr *)&addr, &len )< 0 ) 162: { 163: perror("getsockname"); 164: exit( -1 ); 165: } 166: sockaddr_print( (struct sockaddr *)&addr,len ); 167: }sockaddr_print() は、 echo-server-nofork-fdopen.c のものと同じである。 getaddrinfo() ライブラリ関数により、ホスト名とポート番号の文字列表現を 得ている。文字列といっても、NI_NUMERICHOST|NI_NUMERICSERV というフラグ を指定しているので、結果は "123.4.5.6" や "80" のような数字の並びによ る表記になる。
sockname_print() は、自分自身のポートのアドレスを表示する関数である。 アドレスは、getsockname() で得られる。なお、UDP/IP では、TCP/IP とは異 なり、getpeername() で通信相手を調べることは、この場合できない。UDP/IP では、通信相手は、recvfrom() の引数で知ることができる。 sockname_print() は、 echo-server-nofork-fdopen.c の tcp_peeraddr_print() の getpeername() を、getsockname() に変えたものである。getsockname() は、自分自身の名前(TCP/IP では、IP アドレスとポート番号)を得るためのシステムコールである。
注意:メッセージの送り先としては、 echo-server-udp.c を用いている。 標準の 7 番ポートで動作しているものは、うまく動作しない。
実行例:
% cp ~yas/syspro/ipc/echo-client-udp.c .このプログラムは、コマンドラインから2の引数をとる。第1引数は、ホスト 名、第2引数は、ポート番号である。このプログラムは、標準入力から得られ た行を、その指定されたホスト上のポート番号で動作しているサーバに対して 送る。サーバは、 同じものを送り返してくるので、それを受け取る。% make echo-client-udp
cc echo-client-udp.c -o echo-client-udp % ./echo-client-udp azalea20.coins.tsukuba.ac.jp 1231
my port is 0.0.0.0:0 server is 130.158.86.40:1231 ==> hello
sending [hello ] (6 bytes) to 130.158.86.40:1231 after sendo(), my port is 0.0.0.0:53397 received 6 bytes [hello ] from 130.158.86.40:1231 ==> world!
sending [world! ] (7 bytes) to 130.158.86.40:1231 after sendo(), my port is 0.0.0.0:53397 received 7 bytes [world! ] from 130.158.86.40:1231 ==> ^D % ./echo-client-udp azalea20.coins.tsukuba.ac.jp 1231
my port is 0.0.0.0:0 server is 130.158.86.40:1231 ==> aaa
sending [aaa ] (4 bytes) to 130.158.86.40:1231 after sendo(), my port is 0.0.0.0:53398 received 4 bytes [aaa ] from 130.158.86.40:1231 ==> bbb
sending [bbb ] (4 bytes) to 130.158.86.40:1231 after sendo(), my port is 0.0.0.0:53398 received 4 bytes [bbb ] from 130.158.86.40:1231 ==> ^D %
![]()
1: 2: /* 3: echo-server-udp.c -- 文字列を送受信するサーバ(UDP/IP版) 4: ~yas/syspro/ipc/echo-server-udp.c 5: Created on: 1997/06/16 21:22:26 6: */ 7: 8: #include <stdio.h> 9: #include <stdlib.h> /* exit() */ 10: #include <sys/types.h> /* socket() */ 11: #include <sys/socket.h> /* socket() */ 12: #include <netinet/in.h> /* struct sockaddr_in */ 13: #include <netdb.h> /* getaddrinfo() */ 14: #include <string.h> /* memset() */ 15: 16: extern void echo_server_udp( int portno ); 17: extern int udp_port_bind( int portno ); 18: extern void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len ); 19: extern void sockname_print( int s ); 20: 21: main( int argc, char *argv[] ) 22: { 23: int portno ; 24: if( argc >= 3 ) 25: { 26: fprintf( stdout,"Usage: %s [portno]\n",argv[0] ); 27: exit( -1 ); 28: } 29: if( argc == 2 ) 30: portno = strtol( argv[1],0,10 ); 31: else 32: portno = getuid(); 33: echo_server_udp( portno ); 34: } 35:TCP版のecho サービスのサーバ と同様に、 引数として、ポート番号を取る。ポート番号が与えられなければ、そのプロセ スの UID (User ID) から生成する。UID は、個人個人を識別するための番号 (16ビット程度、システムによっては 32 ビット)である。この課題では、(1台 のコンピュータでは)個人ごとに別々の UID を使う必要がある。上のように UID から生成する方法は、一般的ではない。(筑波大学情報学類のシステムで はうまく働く。)
36: #define BUFFERSIZE 1024 37: 38: void 39: echo_server_udp( int portno ) 40: { 41: int s, rcount, scount; 42: struct sockaddr_storage addr ; 43: socklen_t addrlen ; 44: char buffer[BUFFERSIZE]; 45: 46: s = udp_port_bind( portno ); 47: if( s<0 ) 48: exit( -1 ); 49: printf("my port is "); sockname_print( s ); printf("\n"); 50: 51: while( 1 ) 52: { 53: addrlen = sizeof( addr ); 54: rcount = recvfrom( s, buffer, BUFFERSIZE, 0, 55: (struct sockaddr *)&addr, &addrlen ); 56: if( rcount < 0 ) 57: { 58: perror("recvfrom()"); 59: exit( 1 ); 60: } 61: buffer[rcount] = 0 ; 62: printf("received %d bytes [%s] from ",rcount, buffer ); 63: sockaddr_print( (struct sockaddr *)&addr,addrlen ); printf("\n"); 64: 65: printf("sending back [%s] (%d bytes) to ", buffer, rcount ); 66: sockaddr_print( (struct sockaddr *)&addr,addrlen ); printf("\n"); 67: scount=sendto( s, buffer, rcount, 0, (struct sockaddr *)&addr, addrlen ); 68: if( scount!= rcount ) 69: { 70: perror("sendto()"); 71: exit( 1 ); 72: } 73: } 74: } 75:udp_port_bind() は、引数で与えられたポート番号を使ってUDP/IP のポート を作成し、そのファイル記述子(ソケット)を返す。sockname_print() は、そ のアドレス(IPアドレスとポート番号)を表示する。 TCP/IP とは異なり、このポートは、そのまま通信に使うことができる。 逆に、accept() は使うことはできない。
サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。
このサーバは、fork() しない。サービスの内容が簡単であり、sendto() シス テムコールでブロックすることもないからである。サービスが重たい時には、 他のクライアントからの処理を並列に進めるために、子プロセスを作ること可 能である。
76: int 77: udp_port_bind( int portno ) 78: { 79: struct sockaddr_in addr ; 80: int addr_len ; 81: int s ; 82: 83: if( (s = socket(PF_INET, SOCK_DGRAM, 0)) < 0 ) 84: { 85: perror("socket"); 86: return( -1 ); 87: } 88: 89: memset( &addr, 0, sizeof(addr) ); 90: addr.sin_family = AF_INET ; 91: addr.sin_addr.s_addr = INADDR_ANY ; 92: addr.sin_port = htons( portno ); 93: 94: if( bind(s,(struct sockaddr *)&addr,sizeof(addr)) < 0 ) 95: { 96: perror("bind"); 97: fprintf(stderr,"port number %d is already used. wait a moment or kill another program.\n", portno ); 98: return( -1 ); 99: } 100: return( s ); 101: } ... 104: sockaddr_print( struct sockaddr *addrp, socklen_t addr_len ) ... 115: sockname_print( int s )udp_port_bind() は、サーバ用に、ポート番号を固定した UDP/IP のポートを作成する。 クライアント側との違いは、 bind() システムコールを使っている点にある。
bind() システムコールの引数は、 TCP版のecho サービスのサーバ と同じである。 ソケットが作成できたら、bind() システムコールで、サーバ側で利用するア ドレス(IPアドレスとポート番号)を設定する。IP アドレスは、普通は、 INADDR_ANY を指定する。複数の IP アドレスがある時には、どれに要求が来 ても受け付ける。特定の IP アドレスを指定すると、そのアドレスに来た要求 だけを受け付けるようになる。
ポート番号は、引数で与えられたものを、htons() でネットワーク・バイトオー ダに変換して与える。
listen() システムコールは、UDP/IP では使われない。 要求、すなわち、データが大量に送られ、サーバが処理できない時には、(普 通は古いものから順に)捨てられる。
実行例。
サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。
注意:全員が同じコンピュータでポート番号 1231 を使うとプログラムが動かないことがある。
% cp ~yas/syspro/ipc/echo-server-udp.c .サーバ側では、bind() をつかっているので、最初からポート番号が固定され ていることがわかる。% make echo-server-udp
cc echo-server-udp.c -o echo-server-udp % ./echo-server-udp 1231
my port is 0.0.0.0:1231 received 6 bytes [hello ] from 130.158.86.50:53397 sending back [hello ] (6 bytes) to 130.158.86.50:53397 received 7 bytes [world! ] from 130.158.86.50:53397 sending back [world! ] (7 bytes) to 130.158.86.50:53397 received 4 bytes [aaa ] from 130.158.86.50:53398 sending back [aaa ] (4 bytes) to 130.158.86.50:53398 received 4 bytes [bbb ] from 130.158.86.50:53398 sending back [bbb ] (4 bytes) to 130.158.86.50:53398 ^C %
![]()
ヒント:DatagramSocket() を使う。
UDP/IP では、一度に送ることができるデータの大きさにには、実装上の制限 が付いている。これがいくつかを調べるプログラムを作りなさい。
UDP/IP のデータを中継するようなプログラムを作りなさい。 単方向だけでなく、双方向で中継するようにしなさい。
このようなプログラムの例として、udprelay と呼ばれるプログラムがある。
UDP/IP のサーバ、または、中継プログラムで、クライアントの IP アドレス によってアクセスを許したりエラーを発生させたりしなさい。