TCP/IPについての自分なりのまとめ。
TCPはTransmission Control Protocolの略。RFC793で規定されている。
いろんなサイトでもっと細かく丁寧に説明されているので、ここでは超概要だけ。
アプリケーションから渡されたデータにTCPヘッダーとIPヘッダーを付けて相手に送信する。
通信相手のアプリケーションは「IPアドレス」と「ポート番号」で特定するが、IPアドレスはIPヘッダーで指定するので、TCPヘッダーにはポートだけ入れる。
また、TCPヘッダーにはその電文がどういう種類のものかを示すフラグがある。
例えば「SYNフラグが立った電文を送る」と言うのを略して「SYNを送る」という言い方をすることが多い。
これらのフラグの使い方はTCPとしては当然決まっているが、アプリケーションからはTCP/IPをソケットで扱うことが多い為、ソケットを使っている限り意識することは無い。
しかしsnoopコマンドやツールでTCPの電文(パケット)を確認するときには必要な知識だ。
フラグ | 読み | 概要 |
---|---|---|
SYN | スィン | 接続開始を示す。 |
ACK | アック | 電文を受け取ったことを示す。 |
PSH | プッシュ | データ部を速やかに相手先アプリケーションに渡すべきであることを示す。 |
なし | データ部が付いている。 | |
FIN | フィン | 送信を終了する(切断する)ことを示す。 |
RST | リセット | 異常なコネクションに対する応答を示す。 |
片方が他方に対して何らかの電文を送ると、相手は受け取ったという印にACKを返す。
ACKが来なければ相手が受け取っていないということなので、その場合はある程度待ってから再送する。
ACKは他の電文と一緒に送ってもよい。
コネクションは、【相手先IPアドレス・相手先ポート・自分のポート】の組で一意に表される。
コネクションは現在どういう状態にあるかを示すステータスを持っており、イベントに応じて遷移していく。netstatコマンドで表示されるのは、これ。
ソケット関数を呼び出すと、ソケットライブラリ(プロトコルスタック?OS?)がTCP/IPの規約に従って通信を行う。
サーバー側で接続の受付待ちを開始する。
コネクション(通信相手はいないので、相手先IPアドレス・ポートは無し、自分のポート番号だけ有り)はLISTEN状態になる。
クライアント側アプリがconnectを呼び出すと、クライアントからサーバーへSYNが送られる。
サーバー側アプリがlistenしてさえいれば、(アプリケーションがacceptしなくても、)サーバーからクライアントへSYN・ACKが送られる。
クライアントがサーバーからのSYNに対してACKを送ると、コネクションが確立したことになる。
(↑スリーウェイハンドシェイク)
こうしてコネクションが確立するので、サーバー側アプリがacceptしなくてもクライアントは電文を送信できるようになるので注意!
クライアント側のコネクション(ソケット)はESTABLISH(通信可能)状態になり、
サーバー側のリスニングソケットはLISTEN状態のままで、接続済ソケットはESTABLISH状態になる。
サーバー側のリスニングソケットは【相手なし・自分のポート】のコネクションで表されているが、
接続済ソケットは通信相手が決まったので【相手IPアドレス・相手ポート・自分のポート】で表される。
サーバー側のポート番号は「自分のポート」の1種類しか無いが、コネクションは相手先アドレスまで含めて認識するので別物になる。
SYNを送る際には、適当な番号が一緒に送られる。
以降、電文を送信する度に送信したバイト数が加算されてゆき、全体から見て何バイト目を送ったかを相手に知らせる効果を持つ。
逆にACKを返すときは、この番号を元にどこまで受け取ったか(厳密には「次に欲しいのが何バイト目からか」を示すんだったと思う)を相手に知らせる。
この番号により、電文の順序が入れ替わったり間の電文が抜けたりしても、TCPライブラリがそれを認識して自動的に処理してくれる。
アプリケーションがsendを呼び出すと、相手に電文が送信される。
ただし、ソケットライブラリが実際に送信する電文とsend呼び出しが1:1に対応しているとは限らない!
すなわち、複数のsendを1つのパケットとして送信することもありうるし、1つのsendを複数のパケットに分割して送信することもありうる。
(送信準備にかかる時間や、相手から示されたウィンドウサイズ(「何バイトまでなら受け取れるか」を示すサイズ)等によって変わると思われる)
これは、send呼び出しが正常に戻ってきたとしても、相手がきちんと受け取ったかどうかは不明、ということを意味する。
例えば複数のsendを1つにまとめるということは、1回目のsend呼び出しが戻った時点では まだ実際の送信は行われていないということだ。
TCPレベルでは ACKが返ってくることによって相手が受け取ったかどうかをチェックできるが、アプリケーションレベルのsendでは、それは分からない。(そもそもTCPでは、複数のパケットに対してACKをまとめて1回だけ返すのでもよいとされている)
さらには、相手が受け取ったとしても、それは相手のソケットライブラリが受け取ってバッファに入れただけのこと。相手のアプリケーションが実際にrecvしたかどうかは、やはり分からない。
アプリケーションがrecvを呼び出すと、ソケットライブラリの受信バッファに入っているデータが読み出される。
ソケットライブラリはパケットを受け取ると受信バッファにどんどん追加していくので、アプリケーションから見ると電文の区切りは分からない。
ソケットライブラリがFINを受け取っていたら、受信バッファが空になった以降のrecv呼び出しは、切断されていることを示す値が返される(0か-1かExceptionか…)。
アプリケーションがshutdown(書込)を呼び出すと、FINが送信される(このFINに対するACKが
相手から返る)。
この後、自分からsendを行うことは出来なくなる(相手のsendや自分のrecvは無関係に行える)。
なお、shutdown(読込)はTCPレベルで行うことは何も無い(その後でrecvを呼ぶとエラーになるだけ)。
ソケットをクローズ(コネクションを終了)する。
クライアント側のソケットおよびサーバー側の接続済ソケットは、(まだFINを投げていなければ)FINを送信する。
お互いにクローズする場合は、一方から他方へFINを送ると 他方からFIN・ACKが返ってくるので、さらにそれに対してACKを返す。
(最初の接続の場合と異なり、サーバー側とクライアント側のどちらから切断しても構わない。)
最初にクローズした(アクティブクローズ)側は、最後に送ったACKが相手に届いたかどうかを確認することが出来ないので、ネットワーク上に滞留した電文(ゴミ)が消えるまで待つ為の再送待機(TIME_WAIT)状態になり、一定時間(2〜4分?OSによって異なる)経つとCLOSED状態になる。
後からクローズした(パッシブクローズ)側は、比較的すぐにCLOSED状態になる。
(CLOSED状態になるとコネクションが解放されているので、netstatコマンドで見たとしても何も表示されない。)
クローズ後にソケットライブラリが電文を受け取った場合、ACKでなくRSTを返す。
(RSTは、受け取るべきソケット(コネクション)が存在しない事を示す)
また、アクティブクローズ側がSO_LINGERオプションで時間0をセットしている場合、TIME_WAIT状態にならずにいきなりCLOSED状態になる。
このとき、(ライブラリによっては?)RSTを送信するみたいだ。これにより相手にソケットがクローズされたことを知らせて何も送ってこないようにして、待機状態を回避するんだろう。もっとも、RSTが相手に届く保証も無ければ変な電文が送られてこない保証も無い訳で、正しい手順では
やはりTIME_WAIT状態になるべきだろう。
ただ よく知られている通り、TIME_WAIT状態になっているポートがあると解消されるまでそのポートは使えなくなり
ポートが枯渇するかもしれないという問題があるので、場合によっては使えるかも。まぁメリット・デメリットが両方あるからこういうオプションがあるんでしょうねぇ。
通信相手のソケットが無い(コネクションがクローズされた)とき、何か電文を送ると相手からRSTが返ってくる。
ソケットライブラリがRSTを受け取った後だと、アプリケーションがsendやrecvを呼び出すとエラーが返る。通信相手がいなくなっちゃったんだから仕方ないねぇ。
shutdown(読込)を行ってもTCPレベルでは何も動作しないが、RSTを投げたらどうだろう?
shutdown(読込)を行うと、それ以上アプリケーションではrecvできないにも関わらず、電文を受信するとソケットライブラリはACKを返す。
これでは送信側は電文が送信できたと認識してしまうが、実際には受信側アプリケーションが電文を受け取ることはないので、受け取れなかったということを示す電文を返すのがいいのではないか?
これに該当するのはRSTしか無いので、RSTを送ったらいいのではないかと考えた。
しかし実際にはこのようなケースでRSTが送信されることは無い。
RSTが送られるのは、(既にクローズされた等の)異常なコネクションに対して電文が来たとき。すなわち読みも書きも出来ないとき。
shutdown(読込)をしただけではまだ書込(送信)は出来るので、RSTを投げるわけにいかない。(相手はRSTを受け取るとコネクションが無くなったと認識するので、こちらからの送信も受け入れなくなってしまう。)
ではshutdown(両方)ならRSTを投げていいか?
しかしRSTが来るとFINは投げなくなるので、正常な終了が出来なくなってしまう。
shutdown(両方)は送信も受信もできない状態ではあるが、コネクションはまだ生きている(ハーフコネクション状態ではあるが)ので、RSTを返すのは妥当ではない。
close後に電文を受け取ると、それはもう異常なコネクションなので、RSTを返すのは妥当。というより、そのためのRSTなんだろうな。