筑波大学 システム情報工学研究科 コンピュータサイエンス専攻, 電子・情報工学系 新城 靖 <yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2008/No8.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2008/
http://www.coins.tsukuba.ac.jp/~yas/
% telnet www 80
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, 03 Jun 2008 09:19:38 GMT
Server: Apache/2.0.63 (Unix) PHP/4.4.8
Content-Length: 323
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.63 (Unix) PHP/4.4.8 Server at www.coins.tsukuba.ac.jp Port 80</address>
</body></html>
Connection closed by foreign host.
%
このうち、1行目、Content-Type:、空行、本文はしっかり返したい。
次の場所に、作りかけの 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"); }
size_t strlcpy(char *dst, const char *src, size_t size);第3引数には、コピー先 dst の大きさを与える。strlen(src) のように、コピー 元 src から計算してはならない。
char buf[BUFFERSIZE]; /*正*/ strlcpy( buf,p,sizeof(buf) ); /*正*/ strlcpy( buf,p,BUFFERSIZE ); /*正*/ snprintf( buf,BUFFERSIZE,"%s",p ); /*誤*/ strlcpy( buf,p,strlen(p) );
int snprintf(char *str, size_t size, char *,...);第2引数には、str の大きさを与える。strlen() などで、コピー元から計算し てはならない。
char buf[BUFFERSIZE]; /*正*/ snprintf( buf,sizeof(buf),"%s/public_html/%s",home,file ); /*正*/ snprintf( buf,BUFFERSIZE, "%s/public_html/%s",home,file ); /*誤*/ snprintf( buf,CONST+strlen(home)+strlen(file), "%s/public_html/%s",home,file );
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: }
main の部分は、TCP/IP 版の echo サービスのクライアント とほとんど同じである。
main() 関数は、コマンドラインの引数を調べて、echo_client() を呼んでい る。strtol() で、文字列で与えられたポート番号を、int に変換している。
35: 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+1]; 44: char rbuf[BUFFERSIZE+1]; 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: printf("receiving...\n"); 80: rcount = recvfrom( sock, rbuf, BUFFERSIZE, 0, 81: (struct sockaddr *)&from, &fromlen); 82: if( rcount < 0 ) 83: { 84: perror("recvfrom()"); 85: exit( 1 ); 86: } 87: rbuf[rcount] = 0 ; 88: printf("received %d bytes [%s] from ",rcount, rbuf ); 89: sockaddr_print( (struct sockaddr *)&from, fromlen ); printf("\n"); 90: 91: printf("==> "); fflush(stdout); 92: } 93: printf("\n"); 94: 95: close( sock ); 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: 98: int 99: udp_port_nobind() 100: { 101: int s ; 102: if( (s = socket(PF_INET, SOCK_DGRAM, 0)) < 0 ) 103: { 104: perror("socket"); 105: return( -1 ); 106: } 107: return( s ); 108: } 109:
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() したデータのパケットが失われた時には、再転 送は行われない。
UDP/IP の場合も、TCP/IP と同様に、アドレスの指定には、sockaddr_in 構造 体を使う。sendto() システムコールのマニュアルには、sockaddr 構造体を使 うようにと書かれているが、UDP/IP (IPv4) では、そのサブクラス(オブジェ クト指向用語)であるsockaddr_in を使う。この場合、IP アドレスとポート番 号が埋められている必要がある。その他に先頭に、sockaddr_in であることを 示す定数 AF_INET を置く。
156: void 157: sockname_print( int s ) 158: { 159: struct sockaddr_storage addr ; 160: socklen_t len ; 161: len = sizeof( addr ); 162: if( getsockname( s, (struct sockaddr *)&addr, &len )< 0 ) 163: { 164: perror("getsockname"); 165: exit( -1 ); 166: } 167: sockaddr_print( (struct sockaddr *)&addr,len ); 168: }
sockname_print() は、 echo-server-nofork-fdopen.c の tcp_peeraddr_print() の getpeername() を、getsockname() に変えたものである。getsockname() は、自分自身の名前(TCP/IP では、IP アドレスとポート番号)を得るためのシステムコールである。
UDP/IP では、TCP/IP とは異 なり、getpeername() で通信相手を調べることは、この場合できない。UDP/IP では、通信相手は、recvfrom() の引数で知ることができる。
% cp ~yas/syspro/ipc/echo-client-udp.c .
% 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:49293
receiving...
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:49293
receiving...
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:49294
receiving...
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:49294
receiving...
received 4 bytes [bbb
] from 130.158.86.40:1231
==> ^D
%
このプログラムは、コマンドラインから2の引数をとる。第1引数は、ホスト
名、第2引数は、ポート番号である。このプログラムは、標準入力から得られ
た行を、その指定されたホスト上のポート番号で動作しているサーバに対して
送る。サーバは、 同じものを送り返してくるので、それを受け取る。
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+1]; 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: }クライアント側との違いは、 bind() システムコールを使っている点にある。
bind() システムコールの引数は、 TCP版のecho サービスのサーバ と同じである。 ソケットが作成できたら、bind() システムコールで、サーバ側で利用するア ドレス(IPアドレスとポート番号)を設定する。IP アドレスは、普通は、 INADDR_ANY を指定する。複数の IP アドレスがある時には、どれに要求が来 ても受け付ける。特定の IP アドレスを指定すると、そのアドレスに来た要求 だけを受け付けるようになる。
ポート番号は、引数で与えられたものを、htons() でネットワーク・バイトオー ダに変換して与える。
TCP/IPのtcp_acc_port()とは異なり、UDP/IP では listen() システムコールは、使われない。 要求、すなわち、データが大量に送られ、サーバが処理できない時には、(普 通は古いものから順に)捨てられる。
サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。
注意:全員が同じコンピュータでポート番号 1231 を使うとプログラムが動かないことがある。
% cp ~yas/syspro/ipc/echo-server-udp.c .
% 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.40:49293
sending back [hello
] (6 bytes) to 130.158.86.40:49293
received 7 bytes [world!
] from 130.158.86.40:49293
sending back [world!
] (7 bytes) to 130.158.86.40:49293
received 4 bytes [aaa
] from 130.158.86.40:49294
sending back [aaa
] (4 bytes) to 130.158.86.40:49294
received 4 bytes [bbb
] from 130.158.86.40:49294
sending back [bbb
] (4 bytes) to 130.158.86.40:49294
^C
%
サーバ側では、bind() をつかっているので、最初からポート番号が固定され
ていることがわかる。
ヒント:DatagramSocket() を使う。
UDP/IP では、一度に送ることができるデータの大きさにには、実装上の制限 が付いている。これがいくつかを調べるプログラムを作りなさい。
UDP/IP のデータを中継するようなプログラムを作りなさい。 単方向だけでなく、双方向で中継するようにしなさい。
このようなプログラムの例として、udprelay と呼ばれるプログラムがある。
UDP/IP のサーバ、または、中継プログラムで、クライアントの IP アドレス によってアクセスを許したりエラーを発生させたりしなさい。