筑波大学 システム情報工学研究科
コンピュータサイエンス専攻, 電子・情報工学系
新城 靖
<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 アドレス によってアクセスを許したりエラーを発生させたりしなさい。