Wireless・のおと

サイレックスの無線LAN 開発者が語る、無線技術についてや製品開発の秘話、技術者向け情報、新しく興味深い話題、サイレックスが提供するサービスや現状などの話題などを配信していきます。

前の記事:「GATTのはなし」へ

TCPのはなし

2015年11月30日 09:30
YS
TCPは今日のインターネットを支える中核技術で、人類史上もっとも成功し多用されている通信手順といえます。今回はこの TCP について、ちょっとその内側を紹介してみようと思います。

TCP/IP とは
TCP(Transmission Control Protocol) は俗に TCP/IP と称される通信手順の一要素です。TCP/IP はネットワーク層である IP(Internet Protocol)、トランスポート層である UDP(User Datagram Protocol)と TCPを中核とし、大抵の場合はその下位層に MAC アドレス解決用の ARP や、上位層に IP アドレス自動付加用の DHCP など補助的なプロトコル群が実装されます。
「ネットワーク層」である IP は、指定されたデータをA地点(発信元アドレス)からB地点(宛先アドレス)へ配達する働きを持ちます。IP には 32bit アドレスを使う IPv4 と 128bit アドレスを使う IPv6 がありますが、TCP および UDP は IPv4/IPv6 で共用可能な実装になっています。
IP の仕事は「アドレスにデータを届ける」だけで、その中身が何であるかの意味付けや、データが本当に届いたかの確認は行いません。IP ヘッダには 8bit の「プロトコル」フィールド(※IPv6 ではペイロードと呼ぶ)があり、運んでいるデータの意味付けはこのプロトコル値によって異なります。プロトコル値の代表的なものは次の3つです。

0x01: ICMP
0x06: TCP
0x11: UDP

※註:プロトコル・ペイロード番号の一覧は以下の URL を参照: http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml

ICMP(Internet Control Message Protocol)はネットワーク制御用に制定されたプロトコルですが、現在ではネットワーク制御用としては殆ど使われておらず、主として PING (正確には ICMP ECHO)に使われています。

UDP は一般に「信頼性のないトランスポート階層」と説明されます。「信頼性のない」というのは「データの到着確認を行わない」ことを意味します。UDP はプロトコル層に到着確認手順がないぶん TCP よりオーバーヘッドが低く、また1:多のブロードキャストないしマルチキャスト送信に対応できることが UDP の特徴です。
UDP が「素の」IP と異なるのは、アドレスに加えて送信元・宛先の「ポート番号(16 ビット)」を持っており、これによって1つのアドレス上で複数の異なるサービスが同時に通信できるようになっています。

TCP は「信頼性のあるトランスポート階層」と説明され、ポート番号に加えてデータの到着確認を伴う通信を提供します。通信相手からデータの受信確認が届くまで自動的に再送信が繰り返され、その時間/回数が設定限度を超えるとエラーが通知される仕組みになっています。これに対して UDP は「送りっ放し」なので送信に成功さえすれば送信側には「正常終了」が返され、送ったデータが相手に届いたかどうかは未確認です。

TCP は素の IP や UDP と異なり「状態を持つ(Stateful)」プロトコルで、通信ピア間(送信元/宛先のアドレス:ポートのペア)で仮想回線の接続・切断手順が存在します。この仮想回線のことを「コネクション(Connection)」と呼び、プログラム上でコネクションを扱うための識別子(ハンドル)を「ソケット(Socket)」と呼びます。TCP パケットはコネクションの状態を制御するため「フラグ」「シーケンス」「ACK」「ウィンドウ」のフィールドを持ち、これが今回のブログの主ネタとなります。

tcp04_s.jpgTCPヘッダ構造(表示)

TCP の状態制御
TCP の状態制御は RFC793(1981) の Figure.6 に描かれた有名な遷移図があります。この遷移図が理解できれば TCP の半分は判ったようなものですが、ちょっと一筋縄ではゆかないところがあります。
tcp01_s.jpg
TCP接続状態遷移図 (表示)

青線矢印は受動的な(Passive)状態遷移、赤線矢印は能動的な(Active)状態遷移を示す。
点線矢印は「仕様上は定義されているが、通常はあまり通らない」状態遷移。
状態を示す四角の枠線が太いものは「長時間にわたって維持される」安定状態。
細線の四角は「通信途上で一時的に通過する」状態。

TCP の接続前

