システムプログラム(第7週): 受け取った文字列をそのまま返すサーバ(fork無し版)

                                       筑波大学 システム情報工学研究科 
                                       コンピュータサイエンス専攻, 電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2011/2011-06-08/echo-server-nofork-fdopen.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2011/
http://www.coins.tsukuba.ac.jp/~yas/

echo-server-nofork-fdopen.c

TCP/IP のポート番号 7 (echo) では、受け取ったデータをそのまま返すサー ビスを提供している。以下は、これと同じような機能を提供するサーバである。 複数の接続先(クライアント)の要求を同時に処理するために、クライアント ごとに fork() システム・コールで専用の子プロセスを作る。

print_my_host_port()

print_my_host_port() は、telnet で接続する時のヒントを表示する。
  87:	void
  88:	print_my_host_port( int portno )
  89:	{
  90:	    char hostname[100] ;
  91:	        gethostname( hostname,sizeof(hostname) );
  92:	        hostname[99] = 0 ;
  93:	        printf("run telnet %s(v6) %d \n",hostname, portno );
  94:	}
gethostname() システムコールで自分自身のホスト名を取り出している。(正 式には、gethostname() の結果とインターネット的なホスト名(IPアドレスと 対応している))が一致して異ないことがある。

coins では、2011年6月現在、IPv4 を主に使い、普通のホスト名ではIPv4 の IPアドレスが返されるようにっている。IPv6 のIPアドレスは、ホスト名の末尾 に v6 を付けた名前で引けるようなっている。たとえば、 cosmos10.coins.tsukuba.ac.jp なら、IPv4 の IP アドレス、 cosmos10v6.coins.tsukuba.ac.jp なら、IPv6 の IP アドレスが引けるように なっている。

一般的には、同じホスト名で IPv4 と IPv6 の両方のアドレスを登録すること もできる。移行の過程では、片方のアドレスで接続を試みて時間切れを起こし た後に別のアドレスで接続を試みるとしう処理が入るために、応答時間が増大 することがある。

tcp_peeraddr_print()

tcp_peeraddr_print() は、通信相手(peer)のアドレス(TCP/IPの場合、IPアド レスとポート番号)を表示する。
  95:	
  96:	void
  97:	tcp_peeraddr_print( int com )
  98:	{
  99:	    struct sockaddr_storage addr ;
 100:	    socklen_t addr_len ; /* MacOSX: __uint32_t */
 101:	        addr_len = sizeof( addr );
 102:	        if( getpeername( com, (struct sockaddr *)&addr, &addr_len  )<0 )
 103:	        {
 104:	            perror("tcp_peeraddr_print");
 105:	            return;
 106:	        }
 107:	        printf("[%d] connection (fd==%d) from ",getpid(),com );
 108:	        sockaddr_print( (struct sockaddr *)&addr, addr_len );
 109:	        printf("\n");
 110:	}
 111:	
通信相手のアドレス(IPアドレスとポート番号)は、getpeername() システムコー ルで得られる。得たアドレスを、sockaddr_print() に渡して表示している。

sockaddr_print()

sockaddr_print() は、引数で与えられた struct sockaddr に含まれる IP アドレスとポート番号を画面に表示する関数である。
 112:	void
 113:	sockaddr_print( struct sockaddr *addrp, socklen_t addr_len )
 114:	{
 115:	    char host[BUFFERSIZE] ;
 116:	    char port[BUFFERSIZE] ;
 117:	        if( getnameinfo(addrp, addr_len, host, sizeof(host),
 118:	                        port, sizeof(port), NI_NUMERICHOST|NI_NUMERICSERV)<0 )
 119:	            return;
 120:	        if( addrp->sa_family == PF_INET )
 121:	            printf("%s:%s", host, port );
 122:	        else
 123:	            printf("[%s]:%s", host, port );
 124:	}
 125:	
IP アドレスは、IPv4 では、32 ビット(int)であり、 ポート番号は、16 ビット(sort)である。 ここでは、getnameinfo() ライブラリ関数を用いてホスト名とポート番号の 文字列表現に変換している。この時、NUMERIC と指定しいるので、 IPv4 では、ドット「.」で区切られた10進数4つになる。

getnameinfo() が使われている部分では、以前は、gethostbyaddr() が使われ ていた。

tcp_acc_port()

