『ネットワークはなぜつながるのか』読書記録:第2章 TCP/IP のデータを電気信号にして送る

 第1章はブラウザに入力した URL が DNS 問い合わせで名前解決され, OS のプロトコルスタックに必要な情報を渡すところまでに焦点を当てていました。

www.kangetsu121.work

第2章では, ブラウザから送信するよう渡されたデータをプロトコルスタックが TCP/IP の形式にし, サーバと通信を確立してやり取りをするところに焦点化しています。
例によって本記事は網羅的ではないので, 詳細は本を読んでみてください。
また, 誤りがあればお手数ですがご指摘いただけると嬉しいです。
一旦自分の理解まとめ用に記事に起こしていますが, 随時見直していく予定ではあります。

ネットワークはなぜつながるのか 第2版 知っておきたいTCP/IP、LAN、光ファイバの基礎知識

ネットワークはなぜつながるのか 第2版 知っておきたいTCP/IP、LAN、光ファイバの基礎知識

パケット送信の流れ

 パケット送信, 今回の例だと HTTP リクエストデータの送信の流れは以下の通りです。

  1. アプリ内のライブラリ (Socket ライブラリ) で socket 作成の system call socket 呼び出し
  2. OS プロトコルスタック TCP 担当部分が TCP ヘッダをパケットに付加
  3. OS プロトコルスタック IP 担当部分が IP ヘッダをパケットに付加
  4. SW である LAN ドライバが HW である LAN アダプタをコントロール
  5. HW である LAN アダプタが実際に電気信号を送信

Socket とは

 上の説明で出てきた socket について。
socket は, 通信の制御情報を記録した OS 上のメモリ領域です。
宛先 IP アドレス, ポート, 通信進行状態, socket 識別子であるディスクリプタなどをプロパティとして持つオブジェクトと考えればよさそう。
socket の状態は, netstatss コマンドで様子を見ることができます。
なお, HTTP 通信などで使われる TCP と異なり, UDP でも socket を作成しますが, UDP は特定の IP やポートとは結び付かないため必要な情報が少ないようです。

socket 作成はどんなアプリでも共通して必要とする機能なので, これも OS が担います。
アプリから Socket ライブラリを通して sokcet の system call が呼ばれると OS が socket を作成し, その socket を区別する識別子 = ディスクリプタをアプリに返します。
アプリは通知されたディスクリプタで socket を識別できるため, リクエスト - レスポンスの対応などを取ったり, 連続するパケットをちゃんと連続して送れるようですね。

Connection の作成: クライアント側 socket とサーバ側 socket をつなぐ

 実際に通信をするときは, クライアントとサーバ両方で socket を用意し, 接続する = connection を作成する必要があります。
第1章にあったように, socket 作成の system call を呼び出す段階ですでに名前解決は済んでいるので, socket を接続する system call connect を呼び出すときにこの情報を引数として渡しています。
以降, この connection を使ってデータをやり取りしていきます。
また, 受信データが溢れないように, OS は受信データ一時格納用のバッファメモリもこのタイミングで確保しています。

  1. socket を作成
  2. アプリが通信相手の情報, 1で作った socket の descriptor を渡して connect を呼び出す
  3. OS プロトコルスタックが socket をサーバ側 socket に接続
    • このときクライアント側の制御情報をサーバ側に伝えている
  4. 送受信データ一時格納用のバッファメモリを確保 (system call としては mmap を使っている様子)

制御情報は大別して 2種

 さて, 現段階ですでにいろいろな説明がありましたが, そもそもデータ送受信って何をしたかったのでしょうか。
立ち戻ると, ブラウザに URL を入力したのは, HTTP リクエストを送って Web ページを Web サーバに送ってもらうことが目的でした。
この目的だけからすると HTTP リクエストメッセージだけ送ればよさそうに見えますが, そうはいかず, ちゃんとエラー検出ができる仕組みや, そもそも宛先 IP アドレス・ポートの情報などが必要です。

こうした情報, 制御情報は大別して 2種あり, それぞれ以下になります。

  1. TCP ヘッダ, データやり取りの度にパケット先頭に付加するもの
  2. socket に記録してプロトコルスタックを制御するもの

これらがあるおかげで, 例えば HTTP が HTTP メッセージのやり取りに専念できたりと, よい分業ができているわけですね。

connection 作成の具体的な流れ

 では, 次に connection を作るときの詳細な流れを見てみます。