状態遷移図の「CLOSED」はコネクションが存在しない状態で、「状態のない状態」とも言えます。アプリケーションがソケットを作成(s = socket(AF_INET, SOCK_STREAM, 0))した状態ではまだ「ハンドルが出来ただけ」で、実際のコネクションは作成されていません。ソケットにコネクションを紐付けるためには、まずソケットに「自分側」のポート番号を付ける必要があり、これは bind(s, sa, sa_len) によって行われます。
bind は「自分側」のポート番号を付けるだけなので、まだコネクションは確立していません。コネクション確立には「相手側」のポート番号を付ける必要があり、そのためには (A) 相手から繋ぎに来るのを待つ(listen)、(B) 自分から相手のポートへ繋ぎに行く(connect)、という2つのパスがあります。前者は Passive Open、後者は Active Open と呼ばれ、通常はサーバー側が Passive/Listen、クライアント側が Active/Connect のペアとして動作します。
なお、TCP の仕様上は両者が相手のポート番号を名指しで同時に connect し接続する手順 (C) も存在しますが、実際にはまず使われていません。


TCP の接続
connect すると SYN フラグの立ったパケットが送信され、これが listen ソケットに受け付けられると SYN+ACK フラグの立ったパケットが返信されます。32bit のシーケンス番号(SEQ)は TCP におけるデータの通し番号を示します。受信側は実際に受理したデータのバイト数だけシーケンス番号を増やし、それを「ACK」として返送します。

SEQ/ACK が「パケット」ではなく「データ」の通し番号であることが TCP の特徴の一つであり、これによって通信経路上におけるパケットの分割・結合がより柔軟にできるようになっています(※)。SYN は SEQ 番号の初期同期のために使われるフラグですが、「コネクションの開設要求」という意味で解釈されることが多いです。コネクションの「乗っ取り」を困難にすべく、SEQ の初期値は 0 や 1 ではなく乱数で生成することが推奨されています。

※註:伝送単位がパケット≒データグラムである UDP ではこういう訳にはゆきません。

TCP の接続状態とウィンドウ制御
SYN / ACK が交換されてコネクションが成立すると「ESTABLISHED」状態となり、データの送受信が可能になります。送信側は TCP ヘッダに続けて長さ(LEN) 0 以上のデータを送信し、それを受信した側は ACK 番号を LEN 進めて ACK を返します。TCP における ACK は独立した機能ではなく常に TCP ヘッダに含まれており、データ通信に「相乗り」できることも特徴の一つです。
送信側は ACK の到来を待たずに後続データを送信することができます。一体どこまで先回しにデータを送ってよいかは受信側のバッファ能力によって決まり、これは「ウィンドウ(WIN)」という値で示されます。WIN は SYN / SYNACK 交換時に交換され、送信側はデータを送信するたびに WIN 値を減らします。例えば WIN 初期値が 2000 でパケットデータ最大長(MSS)が 1024 だった場合、一回目のデータ送信では 1024 バイトを送れますが、二回目は 976(=2000-1024) バイトしか送ってはいけません(※註)。送信側の WIN 値が 0 になると送信は停止し、次に ACK を受信するまで送信は「おあずけ」になります。

※註:実際には Congestion Avoidance や Slow Start Algorithm などの仕様が適用され、もう少し複雑になります。


tcp02_s.jpgウィンドウ制御の様子(表示)

MSS とは Maxium Segment Size の略で、分割・再構成なしに送受信できるデータの最大長を指します。通常はローカルリンクのネットワーク仕様から参照されますが(例えば IEEE802.3 有線ネットワークの最大データ長(MTU)は 1500 バイト、IP および TCP ヘッダサイズを引くと MSS=1420 バイト)、接続相手の MSS まではわからないので、SYN および SYNACK にはローカルリンクの MTU を TCP MSS option (RFC879) として付加する実装が一般的です。

受信側は受信データを引き取るたびに WIN 値を増やし、必要に応じて送信側に ACK として返送します。これを「Window Update」と呼びます。送信側が WIN=0 で停止していた場合は、Window Update によって通信が再開します。
WIN=0 で停止したコネクションの Window Update パケットが通信路上で失すると「互いが互いを待っている」デッドロック状態に陥ることがある(※註)ため、WIN=0 で停止したコネクションの送信側は一定時間(数十秒~数分)ごとに LEN=0 のパケットを送って ACK 返送を促す実装が推奨されます。この動作のことを「Window Probe」と呼びます。

