ソケットプログラムメモ
あまり書かれていない問題について書く
read/write は期待通りのサイズを読み書きする前に戻ってきてしまうことがある。
このため、期待通りのサイズになるまで繰り返すサブルーチンを作るのが常套手段になっている。
read で読めるサイズ(読んでもブロックされないサイズ)を事前に知ることができない。
fd を非ブロッキングI/Oにして、読んでみるしかない。
1行だけを読む、というのが意外と難しい。原理的には EOL記号(CR,LF,NUL のシーケンス)が来るまで1文字づつ読む以外安全な方法がない。
やっぱり要らないから元に戻す(ungetc)が標準Cライブラリでは1文字しかできない。(CR,CR+LF,LF に加えて NUL まで考慮すると、2文字以上考えないと正しく動かない可能性がある。)
ソケットは低レベルI/Oなのでそもそも ungetc 相当が存在しない。(ungetc は標準Cライブラリにある高レベルI/O)
普通のストリーム(ファイルディスクリプタ)で直接入出力するのではなく、必ずバッファを介する形にすれば解決はする。(汎用性はなくなる。)
終端データ(LFやNULなど)を期待するよりも、先にバイト数に相当する固定長データを送ってもらった方が楽。
手入力でも動くとか、飽くまで1行コマンド型(あるいはn行ヘッダ型)にこだわるのであれば、行入力は避けられない。
ネットワークでの改行は、CR+LFで統一されている。一方、Unix 系の改行は LF のみになっているため、その変換処理が実は必要。(Unix のテキスト垂れ流しはよくあるが間違い。)
getaddrinfo が複数の結果を返すことが多々ある。特に今は IPv4 と IPv6 のアドレスを返してしまうので、単純に先頭1つだけ試すというわけには行かなくなっている。
IPv4 と IPv6 のどちらを先に返すかは OS(TCP/IPスタック)依存なのでプログラム的にはどうにもならない。
ユーザーが意図的に IPv4のみ、IPv6のみを選択するならば、 struct addrinfo の ai_family で、AF_INET, AF_INET6 を指定することで制限することができる。
クライアントの場合は connect を順に試すことになる。
サーバーの場合はそれぞれのアドレスに bind, listen することが必要となる。(本来はこう動かないとおかしい)
socket 1つで bind できるアドレスは1つのみなので、複数の socket が必要となる。
待ち受け処理は以下の方法がある。
fork でマルチプロセスで分ける方法
pthread などでシングルプロセス・マルチスレッドで分ける方法
シングルプロセス・シングルプロセスにして select で切り分ける方法
受信処理は accept の後以下の方法がある。
fork でマルチプロセスで分ける方法
pthread などでシングルプロセス・マルチスレッドで分ける方法
シングルプロセス・シングルプロセスにして select で切り分ける方法
サーバー側で、getaddrinfo から全アドレス受信("wildcard" address)(INADDR_ANY, IN6ADDR_ANY_INIT)を得るためにはホスト名を NULL にして、hints.ai_flags = AI_PASSIVE にする必要がある。
この API だと、具体的にリテラルでどう表現すればよいのかがわからないため、実装者が各々バラバラな表現で実装してしまった。(0.0.0.0 だったり、 ::0 だったり、ANY だったり * だったり)
仕様策定者は、"wildcard" address の標準的なリテラル表現を決定すべきだった。
恐らく最も安全なのは"*"`1文字
fork の場合
破壊的な処理をしてもプロセスごと破棄できるので安全。
マルチプロセスなので、ある通信で処理をしていても、別の通信を並行実行できる。
fork 自体に時間がかかる。
fork の度にメモリを消費してしまう。
pthread の場合
破壊的な処理をすると、他の通信に影響が出てしまう。
マルチスレッドなので、ある通信で処理をしていても、別の通信を並行実行できる。
pthread_create の実行コストがやや高い。
select の場合
破壊的な処理をすると、他の通信に影響が出てしまう。
シングルプロセスなので、ある通信で処理をしていると、別の通信の処理ができなくなる。
待ち受け自体は軽い処理なので、select 方式でもそれほど問題にならない。
受信処理は重い処理なので、fork か pthread 方式にする方が合理的。
fork や pthread_create のコストを下げるため、事前に fork や pthread_create しておいて、そこと内部的に通信する方法がある。
select, pselect の4番目のパラメータは errorfds, exceptfds と書かれていて、エラー状態(error codition)、例外があることを示しているが、OS(TCP/IPスタック)依存で何が来るのか分からない。
何がエラー状態、例外で、何をどう実装すべきなのかがまったく分からない。
TCP では OOB データの存在としている。(Linuxのマニュアルによる)
TCP はそのプロトコルでデータ漏れとデータ化けがないことを保証しているので、データ漏れとデータ化けに関するコードを書かなくてもよい。また、書いてはいけない。
一定時間、データが得られないときに強制切断して再接続するようなコードは必要となる。
何も考えずにサーバーを書くと、再起動時に bind でポートが使用中になってしまう。
setsockopt 関数で、SO_REUSEADDR を設定すると回避できる。
何も考えずにサーバーを書くと、ピンポン通信になってパフォーマンスが悪くなる。
要求-応答の繰り返しで繰り返し分ネットワーク遅延時間が重なる。
送信時には以下のエラーを考慮する必要がある。
送信しようとしたが送信キューが既に満杯。(エラーではなく待ちになるかもしれない。)
送信に対する ACK 応答がない。(相手が受け取ったかもしれないが、その受け取りがわからない。)
送信に対する応答がない。
相手から切断された。
通信データを単純なファイル(1次元データ)と見なすことはある程度可能
問題
いつ受信した(送信した)データなのかが不明瞭。
どこからどこまでがパケット(送信されたブロック)となるのかが不明瞭。
受信とその応答となる送信がどのタイミングで行われたかが不明瞭。
型の問題
C99 でサイズが明確になった型 uint8_t, uint16_t, uint32_t を使えるようになったが、既存の API では u_char, u_short, u_int が使われてしまっている。
型が等価であることを確認する必要がある。
実際には内部的に同じ型に対する typedef なので特に何も言われずにコンパイルが通ってしまうはず。
8, 16, 32, 64 のビットサイズを持たない環境は現代ではほぼ皆無なので十分無視できる。(移植性の責任はその奇妙な環境側にあると考えてよい)
shutdown は、送信と受信のどちらを切断するかを指定して切断する。
これ以上受信するつもりがないなら、受信を切断する。
これ以上送信するつもりがないなら、送信を切断する。
片方だけオープンの状態はハーフオープンと呼ばれる。
close は、ファイルディスクリプタを閉じる。もしもそれがソケットの最後のファイルディスクリプタなら通信を切断する。
ファイルディスクリプタは複製されて複数存在している場合がある。単純に1つのファイルディスクリプタを close しても別のファイルディスクリプタは有効のままなので通信は切断されない。この場合、shutdown で明示的に通信を切断することに意味がある。
table:type
u_char uint8_t
u_short uint16_t
u_int uint32_t