echoサーバーを書いた日の日記。
Head First C の11章を参考に、クライアントから文字列を受け取り、受け取った文字列をそのままクライアントへ返すechoサーバーを作る。echo サーバーは30000ポートを使用することにする。途中疑問に思ったところは主にUNIXネットワークプログラミングで調べた。

UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI
- 作者: W.リチャードスティーヴンス,W.Richard Stevens,篠田陽一
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 1999/07
- メディア: 単行本
- 購入: 8人 クリック: 151回
- この商品を含むブログ (35件) を見る

- 作者: David Griffiths,Dawn Griffiths,中田秀基,木下哲也
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/04/03
- メディア: 大型本
- この商品を含むブログ (5件) を見る
環境。VagrantとUbuntuで。
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=16.04 DISTRIB_CODENAME=xenial DISTRIB_DESCRIPTION="Ubuntu 16.04.1 LTS"
$ uname -a Linux ubuntu-xenial 4.4.0-38-generic #57-Ubuntu SMP Tue Sep 6 15:42:33 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
$ gcc -v Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.2' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.2)
以下のコードから始める。 while
の無限ループ内でクライアントからデータを受け取ってレスポンスを返すような処理を書いていく。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> void error(char *msg) { fprintf(stderr, "%s:%s\n", msg, strerror(errno)); exit(1); } int main(int argc, char *argv[]) { puts("wait..."); while(1) { } return 0; }
$ gcc echo_server.c -o echo_server $ ./echo_server
30000ポートでは何も動いていないことを確認。
$ lsof -i:30000
echo_serverが開いているファイルディスクリプタを見ておく。
$ ps -x | grep echo_server 9932 pts/0 R+ 0:46 ./echo_server 9942 pts/1 S+ 0:00 grep --color=auto echo_server
$ ll /proc/9932/fd total 0 dr-x------ 2 ubuntu ubuntu 0 Oct 9 07:56 ./ dr-xr-xr-x 9 ubuntu ubuntu 0 Oct 9 07:56 ../ lrwx------ 1 ubuntu ubuntu 64 Oct 9 07:56 0 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 07:56 1 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 07:56 2 -> /dev/pts/0
全体の流れ
- ソケットを作成する
- ソケットをバインドする
- Listen状態になる
- コネクションを取り出す
- クライアントから文字列を受け取る
- クライアントへ文字列を返す
ソケットを作成する
まず、ソケットを作成する処理を追加。
@@ -2,6 +2,7 @@ #include <stdlib.h> #include <string.h> #include <errno.h> +#include <sys/socket.h> void error(char *msg) { @@ -11,6 +12,10 @@ void error(char *msg) int main(int argc, char *argv[]) { + int listener_d = socket(PF_INET, SOCK_STREAM, 0); + if (listener_d == -1) { + error("socket err"); + } puts("wait..."); while(1) { }
ソケットとは、UNIXネットワークプログラミングに以下のように書いてある。
TCPコネクションのソケットペア(socket pair)は、コネクションの両方のエンドポイントを定義する、ローカルIPアドレス、ローカルTCPポート、リモートIPアドレス、およびリモートTCPポートの4つ組である。あるソケットペアは、インターネット中の特定のコネクションを一意に識別する。
各エンドポイントを識別する2つの値、すなわちIPアドレスとポート番号は、多くの場合ソケット(socket)と呼ばれる。
UNIXネットワークプログラミング 第2版 Vol.1
socket関数
socket関数の定義。カーネル内にソケットを作成し、ソケットに対応したディスクリプタを返す。
#include <sys/socket.h> int socket(int domain, int type, int protocol)
PF_INET
とは? man socket
を見ると AF_INET
になってる。
/usr/include/x86_64-linux-gnu/sys/socket.h
を確認してみると以下の記述が。
/* This operating system-specific header file defines the SOCK_*, PF_*, AF_*, MSG_*, SOL_*, and SO_* constants, and the `struct sockaddr', `struct msghdr', and `struct linger' types. */ #include <bits/socket.h>
/usr/include/x86_64-linux-gnu/bits/socket.h
を確認すると。
/* Protocol families. */ #define PF_UNSPEC 0 /* Unspecified. */ #define PF_LOCAL 1 /* Local to host (pipes and file-domain). */ #define PF_UNIX PF_LOCAL /* POSIX name for PF_LOCAL. */ #define PF_FILE PF_LOCAL /* Another non-standard name for PF_LOCAL. */ #define PF_INET 2 /* IP protocol family. */ ・ ・ ・ /* Address families. */ #define AF_UNSPEC PF_UNSPEC #define AF_LOCAL PF_LOCAL #define AF_UNIX PF_UNIX #define AF_FILE PF_FILE #define AF_INET PF_INET ・ ・ ・
AF_INET
でも PF_INET
でも同じ 2
という値で、IP protocol family と書いてある。
UNIXネットワークプログラミングを読むと以下のように書いてあった。
AFプリフィックスは"アドレスファミリ"の意であり、PFプリフィックスは"プロトコルファミリ"の意である。もともとは単一のプロトコルファイミリが複数のアドレスファミリをサポートすることが意図されており、ソケットの作成にはPF値を、ソケットアドレス構造体にはAF値が用いられてきた。実際には複数のアドレスファミリをサポートするプロトコルファミリがサポートされたことはなく、<sys/socket.h>ヘッダでは、あるプロトコルのPF値はそのプロトコルのAF値と同じ値を持つように定義されている。
UNIXネットワークプログラミング 第2版 Vol.1
だそうです。
AF_INETの場合、以下の3種類のソケットを作成できる。
- tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
- udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
- raw_socket = socket(AF_INET, SOCK_RAW, protocol);
この記述からも、PF_* という定数からも、 socket
関数からいろいろなプロトコルに対応したソケットが作れる雰囲気が出てる。
ソケットのディスクリプタ
int listener_d = socket(AF_INET, SOCK_STREAM, 0)
により作成されたTCPソケットのソケットのディスクリプタを取得できる。ファイルディスクリプタがファイル入出力のストリームを識別するように、ソケットのディスクリプタはソケットを識別できる数値である。
このディスクリプタ( listener_d
)に対応したTCPソケットがカーネルで管理されているということか。
動かしてみる
この段階でも30000ポートは使われてない。
$ lsof -i:30000
しかし、ファイルディスクリプタを確認してみると、ソケットが作成されていることが分かる。
$ ll /proc/9950/fd total 0 dr-x------ 2 ubuntu ubuntu 0 Oct 9 07:58 ./ dr-xr-xr-x 9 ubuntu ubuntu 0 Oct 9 07:58 ../ lrwx------ 1 ubuntu ubuntu 64 Oct 9 07:58 0 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 07:58 1 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 07:58 2 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 07:58 3 -> socket:[29062]
ポートをバインドする
ポートをバインドする処理を追加。
@@ -3,6 +3,7 @@ #include <string.h> #include <errno.h> #include <sys/socket.h> +#include <arpa/inet.h> void error(char *msg) { @@ -16,6 +17,13 @@ int main(int argc, char *argv[]) if (listener_d == -1) { error("socket err"); } + + struct sockaddr_in name; + name.sin_family = AF_INET; + name.sin_port = (in_port_t)htons(30000); + name.sin_addr.s_addr = htonl(INADDR_ANY); + bind(listener_d, (struct sockaddr *) &name, sizeof(name)); + puts("wait..."); while(1) { }
bind関数
bind関数は、ソケットにプロトコル特有のアドレスを割り当てる。
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
ソケットアドレス構造体
bind関数の第2引数にプロトコル特有のアドレスのポインタを指定する。プロトコル特有のアドレスを表す構造体をソケットアドレス構造体と呼ぶ。
ソケットAPIにおいて、ソケットアドレス構造体は、プロセスからカーネルへ、カーネルからプロセスへの両方向で受け渡しされる。ソケットアドレス構造体の名前は sockaddr_
で始まり、 IPv4のソケットアドレス構造体は sockaddr_in
。
TCPソケットのアドレスは、IPアドレスとポート番号の組である。
htons
関数はshort型の値をネットワークバイトオーダー(ビッグエンディアン)のバイト列にする。 htonl
関数はlong型の値をネットワークバイトオーダーのバイト列にする。 (参考: http://www-cms.phys.s.u-tokyo.ac.jp/~naoki/CIPINTRO/NETWORK/endian.html )
これらの関数を使って、ネットワークバイトオーダーで、ポート番号とIPアドレスを指定する。INADDR_ANYは0を差しワイルドカードであることを示す。
bindのようなソケットAPIは、いろいろなプロトコルのソケットに対応する必要があるので、IPv4用の sockaddr_in
を引数に取るわけにはいかない。そこで、 sockaddr
という構造体に一旦キャストしてから引数として渡す。
動かしてみる
特に変化なし。
$ lsof -i:30000
$ ll /proc/9971/fd total 0 dr-x------ 2 ubuntu ubuntu 0 Oct 9 08:10 ./ dr-xr-xr-x 9 ubuntu ubuntu 0 Oct 9 08:10 ../ lrwx------ 1 ubuntu ubuntu 64 Oct 9 08:10 0 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 08:10 1 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 08:10 2 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 08:10 3 -> socket:[29203]
LISTEN状態になる
@@ -26,6 +26,10 @@ int main(int argc, char *argv[]) error("bind err"); } + if (listen(listener_d, 1) == -1) { + error("listen err"); + } + puts("wait..."); while(1) { }
listen関数
listen関数により、TCPの状態が初期状態であるCLOSEからLISTENになる。
#include <sys/socket.h> int listen(int sockfd, int backlog);
コネクションキュー
第2引数は、第1引数で指定したソケットに対するコネクションキューのサイズ。このコネクションキューは、ESTABLISH状態のコネクションが格納される。
このキューの大きさは、カーネルパラメータであるnet.core.somaxconn
より大きな値が指定した場合はnet.core.somaxconn
の大きさになる。
SYN_RCVD状態のコネクションが格納されるキューも別にある。こちらはカーネルパラメータnet.ipv4.tcp_max_syn_backlog
最大値が決められる。
http://wiki.bit-hive.com/linuxkernelmemo/pg/listen%20backlog%20%A1%DA3.6%A1%DB
キューに入れなかったリクエストはどうなるか。UNIXネットワークプログラミングを読むと
クライアントからのSYNが到着した際にキューが満杯だった場合、TCPはRSTを送信せず、単に到達したSYNを無視する。これは、この状態が過度的な状態であると考えられるためで、クライアントはSYNを再送することにより、運がよければキューの空きを見つけることが可能である。
UNIXネットワークプログラミング 第2版 Vol.1
キューの大きさを1にして、同時接続を行うと段々と処理が行われることが分かる。
動かしてみる
listen
関数によって30000ポートがTCPのLISTEN状態になった。
$ lsof -i:30000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME echo_serv 9992 ubuntu 3u IPv4 29544 0t0 TCP *:30000 (LISTEN)
netstat
コマンドでも確認できる。
$ netstat -an | grep 30000 tcp 0 0 0.0.0.0:30000 0.0.0.0:* LISTEN
では、 telnet
でつないでみる。Vagrantで動かしてるUbuntu上動いているecho サーバーに、ホストOSであるMacからつないでみる。
$ telnet 192.168.33.10 30000 Trying 192.168.33.10... Connected to 192.168.33.10. Escape character is '^]'.
サーバー上で確認してみると、ESTABLISHED状態の接続ができていることがわかる。
$ netstat -an | grep 30000 tcp 0 0 0.0.0.0:30000 0.0.0.0:* LISTEN tcp 0 0 192.168.33.10:30000 192.168.33.1:64304 ESTABLISHED
TCPの状態遷移図を見ながらだとイメージしやすい。
引用元:第16回 信頼性のある通信を実現するTCPプロトコル(3) (3/4):基礎から学ぶWindowsネットワーク - @IT
ESTABLISHEDになってるということは3ウェイハンドシェークが完了したってこと。tcpdumpでパケットを覗いてみる。tcpdumpを実行してから、telnetで再度つなぐ。
$ tcpdump -i vboxnet0
telnetでつなぐと以下のように出力された。
13:14:16.795321 IP 192.168.33.1.65088 > 192.168.33.10.30000: Flags [S], seq 2760695165, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 654960331 ecr 0,sackOK,eol], length 0 13:14:16.795364 IP 192.168.33.10.30000 > 192.168.33.1.65088: Flags [S.], seq 3180502780, ack 2760695166, win 28960, options [mss 1460,sackOK,TS val 1405176 ecr 654960331,nop,wscale 7], length 0 13:14:16.796340 IP 192.168.33.1.65088 > 192.168.33.10.30000: Flags [.], ack 1, win 4117, options [nop,nop,TS val 654960334 ecr 1405176], length 0
3ウェイハンドシェークされてる。
tcpdumpを実行したまま、telnetを切断すると、クライアントからサーバーへのFIN,ACKが送られるが、サーバーからのACKとFINが送られないことが分かる。クライアントは何度かFIN,ACKを送るが最終的には、RST,ACKを送り強制的に接続を切った。
キューからESTABLISH状態のコネクションを取り出す
コネクションキューの先頭からESTABLISH状態のコネクションを1つ取り出す。キューが空の場合、プロセスはブロックされスリープ状態となる。
@@ -32,6 +32,12 @@ int main(int argc, char *argv[]) puts("wait..."); + + struct sockaddr_storage client_addr; + unsigned int address_size = sizeof(client_addr); while(1) { + int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size); + if (connect_d == -1) { + error("accept err"); + } } return 0; }
accept関数
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept関数は、クライアントとのTCPコネクションを参照しているソケットディスクリプタを返す。socket関数で取得したソケットディスクリプタとは別のものを差している。
第2引数で指定したソケットアドレス構造体にクライアントのアドレスが格納される。
動かしてみる
$ netstat -an | grep 30000 tcp 0 0 0.0.0.0:30000 0.0.0.0:* LISTEN tcp 0 0 192.168.33.10:30000 192.168.33.1:49697 ESTABLISHED $ lsof -i:30000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME echo_serv 10264 ubuntu 3u IPv4 31554 0t0 TCP *:30000 (LISTEN) echo_serv 10264 ubuntu 4u IPv4 31555 0t0 TCP 192.168.33.10:30000->192.168.33.1:49697 (ESTABLISHED)
$ ll /proc/10264/fd total 0 dr-x------ 2 ubuntu ubuntu 0 Oct 9 13:44 ./ dr-xr-xr-x 9 ubuntu ubuntu 0 Oct 9 13:44 ../ lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 0 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 1 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 2 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 3 -> socket:[31554] lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 4 -> socket:[31555]
telnetで接続を切ってみる。今度は、クライアントからのFIN,ACKに対し、サーバーからACKが返ってきた。
サーバー側のソケットは、CLOSE_WAIT状態のまま残っている。
$ ll /proc/10264/fd total 0 dr-x------ 2 ubuntu ubuntu 0 Oct 9 13:44 ./ dr-xr-xr-x 9 ubuntu ubuntu 0 Oct 9 13:44 ../ lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 0 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 1 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 2 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 3 -> socket:[31554] lrwx------ 1 ubuntu ubuntu 64 Oct 9 13:44 4 -> socket:[31555] $ netstat -an | grep 30000 tcp 0 0 0.0.0.0:30000 0.0.0.0:* LISTEN tcp 1 0 192.168.33.10:30000 192.168.33.1:49697 CLOSE_WAIT $ lsof -i:30000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME echo_serv 10264 ubuntu 3u IPv4 31554 0t0 TCP *:30000 (LISTEN) echo_serv 10264 ubuntu 4u IPv4 31555 0t0 TCP 192.168.33.10:30000->192.168.33.1:49697 (CLOSE_WAIT)
CLOSE_WAIT状態は、FINに対してACKを送って、アプリケーションからのクローズ待ち。クローズ処理漏れがあるとこの状態のTCPソケットが残ったままになってしまう。
ソケットを閉じる
@@ -4,6 +4,7 @@ #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> +#include <unistd.h> void error(char *msg) { @@ -38,6 +39,8 @@ int main(int argc, char *argv[]) if (connect_d == -1) { error("accept err"); } + + close(connect_d); } return 0; }
close関数
ファイルでも利用するclose関数で、ソケットをクローズする。
#include <unistd.h> int close(int sockfd);
ソケットに対してclose関数を実行すると、ソケットディスクリプタが使用できなくなる。カーネルでは、TCPソケットの送信バッファ内のデータを通信先へ送ってから、TCPコネクションの終了シーケンス(FINとACKを相互に送り合う)が実行される。
動かしてみる
telnetでつなぐとすぐにサーバー側から切断される。今度はサーバー側からclose関数で接続を切っている。接続が切られたのでTIME_WAIT状態になっている。
$ netstat -an | grep 30000 tcp 0 0 0.0.0.0:30000 0.0.0.0:* LISTEN tcp 0 0 192.168.33.10:30000 192.168.33.1:50509 TIME_WAIT $ ll /proc/10314/fd total 0 dr-x------ 2 ubuntu ubuntu 0 Oct 10 02:18 ./ dr-xr-xr-x 9 ubuntu ubuntu 0 Oct 10 02:18 ../ lrwx------ 1 ubuntu ubuntu 64 Oct 10 02:18 0 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 10 02:18 1 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 10 02:18 2 -> /dev/pts/0 lrwx------ 1 ubuntu ubuntu 64 Oct 10 02:18 3 -> socket:[31911]
tcpdumpを見ると、3ウェイハンドシェークの後、FINとACKを相互に送りあって接続を閉じている。
02:17:52.031983 IP 192.168.33.1.50509 > 192.168.33.10.30000: Flags [S], seq 537180097, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 658332935 ecr 0,sackOK,eol], length 0 02:17:52.032002 IP 192.168.33.10.30000 > 192.168.33.1.50509: Flags [S.], seq 169597332, ack 537180098, win 28960, options [mss 1460,sackOK,TS val 2223648 ecr 658332935,nop,wscale 7], length 0 02:17:52.032175 IP 192.168.33.1.50509 > 192.168.33.10.30000: Flags [.], ack 1, win 4117, options [nop,nop,TS val 658332935 ecr 2223648], length 0 02:17:52.032372 IP 192.168.33.10.30000 > 192.168.33.1.50509: Flags [F.], seq 1, ack 1, win 227, options [nop,nop,TS val 2223648 ecr 658332935], length 0 02:17:52.032627 IP 192.168.33.1.50509 > 192.168.33.10.30000: Flags [.], ack 2, win 4117, options [nop,nop,TS val 658332935 ecr 2223648], length 0 02:17:52.032643 IP 192.168.33.1.50509 > 192.168.33.10.30000: Flags [F.], seq 1, ack 2, win 4117, options [nop,nop,TS val 658332935 ecr 2223648], length 0 02:17:52.032654 IP 192.168.33.10.30000 > 192.168.33.1.50509: Flags [.], ack 2, win 227, options [nop,nop,TS val 2223649 ecr 658332935], length 0
ソケットに書き込む
クライアントから文字列を読み込む前に、 Hello World!
を返すようにしてみる。
@@ -40,6 +40,9 @@ int main(int argc, char *argv[]) error("accept err"); } + char *msg = "Hello World!\r\n"; + write(connect_d, msg, strlen(msg)); + close(connect_d); } return 0;
write関数
ファイルに書き込む時にも使うwrite関数でソケットにデータを書き込む。
#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);
writeによりデータが、カーネル内のソケットの送信バッファにコピーされる。ソケットバッファに空きがない場合、ブロックされプロセスはスリープ状態になる。writeの成功はバッファへのコピーが完了しただけであり、通信相手にデータが届いたわけではない。
ソケットの送信バッファ内のデータは、カーネルによってTCPのMSSやIPのMTUのサイズに分割されて、通信先へ送信される(調べた感じだと通常はIPのMTUのサイズの方が大きいみたい)。
動かしてみる
3ウェイハンドシェークの内容から、クライアントがサーバーへ伝えているmssが1460バイトであることが分かる。
12:10:03.903979 IP 192.168.33.1.52745 > 192.168.33.10.30000: Flags [S], seq 732193526, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 661451291 ecr 0,sackOK,eol], length 0 12:10:03.904111 IP 192.168.33.10.30000 > 192.168.33.1.52745: Flags [S.], seq 389803173, ack 732193527, win 28960, options [mss 1460,sackOK,TS val 3007057 ecr 661451291,nop,wscale 7], length 0 12:10:03.904157 IP 192.168.33.1.52745 > 192.168.33.10.30000: Flags [.], ack 1, win 4117, options [nop,nop,TS val 661451291 ecr 3007057], length 0
"Hello World!"という文字列のバイト数は、mssより小さいので、1回だけデータが送られている。
12:10:03.904402 IP 192.168.33.10.30000 > 192.168.33.1.52745: Flags [P.], seq 1:15, ack 1, win 227, options [nop,nop,TS val 3007057 ecr 661451291], length 14 12:10:03.904436 IP 192.168.33.1.52745 > 192.168.33.10.30000: Flags [.], ack 15, win 4117, options [nop,nop,TS val 661451291 ecr 3007057], length 0
mss(1460バイト)より大きなバイト数にしてみる。
char *msg = "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901\r\n";
1つのTCPセグメントだけでは送信仕切れず、2つのTCPセグメントに分割して送っていることがわかる。
12:11:56.140718 IP 192.168.33.10.30000 > 192.168.33.1.52797: Flags [.], seq 1:1449, ack 1, win 227, options [nop,nop,TS val 3035101 ecr 661563206], length 1448 12:11:56.140725 IP 192.168.33.10.30000 > 192.168.33.1.52797: Flags [P.], seq 1449:1464, ack 1, win 227, options [nop,nop,TS val 3035101 ecr 661563206], length 15 12:11:56.140867 IP 192.168.33.1.52797 > 192.168.33.10.30000: Flags [.], ack 1464, win 4072, options [nop,nop,TS val 661563207 ecr 3035101], length 0
クライアントからデータを受け取る
read関数で読む。read_line関数というラッパー関数を作り1行ずつ読むようにする。
@@ -12,6 +12,22 @@ void error(char *msg) exit(1); } +int read_line(int socket, char *buf, int len) +{ + char *s = buf; + int slen = len; + int c = read(socket, s, slen); + while ((c > 0) && (s[c - 1] != '\n')) { + s += c; + slen = -c; + c = read(socket, s, slen); + } + if (c < 0) { + return c; + } + return len - slen; +} + int main(int argc, char *argv[]) { int listener_d = socket(PF_INET, SOCK_STREAM, 0); @@ -32,6 +48,8 @@ int main(int argc, char *argv[]) } puts("wait..."); struct sockaddr_storage client_addr; unsigned int address_size = sizeof(client_addr); + + char buf[255]; while(1) { @@ -40,8 +58,9 @@ int main(int argc, char *argv[]) error("accept err"); } + read_line(connect_d, buf, sizeof(buf)); + + write(connect_d, buf, strlen(buf)); close(connect_d); }
read関数
ファイルから読む時にも使用するread関数を使う。
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count);
ファイルディスクリプタとは異なり、ソケットのディスクリプタに対するreadは、要求したバイト数より少ない入出力を行うことがある。これはカーネル内のソケットのバッファが限界に達するからである。そのため、ループで文字列の終端(ソケットの場合は \0
ではなく \r\n
)に達するまで何回もreadを呼ぶ必要がある。
おしまい
コードはこんな感じ。次は複数クライアントの同時接続に対応しようと思う。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> void error(char *msg) { fprintf(stderr, "%s:%s\n", msg, strerror(errno)); exit(1); } int read_line(int socket, char *buf, int len) { char *s = buf; int slen = len; int c = read(socket, s, slen); while ((c > 0) && (s[c - 1] != '\n')) { s += c; slen = -c; c = read(socket, s, slen); } if (c < 0) { return c; } return len - slen; } int main(int argc, char *argv[]) { int listener_d = socket(PF_INET, SOCK_STREAM, 0); if (listener_d == -1) { error("socket err"); } struct sockaddr_in name; name.sin_family = AF_INET; name.sin_port = (in_port_t)htons(30000); name.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listener_d, (struct sockaddr *) &name, sizeof(name)) == -1) { error("bind err"); } if (listen(listener_d, 1) == -1) { error("listen err"); } puts("wait..."); struct sockaddr_storage client_addr; unsigned int address_size = sizeof(client_addr); char buf[255]; while(1) { int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size); if (connect_d == -1) { error("accept err"); } char *msg = "Hello World!\r\n"; write(connect_d, msg, strlen(msg)); read_line(connect_d, buf, sizeof(buf)); write(connect_d, buf, strlen(buf)); close(connect_d); } return 0; }