※註:WIN=0 で Window Update パケットが「偶然」消失した時にしか発現しないので、再現頻度の少ない厄介なトラブルになります。かつてプリントサーバ開発を主業務にしていたときは、意図的に Window Update を消失させるコードを仕込んで通信停止しないことを確認したりしていました。なお後述の KeepAlive には副次的に、Window Update 消失による通信停止を回避する効果があります。

tcp03_s.jpgWindow Probe の様子(表示)

TCP の WIN ヘッダは 16bit 幅で、かつては最大 65535 バイトでした。LSI 集積度が上がってメモリ容量が増大するとより大きなウィンドウサイズの実装が求められるようになり、RFC1323 で「Window scaling option」が追加されます。これは WIN 値への倍数を 8bit のシフト値として追加したもので、理屈上は 65535x2^255 という無茶苦茶な値(3.8x10^81)まで拡張できますが、仕様上は最大 14 (WIN 1 あたり 16KByte、最大ウィンドウサイズ 65535x2^14=1GByte)までに制限されています。

通信経路上でパケットが消失した場合、受信側は「最後に受理した SEQ+LEN」を ACK として返送し、送信側はこれをもってパケット消失と再送の必要性を知ることができます。しかしウィンドウサイズが大きくパケット消失が多発する場合、ウィンドウ内に「虫食い」的にパケット消失が発生し、このとき単純な「最後の SEQ+LEN」方式では効率的な再送ができません。これは特に無線ネットワークにおいて顕著で、後に RFC3517 で選択的 ACK(Selective ACK または SACK)という拡張仕様が定義されました。SACK はビットマップによってデータの受信状態を示すもので、これによってパケット消失多発時の通信効率は向上しますが、実装はそれなりに複雑なものになります。IoT のように通信データ量が少なくかつ実装リソースの最小化が要求される場合、WIN サイズを小さめ(数キロバイト)として SACK は実装せず古典的な SEQ/ACK 実装にとどめるほうが有利かも知れません。


TCP の切断
TCP の通信終了には3つのパターンがあります。FIN による通常切断、RST による強制切断、そして通信の途絶による時間切れ(タイムアウト)切断です。

FIN による通常切断

FIN は「切断要求」ではなく「送信終了」を意味します。接続相手からの FIN を受信(D)すると最後の SEQ 番号+1の ACK (Last ACK) が返送され、それ以降受信側での recv() は EOF (-1) を返すようになりますが、send() は引き続き有効です。これを俗に片道クローズと呼び、状態遷移図では FIN を送信した側が FINWAIT2、受信した側が CLOSE_WAIT となります。
BSD socket API では close() を呼び出すとソケットがアプリケーションから切り離され、FIN が送信されて切断手続き(E)が始まります。ソケットを活かしたまま FIN を送りたい、すなわち意図的に片道クローズを使いたいときには shutdown() を用います。shutdown の how 引数は SHUT_RD(0), SHUT_WR(1), SHUT_RDWR(2) いずれかの値を取り、SHUT_WR のとき「FIN を送信して以降の送信を止めるが、受信は引き続き可能とする」という片道クローズの状態になります。
一般的な socket アプリケーションの実装では送信側から close() を呼び出して切断手続き開始、受信側は recv() のエラーを受けて close() が呼び出され、受信側からも FIN が返されてコネクション切断が成立します。


RST による強制切断
FIN がアプリケーションからの API 呼び出しによる「穏やかな切断(Gentle Disconnect)」であるのに対し、RST はコネクションの強制切断です。RST を受けると TCP バッファ上の未送信データや受信済み/未受理データは消去されます。このように RST 強制切断では TCP の特徴である「データ配達の確実性」が失われれるため、アプリケーションから意図して RST を発行することは普通はありません(※註)。

※註:ただし BSD socket API ではソケットオプション SO_LINGER で l_onoff=1, l_linger=0 に設定して close() を呼び出すことで RST 切断を選択することもできます。


タイムアウトの検出と切断

TCP のタイムアウト切断は送信側において、データ送信後一定時間内に ACK を受信しなかったときに発生します。逆に受信側は「通信が途絶した」のか、「単にデータが送られてこない」だけなのかわかりません。recv() でデータ待ち受け状態で置いておいて、接続先相手をいきなり電源切断した場合、recv() のコネクションは永久にデータ待ちを続けることになります。これは即座に致命的障害は招きませんが、永久データ待ちのプロセス・コネクションは徐々にシステムリソースを枯渇させてゆくメモリリークの原因になる場合があるので、放置すべきではありません。
これを防ぐためにはアプリケーション側でデータ受信タイムアウト(BSD socket では select() を使用)を設定します。TCP のオプションとして、有効送受信データが無い場合でも一定時間ごと(30 分など)に LEN=0 のパケットを送信して生存確認する「KeepAlive」という機能もありますが、KeepAlive は実装必須の機能ではなく、実装されている場合もデフォルト OFF で使用しないことが推奨されています(※註)。