ここでは TCP 担当の働きに焦点化して, 他は省略しています。
また, まだ説明がない言葉もありますが後ほど説明します。

  1. アプリがディスクリプタ, IP, ポートを引数として渡して system call connect を呼び出し
  2. プロトコルスタックの TCP 担当が TCP ヘッダに制御情報付加
    • SYN bit 1
    • シーケンス番号初期値
    • ウインドウサイズ
  3. IP 担当がパケット送信
  4. サーバ側プロトコルスタック IP 担当が受け取って TCP 担当に渡す
  5. サーバ側プロトコルスタック TCP 担当が LISTEN な該当する socket にパケットを渡し進行状態を記録
  6. TCP 担当が制御情報付加
    • SYN bit 1
    • ACK bit 1
    • シーケンス番号初期値*1
    • 受信データ連番である ACK 番号*2
    • ウインドウサイズ
  7. クライアント側 TCP 担当が受け取り SYN 1 なら socket に通信情報記録
  8. クライアントが ACK bit 1, ACK 番号を送り, 届けば接続 = スリーウェイハンドシェイク 完了

データ送受信の流れ

 上の流れで, connection = データやり取りの経路が作成できました。
次は実際にこの経路を使ってデータをやり取りする動きの流れです。

  1. アプリがデータをプロトコルスタックに渡す
  2. プロトコルスタックはデータをすぐに送らずバッファメモリにためる
    • このとき MTU((Maximum Transmission Unit, 送信可能な最大フレーム長)) - TCP/IP ヘッダ = MSS((Maximum Segment Size, MTU から TCP/IP ヘッダ分を引いたサイズ)) まで最大ためられる
  3. MSS までデータがたまるか, 一定時間経つとプロトコルスタックがデータ送信
  4. 受信側は TCP ヘッダのシーケンス番号と受信済みデータから ACK 番号を計算・付加して返信

ACK 番号待ち時間 = パケット再送間隔は動的に変化させている

 上の説明で, データ送信後, 受け手が正常に受信したことを伝えるために ACK 番号を返送する, とありました。
ところで, 送り手は ACK 番号が返ってくるのをいつまで待てばよいのでしょうか。
いつまでも待ってるとデータ転送効率が悪く, ユーザーはストレスを感じてしまいます。
このため, ある程度経って ACK 番号が返ってこなかったら, 送り手はパケットを再送します。

 しかし, この仕組みでもまだ問題があります。
ネットワークの状況によって, 正常なデータ転送にかかる時間は様々です。
同じ LAN 内のやり取り, 国内のやり取り, 海外とのやり取り, また各種機材のスペックなどでも変わってくるでしょう。
こうした環境要因があるため ACK 番号待ち時間は一律に決められないので, TCP は ACK 番号の平均往復時間を計測していて, 都度動的にタイムアウト時間を変化させているようです。

ウインドウ制御方式でデータ転送効率をさらに上げる

 タイムアウト値を動的に変更することで, 無駄な待ちや重複送信などが起こりにくくなり, データ転送効率が上がりました。
しかしまだ改善の余地があります。
通常イメージするデータやり取りの方式は, いわゆるピンポン方式と言われる, 送信 -> 受信 -> 送信 ...... です。
送ったらその返事を待って, 返事が来たら次を送るかたちですね。

しかしこれでは, 無駄にデータのやり取り回数が増えてしまいます。
返事を待たずに連続してある程度のデータを送ることができれば, その分返事を送るプロセスを省略できます。
これを実現するのが ウインドウ制御方式 と呼ばれるものです。

ウインドウ制御方式を実現するために, TCP ヘッダには ウインドウフィールド があり, ここにはスリーハンドウェイシェイクの段階で, バッファメモリサイズを記録しています。
送り手はここを参照し, それを超えない範囲でデータを連続して送ることができます。
受け手はデータを正常に受信し, メモリバッファからサーバ側のアプリにデータを渡せたらたら, その分メモリバッファが空くので利用可能なウインドウサイズを更新し, ウインドウフィールドに今利用可能なウインドウサイズをセットして送り返します。
また, このとき ACK 番号の返信用のパケットと相乗りさせることができるなら, できるだけ相乗りさせて送るようにして, データ転送効率を上げています。

connection 切断

 こうしてデータ送受信が無事完了したら, 今度は connection を切断します。
