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

                                       筑波大学 システム情報系 情報工学域
                                       新城 靖
                                       <yas@cs.tsukuba.ac.jp>

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

echo-server-nofork-fdopen.c

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

主要部分は、講義資料のメインのページにある。 このページにあるもの、その他の細かい部分である。

ここに掲載されている関数の多くは、coins-syspro.cで定義されている。

print_my_host_port()

print_my_host_port() は、telnet で接続する時のヒントを表示する。
  17:	#define PRINT_MY_HOST_PORT_HOST_NAME_MAX 256
...
  77:	void
  78:	print_my_host_port( int portno )
  79:	{
  80:	        char hostname[PRINT_MY_HOST_PORT_HOST_NAME_MAX+1] ;
  81:	
  82:	        gethostname( hostname,PRINT_MY_HOST_PORT_HOST_NAME_MAX );
  83:	        hostname[PRINT_MY_HOST_PORT_HOST_NAME_MAX] = 0 ;
  84:	        printf("run telnet %s %d \n", hostname, portno );
  85:	}
gethostname() システムコールで自分自身のホスト名を取り出している。(正 式には、gethostname() の結果とインターネット的なホスト名(IPアドレスと 対応している))が一致していないことがある。

2026年7月現在、coins では、同じホスト名で IPv4 と IPv6 の両方のアドレス を登録している。多くのサービスでは、両方のアドレスでサービスを提供して いるが、一方のアドレスでのみサービスを提供していることもある。 そのような場合、片方のアドレスで接続を試みて時間切れを起こした後に別の アドレスで接続を試みる処理が入るために、応答時間が増大することがある。

tcp_sockaddr_print()

tcp_sockaddr_print() は、自分自身のアドレス(TCP/IPの場合、IPアド レスとポート番号)を表示する。
  87:	void
  88:	tcp_sockaddr_print( int com )
  89:	{
  90:	        struct sockaddr_storage addr ;
  91:	        socklen_t addr_len ; /* macOS: __uint32_t, Linux: unsigned int */
  92:	
  93:	        addr_len = sizeof( addr );
  94:	        if( getsockname( com, (struct sockaddr *)&addr, &addr_len  )<0 )
  95:	        {
  96:	                perror("tcp_peeraddr_print");
  97:	                return;
  98:	        }
  99:	        sockaddr_print( (struct sockaddr *)&addr, addr_len );
 100:	        printf("\n");
 101:	}
自分自身のアドレス(IPアドレスとポート番号)は、getsockname() システムコー ルで得られる。得たアドレスを、sockaddr_print() に渡して表示している。

tcp_peeraddr_print()

tcp_peeraddr_print() は、通信相手(peer)のアドレス(TCP/IPの場合、IPアド レスとポート番号)を表示する。
 103:	void
 104:	tcp_peeraddr_print( int com )
 105:	{
 106:	        struct sockaddr_storage addr ;
 107:	        socklen_t addr_len ; /* macOS: __uint32_t, Linux: unsigned int */
 108:	
 109:	        addr_len = sizeof( addr );
 110:	        if( getpeername( com, (struct sockaddr *)&addr, &addr_len  )<0 )
 111:	        {
 112:	                perror("tcp_peeraddr_print");
 113:	                return;
 114:	        }
 115:	        sockaddr_print( (struct sockaddr *)&addr, addr_len );
 116:	        printf("\n");
 117:	}
通信相手のアドレス(IPアドレスとポート番号)は、getpeername() システムコー ルで得られる。得たアドレスを、sockaddr_print() に渡して表示している。

sockaddr_print()

sockaddr_print() は、引数で与えられた struct sockaddr に含まれる IP アドレスとポート番号を画面に表示する関数である。
  17:	#define PRINT_MY_HOST_PORT_HOST_NAME_MAX 256
...
 119:	void
 120:	sockaddr_print( struct sockaddr *addrp, socklen_t addr_len )
 121:	{
 122:	        char host[SOCKADDR_PRINT_BUFSIZE] ;
 123:	        char port[SOCKADDR_PRINT_BUFSIZE] ;
 124:	
 125:	        if( getnameinfo(addrp, addr_len, host, sizeof(host),
 126:	                        port, sizeof(port), NI_NUMERICHOST|NI_NUMERICSERV)<0 )
 127:	                return;
 128:	        if( addrp->sa_family == PF_INET )
 129:	                printf("%s:%s", host, port );
 130:	        else
 131:	                printf("[%s]:%s", host, port );
 132:	}