tcp_acc_port() は、 通信路の開設 の仕事のうち、サーバ側で接続要求受付用ポートを作る関数である。
 126:	#define PORTNO_BUFSIZE 30
 127:	
 128:	int
 129:	tcp_acc_port( int portno, int ip_version )
 130:	{
 131:	    struct addrinfo hints, *ai;
 132:	    char portno_str[PORTNO_BUFSIZE];
 133:	    int err, s, on, pf;
 134:	
 135:	        switch( ip_version )
 136:	        {
 137:	        case 4:
 138:	            pf = PF_INET;
 139:	            break;
 140:	        case 6:
 141:	            pf = PF_INET6;
 142:	            break;
 143:	        default:
 144:	            fprintf(stderr,"bad IP version: %d.  4 or 6 is allowed.\n",
 145:	                ip_version );
 146:	            goto error0;
 147:	        }
 148:	        snprintf( portno_str,sizeof(portno_str),"%d",portno );
 149:	        memset( &hints, 0, sizeof(hints) );
 150:	        ai = NULL;
 151:	        hints.ai_family   = pf ;
 152:	        hints.ai_flags    = AI_PASSIVE;
 153:	        hints.ai_socktype = SOCK_STREAM ;
 154:	        if( (err = getaddrinfo( NULL, portno_str, &hints, &ai )) )
 155:	        {
 156:	            fprintf(stderr,"bad portno %d? (%s)\n",portno,
 157:	                    gai_strerror(err) );
 158:	            goto error0;
 159:	        }
 160:	        if( (s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol)) < 0 )
 161:	        {
 162:	            perror("socket");
 163:	            goto error1;
 164:	        }
 165:	
 166:	#ifdef  IPV6_V6ONLY
 167:	        if( ai->ai_family == PF_INET6 )
 168:	        {
 169:	            on = 1;
 170:	            if( setsockopt(s,IPPROTO_IPV6, IPV6_V6ONLY,&on,sizeof(on)) < 0 )
 171:	            {
 172:	                perror("setsockopt(,,IPV6_V6ONLY)");
 173:	                goto error1;
 174:	            }
 175:	        }
 176:	#endif  /*IPV6_V6ONLY*/
 177:	
 178:	        if( bind(s,ai->ai_addr,ai->ai_addrlen) < 0 )
 179:	        {
 180:	            perror("bind");
 181:	            fprintf(stderr,"port number %d can be already used. wait a moment or kill another program.\n", portno );
 182:	            goto error2;
 183:	        }
 184:	        on = 1;
 185:	        if( setsockopt( s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) ) < 0 )
 186:	        {
 187:	            perror("setsockopt(,,SO_REUSEADDR)");
 188:	            goto error2;
 189:	        }
 190:	        if( listen( s, 5 ) < 0 )
 191:	        {
 192:	            perror("listen");
 193:	            goto error2;
 194:	        }
 195:	        freeaddrinfo( ai );
 196:	        return( s );
 197:	
 198:	error2:
 199:	        close( s );     
 200:	error1:
 201:	        freeaddrinfo( ai );
 202:	error0:
 203:	        return( -1 );
 204:	}
 205:	

この関数の引数は、ポート番号(整数値)と IP のバージョン(4か6、整数値)を取る。 まず、IPのバージョンに応じて、Protocol Family (PF_) を選択している。 socket() の引数で、PF_INET/PF_INET6 の変りに、AF_INET/AF_INET6 と書いて もよい。ここでは、Protocol を選んでいるので、PF_ が正しいが、実際には、 PF_INET と AF_INET は同じであり、また、多くのテキストで混在されて使われ いる。

次に、getaddrinfo() を使って、socket() システム・コール、および、 bind() システム・コールに渡すためのパラメタを整えている。 hints.ai_family には、 PF_INET か PF_INET6 を設定している。 hints.ai_socktype に SOCK_STREAM を設定している。これにより、TCP を意味 する。ポート番号は、引数で与えられたもの(を、文字列に変換したもの)を指 定している。

hints.ai_flags には、AI_PASSIVE を指定し、 getaddrinfo() の第一引数とし て NULL を指定している。複数の IP アドレスがある時には、どれに要求が来 ても受け付ける。特定の IP アドレスを指定すると、そのアドレスに来た要求 だけを受け付けるようになる。IPv4 の時代には、IPアドレスとして INADDR_ANY という定数を指定することが一般的であった。

次に、クライアント側と同様に、ソケットを、socket() システムコールで作成 している。 PF_INET と SOCK_STREAMの組み合わせ または、 PF_INET6 と SOCK_STREAMの組み合わせ なので、TCP を使うことを意味する。

IPv6 の場合、setsockopt() で IPV6_V6ONLY というオプションを有効にしてい る。これで、IPv6 が指定された時には、IPv6 のみで接続要求を受け付ける。 このオプションをつけないと、IPv4射影アドレス(IPv4-mapped address)でも 要求を受け付けることになる。意図せずこのアドレスでもサービスを提供する とセキュリティ上の問題が生じることがある。

ソケットが作成できたら、bind() システムコールで、サーバ側で利用するア ドレス(IPアドレスとポート番号)を設定する。IP アドレスとポート番号は、 getaddrinfo() で返されたものを使っている。

次に、setsockopt() で、SO_REUSEADDR オプションを設定している。これによ り、同じ IP アドレスとポート番号で(連続して)サービスを提供することがや りやすくなる。ただし、可能性は低いが、前のプロセスと新しいプロセスで、 通信内容が混じってしまう危険性がある。

次に、listen() システムコールにより、要求受け付けを開始する。第2引数 は、最大何個のクライアントを接続要求待ちで待たせるか(待ち行列の長さ) を指定する。 重たいサーバを設計する時には、キューの長さを調節する。Apache (WWW サー バ) などでは、500 程度になっていることがある。

注意:このプログラムには 複数のクライアントに対してサービスを同時に提供できない という問題がある。

fdopen_sock()

fdopen_sock() は、TCP/IP による通信を、fprintf(), fgets(), fread() 等で 行えるようにする関数である。 この関数は、 クライアント側 とまったく同じである。
Last updated: 2011/06/06 15:32:49
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>