Web アプリなどにもキャパシティがあるので, 利用が終わった connection はちゃんと切断してあげないと新しい connection を作れなくなってしまうので, 使い終わったら後片付けをちゃんとする感じですね。

  1. 切断側が FIN bit 1 で送信
  2. 受信側が ACK 番号を送信
  3. アプリの read に対しデータ受信完了を system call の返り値で通知
  4. アプリが close 呼び出し
  5. 受信側も FIN bit 1 で送信
  6. 切断側が ACK 番号を送信
  7. しばらく待って socket 抹消

パケットが運ばれる方法

 これまででデータを送受信する仕組みは見えました。
今度はもっと下のレイヤー, より物理的な方面を見てみます。
クライアントとなるマシンとサーバとなるマシンは直接つながっているわけではなく, 間に多くのハブ, ルータなどがあります。 まずはハブやルータなどを, データがどのようにして通っていくかを見てみます。

  1. ルータ (IP ルールで動くもの) が次のルータ = IP 中継地を示し
  2. ハブ (イーサネットルールで動くもの) がサブネット*3内でそこまでパケットを運ぶ
  3. IP アドレスのネットワーク部を見れば方向がわかる*4ので, プロトコルスタック IP 担当はその方向にあるルータの MAC アドレスを MAC ヘッダに付加
  4. サブネットをまとめるハブに到着すると, ハブが MAC ヘッダの値と MAC アドレステーブルを突き合わせて中継
  5. ルータに着くと IP ヘッダの値と IP ルーティングテーブルを見て新しい MAC ヘッダを set し, 宛先 IP の方向のサブネットに送信

  6. プロトコルスタック内部の動き

    1. TCP 担当がパケットに TCP ヘッダを付加し, IP 担当にパケット送信を依頼
    2. IP 担当が IP ヘッダ, MAC ヘッダを付加
    3. NWSW (LAN ドライバなど) がそれを受け取って NWHW (LAN アダプタなど) に渡す
    4. NWHW がデジタルデータを電気・光信号に変換して送信
    5. ハブ, ルータを中継して目的のサーバに届く

パケットの宛先の決定方法

 パケットが運ばれる概要は上の通りですが, たくさんあるハブやルータの中から, どこにパケットを中継すればいいかはどう決められているのでしょうか。
これを決めるために使われれうのが, 説明もな句うえで出てきた MAC アドレステーブルやルーティングテーブルというものです。

まずパケットを送る第一中継点を決めなければいけないのは, 大元のクライアントマシンです。
クライアントは IP ルーティングテーブルと言われる, 表を持っていて, これを参照して送り先を決めることができます。
なぜそんなことができるかというと, IP アドレスは階層 (グループ) を持っているためです。
一対一対応で全ての機器と IP アドレスを紐づけた参照表を持ち続けると徒はほぼ不可能ですが, サブネット単位なら何とかなります。
第一オクテットから順に見ていって, 一致する方向の ゲートウェイ = ルータ にパケットを送ればよい, ということになります。

なお, Windows では route print などするとルーティングテーブルが見えます。
この時の1行目が デフォルトゲートウェイ で, 該当する方向のレコードがないときにとりあえずパケットを送る先となります。
表示項目をいくつかピックアップすると, こんな感じです。

  • Interface: パケットを送るべき NIF, NIC が複数の時などはそれそれがつながる NW が異なったりするので必要
  • Gateway: 中継点となるルータの IP アドレス
  • Destination Netmask0.0.0.0 の行はデフォルトゲートウェイを表す, 該当行がないときこのの Gateway に送信される

IP 担当部分による MAC アドレス解決方法

 これでどうやって中継点を解決しながらパケットが運ばれるか, 想像がつきました。
しかし, 実はまだ問題があります。

ルーティングテーブルを見ることで IP アドレスはわかったのですが, これはあくまで IP の世界の話です。
その下, 物理的な LAN ケーブルの接続・伝送などに関わるイーサネットの世界では, そもそもの仕様が違うので IP を理解できません。
機器を識別するのに, IP アドレスの代わりに MAC アドレスを使うので, なんとかして IP アドレスを MAC アドレスに変換する必要があります。
このとき, IP を理解できる IP 担当がこの解決をすることになります。
そして, この時に使われるプロトコルが ARP*5です。
IP -> MAC アドレスの解決の流れは以下です。

  1. ARP キャッシュを見る
  2. キャッシュがなければ ARP でサブネット内の機器全てにブロードキャストを使って問い合わせ

