TCP/IPを使って通信、文字列を送受信するプログラムを作ります。
次の図のような仕組み(Server-Client)で動きます。
データの送受信を行う部分は、別スレッドで動くようにしますが
先ずは単純なサーバ/クライアントを作ります。
マルチスレッド化などは、後程この単純なサーバ/クライアントを元に考えることにします。
サーバ側(SimpleServer)
次のような機能を実現します。
起動引数:待機ポート番号
このポート番号でクライアントが接続要求があれば、接続を受容する。
送信されてきたクライアントから送信されてきたデータ(ここでは文字列)を
表示する。
キーボードから何か入力されたら終了する。
ここで使用するソケットAPIのいくつかはブロッキング動作します。
ブロッキング動作する関数は、特定の条件が満たされるまで制御を戻しません。
アプリケーションから見ると、プログラムが固まったように見えます。
ノンブロッキング動作にすることもできますが、Windows, Linuxでできるだけ
ソースを共通化するためにここではselectによる監視処理をおこなうことで
ブロッキングの問題を解決することにします。
ブロッキングする関数と、制御を戻してくる条件
accept 誰かが接続に来ないと、関数呼び出しから戻ってきません。
recv 何か受信するか、切断されるかするまで関数呼び出しから
戻ってきません。
send 送信完了(TCPの送信バッファにデータを書き終わる)まで
関数呼び出しから戻ってきません。
SimpleServerでは、ソケットAPIを使用する関数を次のような関数にまとめることにします。
今回新たに使用するソケットAPIについてみていくことにします。
CreateAndBindSocket関数では、次のソケットAPIを使用します。
SOCKET socket(int af, int type, int protocol);
ソケットを作成します。
サーバではIPv4, IPv6で待機するためのソケット(複数)を作成します。
通信のための端点 (endpoint) を作成し、 ディスクリプターを返します。
af, type, protocolの指定にははgetaddrinfoの結果を使用します。
int bind(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen);
ソケットに名前 (ポート, アドレスをソケットに割り当てる)を付けます。
ファイルディスクリプター sockfd で参照されるソケットに addr で指定された
アドレス、ポートを割り当てます
addrはsockaddr_in(IPv4のアドレス情報)またはsockaddr_in6(IPv6のアドレス情報)です。
DoListen関数では、次のソケットAPIを使用します。
int listen(SOCKET sockfd, int backlog);
ソケットを接続待機状態にします。
sockfd が参照するソケットを接続待ちソケット (passive socket) 状態にします。
backlog 引き数は、 sockfd についての保留中の接続のキューの最大長を指定します。
DoAccept関数では、次のソケットAPIを使用します。
SOCKET accept(SOCKET sockfd, struct sockaddr *addr, socklen_t *addrlen);
接続要求を受容します。
接続待ちソケット sockfd 宛ての接続要求に対して、接続済みソケットを新規に生成します。
新規に生成されたソケットは、送受信に使用します。
もとのソケット sockfd はこの呼び出しによって影響を受けません。
つまり、再び接続待ちに使用することができます。
addrには相手のアドレス情報がはいります。
相手がIPv4かIPv6かわからないので大きな構造体sockaddr_storageを渡します。
acceptはブロックするので、呼び出す前に接続要求があるかどうかをselectを
使って調べることにします。
アドレス情報を表す構造体がいくつか出てきたので、整理しておきましょう。
IPv4/IPv6の共通部分の構造体、ポインタはこれでキャストして関数に渡します。
sockaddr構造体 (16バイト)
IPv4のアドレス情報
sockaddr_in構造体 (16バイト)
in_addr構造体 [iPv4のアドレス値32bits]が含まれます。
IPv6のアドレス情報
sockaddr_in6構造体 (28バイト)
in6_addr構造体 [iPv6のアドレス値128bits]が含まれます。
IPv4/IPv6のどちらも格納できる大きな構造体
sockaddr_storage構造体 (128バイト)
DoRecv関数では、次のソケットAPIを使用します。
int recv(SOCKET sockfd, void *buf, size_t len, int flags);
データ受信
ソケットからメッセージを受け取ります。
成功した場合にはメッセージの長さが返ってきます。
0:切断 -1:エラー
ソケットに受け取るメッセージが存在しなかった場合、メッセージが到着するまで
待ちます。(ブロック動作)
受信したデータのサイズが要求したサイズに 達するまで待つのではなく、
何らかのデータを受信すると復帰します。
recvはブロックするので、呼び出す前に接続要求があるかどうかを
selectを使って調べることにします。
DestroySocket関数では、次のソケットAPIを使用します。
int shutdown(SOCKET sockfd, int how);
ソケットの破棄
sockfd に関連づけられているソケットによる全二重接続 (full-duplex
connection)の一部または全てを閉じます。
ここでは、送受信を閉じるため次の値を設定します。
WindowsではhowにSD_BOTHを設定します。
LinuxではhowにSHUT_RDWRを設定します。
int closesocket(SOCKET sockfd); Windows用
int close(SOCKET sockfd); Linux用
ソケットディスクリプターをクローズします 。
ブロッキング動作をする関数の監視処理のための関数とマクロは次のものです。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
3 つの独立したファイルディスクリプター集合の監視を行ことができます。
readfds に入れられたディスクリプターについては、読み込みが可能かどうか
writefds に入れられたディスクリプターについては、書き込み用に利用可能な領域があるか
exceptfds にあるものについては、例外の監視を行ないます。
イベントを監視したいファイルディスクリプターが一つもない場合には、
対応するファイルディスクリプター集合に NULL を指定します。
nfdsには、対象とするソケットの最大値+1を指定します。
ただし、Windowsではこの項目は無視されるのでFD_SETSIZEを指定します。
これについては、構造体fd_setの説明を参考にしてください。
void FD_ZERO(fd_set *set);
集合を消去します。
void FD_SET(int fd, fd_set *set);
指定したファイルディスクリプターの集合への追加
int FD_ISSET(int fd, fd_set *set);
集合にファイルディスクリプターがあるかどうか調べます。
構造体fd_setは、WindowsとLinuxで構成が全く異なりますが使用方法は同じです。
どのような構造体か調べてみましょう。
【Linuxのfd_set】 typedef long int __fd_mask; #define __FD_SETSIZE 1024 #define __NFDBITS (8 * (int)sizeof(__fd_mask)); typedef struct { __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; #define __FDS_BITS(set) ((set)->fds_bits) } fd_set;
LinuxはLP64つまりlong:64bits int:32bitsなので、__fd_maskは64bits
__FD_SETSIZE / __NFDBITS = 1024 / 64 = 16
なので次の定義と同等ということで、10241bitsの領域をもつ配列です。
typedef struct {
long int fds_bits[16];
} fd_set;
各bitが、ソケット(ファイルディスクリプタ)に対応します。
(参考) 0:標準入力 1:標準出力 2:標準エラー
ソケットとファイルディスクリプタは同じなので、selectを使って
キーボードの入力を検査することができます。
つまり、Linuxでは、selectを使った監視は最大、ソケット番号1023までしか対応できません。
ulimitを使ってファイルディスクリプタの上限を1024より大きくすると、FD_SETで1024以上の
ソケットをセットしようとするとプログラムはクラッシュします。
対応方法は、selectを使わずにpollを使います。
時間があれば、poll対応に変更することにしましょう。
【Windowsのfd_set】 #define FD_SETSIZE 64 typedef struct fd_set { u_int fd_count; /* how many are SET? */ SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ } fd_set;
SOCKETはlong long なので、64bits整数の要素数が64配列です。
Windowsではソケットがそのままこの配列に可能されるため上限はlong longの上限までです。
したがってselectで大きなソケットを使うことができます。
Windowsではソケットとファイルディスクリプタは別のものという扱いなので、selectを使って
キーボードの入力を検査することはできません。
特殊なアドレス(IPv4-mapped address)に対応することにします。
例えば、192.0.2.1のIPv4-mapped addressは::ffff:192.0.2.1です。
これは、IPv6のノードがIPv6をサポートしていないノード(IPv4しかサポート
していない)と通信するためのIPv6が使用できるアドレスです。
IPv4のみのノードが192.0.2.1に接続(connect)要求すると::ffff:192.0.2.1で
待機しているサーバに接続できます。
つまり、192.0.2.1と::ffff:192.0.2.1は同じアドレスです。
従って両方のアドレスにbindしようとするとエラーになります。
今回のサーバはIPv4でもIPv6でも動くので::ffff:192.0.2.1は使用しないようにします。
【対応方法】
int setsockopt(SOCKET sockfd, int level, int optname, const void *optval, int optlen);
sockfd で参照されるソケットに関連するオプションの設定を行う
IPv6のソケットに対して、 IPv4-mapped addressを使用しない設定
int on = 1; setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&on, sizeof(on));
クローズ直後に際bindできない状態が起こらないようにします。
サーバを終了した直後にもう一度サーバを起動しようとすると、bindがエラーで
終了することがあります。
しばらく時間がたってからもう一度実行すると問題なくbindが成功します。
TCPサーバ側でcloseを先に実行すると発生
TCPクライアント側で先にcloseを実行すると、発生しない
【対応方法】
int setsockopt(SOCKET sockfd, int level, int optname, const void *optval, int optlen);
sockfd で参照されるソケットに関連するオプションの設定を行う
int on = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
以上のことを念頭に実際のプログラムを作ることにします。
かなり長くなりましたので作成は、次回ということで。。。