IP アドレスは、IPv4 では、32 ビットであり、 IP アドレスは、IPv6 では、128 ビットである。 ポート番号は、16 ビットである。 この関数は、getnameinfo() ライブラリ関数を用いてホスト名とポート番号の 文字列表現に変換している。この時、NUMERIC と指定しいるので、 IPv4 では、8ビットごと区切り、各8ビットを10進数で表記し、ドット「.」で結合する。 IPv6 では、16ビットごとに区切り、各16ビットを16進数で表記し、コロン「:」で結合する。

getnameinfo() が使われている部分では、以前は、gethostbyaddr() が使われ ていた。gethostbyaddr() は、IPv6 には対応していない。

tcp_acc_port()

tcp_acc_port() は、 通信路の開設 の仕事のうち、サーバ側で接続要求受付用ポートを作る関数である。
  15:	#define PORTNO_BUFSIZE 30

 134:	int
 135:	tcp_acc_port( int portno, int ip_version )
 136:	{
 137:	        struct addrinfo hints, *ai;
 138:	        char portno_str[PORTNO_BUFSIZE];
 139:	        int err, s, on, pf;
 140:	
 141:	        switch( ip_version )
 142:	        {
 143:	        case 4:
 144:	                pf = PF_INET;
 145:	                break;
 146:	        case 6:
 147:	#if     !defined(IPV6_V6ONLY)
 148:	                fprintf(stderr,"Sorry, IPV6_V6ONLY is not supported in this system.\n");
 149:	                goto error0;
 150:	#endif  /*IPV6_V6ONLY*/
 151:	                pf = PF_INET6;
 152:	                break;
 153:	        case 0:
 154:	        case 46:
 155:	        case 64:
 156:	                pf = PF_INET6; /* pf = 0; in macOS */
 157:	                break;
 158:	        default:
 159:	                fprintf(stderr,"bad IP version: %d.  4 or 6 is allowed.\n",
 160:	                        ip_version );
 161:	                goto error0;
 162:	        }
 163:	        snprintf( portno_str,sizeof(portno_str),"%d",portno );
 164:	        memset( &hints, 0, sizeof(hints) );
 165:	        ai = NULL;
 166:	        hints.ai_family   = pf ;
 167:	        hints.ai_flags    = AI_PASSIVE;
 168:	        hints.ai_socktype = SOCK_STREAM ;
 169:	        if( (err = getaddrinfo( NULL, portno_str, &hints, &ai )) )
 170:	        {
 171:	                fprintf(stderr,"bad portno %d? (%s)\n",portno,
 172:	                        gai_strerror(err) );
 173:	                goto error0;
 174:	        }
 175:	        if( (s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol)) < 0 )
 176:	        {
 177:	                perror("socket");
 178:	                goto error1;
 179:	        }
 180:	
 181:	#ifdef  IPV6_V6ONLY
 182:	        if( ai->ai_family == PF_INET6 && ip_version == 6 )
 183:	        {
 184:	                on = 1;
 185:	                if( setsockopt(s,IPPROTO_IPV6, IPV6_V6ONLY,&on,sizeof(on)) < 0 )
 186:	                {
 187:	                        perror("setsockopt(,,IPV6_V6ONLY)");
 188:	                        goto error1;
 189:	                }
 190:	        }
 191:	#endif  /*IPV6_V6ONLY*/
 192:	
 193:	        if( bind(s,ai->ai_addr,ai->ai_addrlen) < 0 )
 194:	        {
 195:	                perror("bind");
 196:	                fprintf(stderr,"Port number %d\n", portno );
 197:	                goto error2;
 198:	        }
 199:	        on = 1;
 200:	        if( setsockopt( s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) ) < 0 )
 201:	        {
 202:	                perror("setsockopt(,,SO_REUSEADDR)");
 203:	                goto error2;
 204:	        }
 205:	        if( listen( s, 5 ) < 0 )
 206:	        {
 207:	                perror("listen");
 208:	                goto error2;
 209:	        }
 210:	        freeaddrinfo( ai );
 211:	        return( s );
 212:	
 213:	error2:
 214:	        close( s );     
 215:	error1:
 216:	        freeaddrinfo( ai );
 217:	error0:
 218:	        return( -1 );
 219:	}