LAN アダプタによる電気信号送信の方法

 次はもっと下, LAN アダプタの動きです。
流れは以下。

  1. 初期化時に LAN ドライバが LAN アダプタ内の MAC 回路に MAC アドレスをセット
  2. LAN ドライバがパケットを LAN アダプタ内のバッファメモリにコピー
  3. MAC 回路がパケット先頭にプリアンブル, スタートフレームデリミタ (SFD) を, 末尾に フレームチェックシーケンス (FCS) を付加
    • プリアンブル: クロック信号のタイミングを計るのに使う 01 データの連続 56 bit
    • SFD: パケットのデータの区切りを示す, ここから MAC ヘッダ
    • FCS: エラー検出用

これでようやくパケットが完成です。

信号の物理的な送信

 ちょっと視点を変えて, パケットの中継点であり, サブネットをまとめるハブの種類についてです。
ハブにはリピーターハブとスイッチングハブの大きく 2種類がありますが, 現行では大体スイッチングハブが使われているようです。
ただ, 本書では出版年が古いこともあってか, またはより原理的なところを見るためか, リピーターハブの動きについて解説しているのでここでもそれに倣います*6
2種類のモードはそれぞれ以下です。

  1. 半二重モード: リピータハブのモード, 送受信どちらかだけを一時に行う, 信号衝突を避けるため信号が流れていないかを調べてから送信
  2. 全二重モード: スイッチングハブのモード, 衝突を考慮せず送受信を一度に行える

半二重モードの仕組み

  1. MAC 回路が電気信号にデータを変換し, PHY (MAU) という送受信担当部分に渡す
    ※このときの変換速度が伝送速度
  2. PHY (MAU) がケーブルの種類ごとに適切に変換して送信
    • 信号衝突が起こった場合はジャミング信号*7を出し, 一定時間後に再送する
  3. PHY (MAU) 回路が受信したデータを共通形式変換
  4. MAC 回路がデジタルデータに変換し, バッファメモリにためる
  5. FCS を検査
  6. MAC ヘッダを見て自分宛ならバッファメモリに保存
  7. LAN アダプタが割り込みを使って CPU にデータ受信を通知
  8. CPU が作業中の処理を棚上げして LAN ドライバに処理を命令
  9. LAN ドライバが MAC ヘッダのタイプフィールドを見て適切なプロトコルスタックに渡す
    • 不適切な IP パケットが届いた場合は ICMP*8 でエラー通知
  10. TCP 担当が IP ヘッダを見て適切な socket に渡す

信号衝突の可能性があるので, そうならないような仕組みが備わっています。
信号送信前にほかの信号が流れていないか確認する点と, 2 のジャミング信号あたりがポイントのようですね。

DNS 問い合わせについて

 以上が TCP/IP メッセージ送信の概要でした。
ところで, UDP の話が出てきませんでした。
UDP は, TCP ヘッダのような各種制御情報がなく, パケットロスとなどのリスクはあるけどその分データ転送効率が良いプロトコルのようです。
例えば DNS 問い合わせなどで使われているみたいですね。
名前解決の失敗などのエラーはアプリ側で検知できるため, UDP 側でこうしたエラーチェックの仕組みを持たなくてもキャッチできるようになっています。

まとめられてない

 以上です。
まとめられてません。
ただ, こうして文字に起こすための情報整理の過程が, 自分のための勉強になったことっは確かです。
学習の中で自分で手を動かして検証などはしているので, こちらの記事に紐づけてそれら検証内容を個々に書いていくことで補足できたらと思います。

本記事は度々更新が入るかもしれませんが, こうでもしないとなかなかまとめができない性分なので, これからも継続的に更新する前提で学習記事は書いていく予定です。
最終的に, 他の方が見ても学びのあるものになれば......!

*1:セキュリティ上乱数を初期値とする

*2:ACK bit ≠ ACK 番号, ACK bit は 2値

*3:一つのハブでまとめられたコンピュータ, ハブ, ルータの集合をサブネットと呼ぶ

*4:IP は階層的なので

*5:Address Resolution Protocol

*6:第三章でスイッチングハブの説明もするらしい?

*7:MAC アドレスを基にした乱数を生成しているので被らない

*8:Internet Control Message Protocol