※註:「有効データを伴わないパケットを、生存確認のためだけに流すのは雑音であり無駄である」という IETF の方針というか哲学というか趣味に基づく推奨です。

KeepAlive の動作は前述の Window Probe とよく似ており、実装系によっては KeepAlive を禁止すると Window Probe まで止まってしまうバグ持ちのプロトコルスタックがあったりもします。TCP/IP の KeepAlive Timeout はこのようにあまり頼りにならないため、確実性を求められる場合は前述のようにアプリケーション層で受信タイムアウトを実装することが慣例となっています。


そのほか

URG

TCP ヘッダには SEQ, ACK, WIN の他に 16bit 幅の URG (Urgent Pointer) という値も含まれています。もともと通常のデータ通信より優先して配送される(受信済みでバッファに格納されているデータよりも先にアプリケーションが受け取る)緊急データ(Out of band, OOB)という機能を想定したもので、SEQ+URG に「来るべき位置」のデータが含まれていることを URG フラグによって示すという仕様でした。昔の BSD Unix では TELNET でアボート(Ctrl+C)を伝えるために使われたりもしましたが、現在では URG 機能は「存在するけど使われない」盲腸になっています。

TCP と IPv6

TCP(UDP も) は殆ど IPv4 層に依存しないよう設計されており、ほぼそのままの仕様で IPv6 上でも稼動します。唯一の依存はヘッダのチェックサム計算に送受信アドレスを含めるところ(※註)で、ここはスタックの内部で IPv4/v6 に応じて処理を変える必要があります。

※註:疑似ヘッダ(pseudo-header) と呼ばれます。


TCP とセキュリティ
TCP 自身はセキュリティ(暗号化)拡張オプションを持ちません。暗号化つき TCP は様々なかたちで提唱されたのですが、一つも根付きませんでした。IETF の公式見解としては、IP および IPv6 層のセキュリティ規格(IPsec)をもって TCP のセキュリティを実現するとしていますが、現実的には TCP の上位層にあたる SSL/TLS が「暗号化 TCP のようなもの」として広く使用されています。

TCP の限界

もちろん TCP は万能ではなく幾つもの限界があります。TCP はあくまで2点間・1対1での連続データ転送を実現するプロトコルであり、1対多のマルチキャスト配送や、不連続なデータの扱いは苦手です(というか、基本的にできません)。「不連続なデータ」というのは例えばストリーミングのビデオ中継で、ネットワークが一時停滞・再開しても停滞中に途切れたところは飛ばし、現在最新の映像から配送を再開するような機能です。こういった用途には RTP (Real-time Transport Protocol) SCTP (Stream Control Transmission Protocol) などが提案されているのですが、いまひとつ普及の勢いが無いように感じています。


まとめ
最初の TCP 仕様である RFC793 の発表は 1981 年 9 月です。それから 30 年以上、TCP はインターネットの主幹プロトコルとして文字通り世界中の人に使われ、高度な科学情報データや巨額の金融取引、数知れぬスパム広告からムフフな動画に至るまでありとあらゆる種類のデータ転送を担ってきました。TCP 自身も再送アルゴリズムを何度も改訂し、MSS Option, Window Scaling や SACK など後付けの仕様拡張を重ねてきましたが根幹のアルゴリズム(SEQ, ACK, WIN)は原型のままで、後付け拡張を許した懐の深さも合わせて設計の優秀さを物語っています。
しかし TCP/IP(v4) は歴史的な成功を収めた反面、後継者には恵まれていません。アドレス空間を拡大した IPv6 は仕様制定後 20 年も経ったのに遅々として普及せず、IPv6 と同時に普及するだったはずの IPsec よりも SSL/TLS が事実上の暗号化標準となり、リアルタイム・マルチキャスト対応のトランスポート層も今一つ普及していません。この状況が一時的(という割には長い時間ですが)な停滞に過ぎないのか、実は TCP/IP(v4) は「実用上充分」であってそれ以上は必要とされない余計な発明なのか、今後も見守ってゆきたいところです。



次の記事:「IPv6のはなし」へ

最新の記事

カテゴリ

バックナンバー