この関数の引数は、ポート番号(整数値)と IP のバージョン(4、6、46、64、または0の整数値)を取る。 まず、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 の場合(ip_version==6の場合)、setsockopt() で IPV6_V6ONLY というオ プションを有効にしている。これで、IPv6 が指定された時には、IPv6 のみで 接続要求を受け付ける。このオプションをつけないと、 IPv4 でも要求を受け付ける。この場合、クライアントのアドレスは、 IPv4射影アドレス(IPv4-mapped address) になる。

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

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

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

IPv4射影アドレス(IPv4-mapped address)

IPv6 が有効なホストは、IPv4 のホストからの接続された時にIPv4射影アドレ スでアクセス可能になっててることがある。たとえば、次のホストを考える。
$ host azalea16.coins.tsukuba.ac.jp [←]
azalea16.coins.tsukuba.ac.jp has address 130.158.231.16
azalea16.coins.tsukuba.ac.jp has IPv6 address 2001:2f8:3a:1711::231:16
$ hostname [←]
azalea16
$ ifconfig enp0s31f6 [←]
enp0s31f6: flags=4163  mtu 1500
        inet 130.158.231.16  netmask 255.255.254.0  broadcast 130.158.231.255
        inet6 fe80::6e3c:8cff:fe16:82f2  prefixlen 64  scopeid 0x20
        inet6 2001:2f8:3a:1711::231:16  prefixlen 64  scopeid 0x0
        ether 6c:3c:8c:16:82:f2  txqueuelen 1000  (イーサネット)
        RX packets 2748591  bytes 889438993 (889.4 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1040201  bytes 536689353 (536.6 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        device interrupt 19  memory 0x70800000-70820000

$ ip addr show dev enp0s31f6 [←]
3: enp0s31f6:  mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 6c:3c:8c:16:82:f2 brd ff:ff:ff:ff:ff:ff
    inet 130.158.231.16/23 brd 130.158.231.255 scope global noprefixroute enp0s31f6
       valid_lft forever preferred_lft forever
    inet6 2001:2f8:3a:1711::231:16/64 scope global noprefixroute
       valid_lft forever preferred_lft forever
    inet6 fe80::6e3c:8cff:fe16:82f2/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
$ []
このホストは、次の IP アドレスを持っている。 サーバで PF_INET (IPv4) でソケットを作成すると、次の IP アドレスでサー ビスを提供することになる。 PF_INET6 (IPv6) でソケットを作成すると、次のようなアドレスでサービスを 提供することになる。 IPV6_V6ONLYを有効にしなかった時は、 PF_INET6 でありながら、IPv4 でもサービスを提供することになる。

次の例では、 クライアント側は、IPv4 でアクセスしている。

$ host azalea16.coins.tsukuba.ac.jp [←]
azalea16.coins.tsukuba.ac.jp has address 130.158.231.16
azalea16.coins.tsukuba.ac.jp has IPv6 address 2001:2f8:3a:1711::231:16
$ telnet -4 azalea16.coins.tsukuba.ac.jp 1231 [←]
Trying 130.158.231.16...
Connected to azalea16.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
abc
def
def
ghi
ghi
^]
telnet> quit
Connection closed.
$ []
echo-server-nofork-fdopen は、IPv4 のホストから接続要求を受け付けてい る。IPv6 のサーバは、::ffff で始まるIPv4射影アドレスからアクセスされた ものとしてサービスを提供している。
$ ./echo-server-nofork-fdopen 1231 [←]
run telnet azalea16 1231
[::]:1231
[3049232] accepting incoming connections (acc==3) ...
[3049232] connection (fd==4) from [::ffff:130.158.231.28]:34120
[3049232] received (fd==4) 5 bytes, [abc
]
[3049232] received (fd==4) 5 bytes, [def
]
[3049232] received (fd==4) 5 bytes, [ghi
]
[3049232] connection (fd==4) closed.
[3049232] accepting incoming connections (acc==3) ...
^C
$ []

このように、IPv6 で意図せず IPv4 でもサービスを提供するとセキュリ ティ上の問題が生じることがある。たとえば、あるホストからのアクセスを禁 止しようとして、通常の IPv6 のアドレスで禁止する設定をしていたとしても、 IPv4 からアクセスされた時にはIPv4射影アドレスを通じて接続を許可してしま うことがある。

fdopen_sock()

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