Bo2SS

Bo2SS

7 ソケットプログラミングの基礎

コース内容#

ソケットとは何か?ネットワークプログラミングは何をするのか?

  • TCP/IP の五層モデル、OSI の七層モデルを理解する
  • 類比
    • ソケット —— 配達員
    • トランスポート層 —— 配達会社:TCP—— 某豊配達会社、UDP—— 某通配達会社
    • 交通輸送道路 —— インターネット
    • 通信アドレス ——IP

—— トランスポート層プロトコル ——#

配達会社の類比
開発者は TCP または UDP プロトコルを選択することしかできず、プロトコルパラメータを変更する

TCP#

伝送制御プロトコル;接続指向で、信頼性のあるデータ伝送プロトコル

  • 接続:三回のハンドシェイク [詳細は追加知識点を参照]
  • 信頼性の本質:確認と再送 [順序番号が必要]
    • もし失われた場合は再送される
    • [PS] 双方はお互いの状態を示す変数を保存する
  • ヘッダ形式
  • 画像
  • ソースポート番号:どのポートから発信されたか;ターゲットポート番号:どのポートに送信されたか
    • 異なるポートは異なるアプリケーションに対応
    • コンピュータをビルに例えると、ポート番号はビル内の部屋番号
    • IP アドレスは IP 層によって提供される
  • シーケンス番号:何回目の通信かを示す;確認応答番号:次回通信の際に期待される相手のシーケンス番号
  • ヘッダ長:単位はワード [一般的に 4 バイト]
  • 機能ビット[黄色の部分に注目]
    • ACK:確認
    • RST:接続リセット [次回接続を拒否]
    • SYN:接続要求 [三回のハンドシェイクの最初の二回で使用]
    • FIN:接続を閉じる [四回のハンドシェイクの最初と第三回で使用され、同時にデータを送ることもできる、詳細は追加知識点を参照]
  • ウィンドウサイズ:相手にどれだけのデータを送れるかを知らせ、相手の送信速度を抑制するために使用
  • チェックサム:データが正しいか確認する。問題があれば、直接破棄し、再送を要求する
  • [PS]
    • これだけの設計は主に信頼性のため
    • 現実の配達会社は信頼性を達成できない、なぜなら運ぶ物品は唯一無二だから

UDP#

ユーザーデータグラムプロトコル;接続なし、信頼性のないデータ伝送プロトコル

  • 接続なし:ハンドシェイクは不要
  • 信頼性なし:相手が受け取ったかどうかは関係ない
  • 利点:柔軟性があり、コストが低い
  • ヘッダ形式
  • 画像
  • TCP に比べて、はるかにシンプル

—— ソケット ——#

配達員の類比だが、一つのタスクのためだけにサービスを提供
プロセスとトランスポート層の間のインターフェース、プロセスがネットワークデータを送信するには、必ずこれを通じてトランスポート層に渡す必要がある

【生と死】#

ソケット:ソケットを作成する#

  • 画像
  • domain:ドメイン名タイプ
    • AF_INET、ipv4 に対応 [一般的]
    • AF_INET6、ipv6 に対応
  • type:タイプ
    • SOCK_STREAM、バイトストリームに対応 [TCP]
    • SOCK_DGRAM、データグラムに対応 [UDP]
  • protocol:プロトコル
    • domain と type は protocol を一意に決定する場合がある、例えば AF_INET と SOCK_STREAM は IPPROTO_TCP を決定する
    • [PS] 一つだけ選ぶ場合は、0を代用できる
  • 戻り値:ファイルディスクリプタ
    • エラーの場合は - 1 を返す
    • ソケットもファイルであり、すべてはファイルである

close:接続を閉じる#

  • int close(int fd);
  • 四回のハンドシェイク [詳細は追加知識点を参照]
  • 両端で close を呼び出す必要があり、呼び出し側が FIN を送信し、受信側の recv の戻り値は 0

【服】#

bind:IP とポートをバインドする#

受信側にのみ適用

  • 画像
  • sockfd:ファイルディスクリプタ
  • addr:IP アドレスとポート
    • IP をバインド:その IP アドレスからのデータを受信できる [ローカル]
      • 空であれば、任意の IP アドレスからのデータを受信できる
      • 内網と外網の接続点で使用でき、ファイアウォールとして機能する
    • ポートをバインド:どのポートにサービスを提供するか [共 $2^{16}=65536$ 個のポート]
  • addrlen:アドレスの長さ
  • 戻り値:成功の場合は 0;それ以外は - 1

+ 関連構造体:sockaddr、sockaddr_in#

sockaddr

  • 画像
  • sin_family:アドレスプロトコルファミリー、一般的に AF_INET を使用し、ipv4 に対応
  • sa_data:IP アドレスとポートを同時に含む
  • ❗ 使用は不便であり、以下のよりフレンドリーな方法に切り替え、再度 (struct sockaddr*) でキャストすればよい

sockaddr_in

  • 画像
  • sin_port:ポート番号 [ネットワークバイトオーダーが必要、下記参照]
  • sin_addr:IPアドレス
  • その中で、sin_addr は新しい構造体in_addrに対応
    • 画像
    • 32 ビットの符号なし整数を格納し、一般的にinet_addr関数を使用してドット十進法を in_addr 構造体に変換する:
      • 画像
      • ドット十進法表記 [文字列形式] の方が便利
      • inet_ntoa はその逆

+ ホストバイトオーダー & ネットワークバイトオーダー#

  • ホストバイトオーダー:ビッグエンディアン、小エンディアン
    • 一般的には小エンディアンマシンで、位バイトがメモリのアドレス端に配置される
  • ネットワークバイトオーダー:4 バイトの 32 ビット値について、まず 0〜7 ビットを送信し、...、最後に 24〜31 ビットを送信する
  • 整数バイトオーダーの変換関数
    • 画像
    • htonl:32 ビットホストバイトオーダーからネットワークバイトオーダーへの変換
    • htons:16 ビットホストバイトオーダーからネットワークバイトオーダーへの変換
    • ntohl、ntohs はその逆

listen:リスニング状態に設定#

ソケットをアクティブ(デフォルト)からパッシブに切り替える [まず bind でポートをバインドする必要がある]

  • 画像
  • 注意:第二引数の実際の意味は完了キューの長さである
    • ① TCP 接続プロセスには二つのキューが存在する
      • 未完了キュー:クライアントが SYN を送信し、サーバーが SYN+ACK を応答した後、サーバーは現在 SYN_RECV 状態にあり、この時の接続は未完了キューにある
      • 完了キュー:クライアントが ACK を応答した後、両方が ESTABLISHED 状態にあり、この時接続は未完了キューから移動して完了キューに入る
      • 👉 サーバーが accept を呼び出すと、接続は完了キューから削除される
    • ② 注意事項:適切な backlog を設定する;サーバーは新しい接続をできるだけ早く accept する必要がある

accept:接続を受け入れる#

新しい配達員を生成する [さらに複数の接続を確立できる]

  • 画像
  • ① 渡された sockfd は socket ()、bind ()、listen () 処理を経る必要がある
  • ② addr は出力パラメータで、クライアントのアドレスを格納するために使用
  • 戻り値
    • 成功すれば新しい sockfdを返し、元の sockfd は依然として accept に使用できる
    • 失敗すれば - 1 を返す
  • [PS] 一般的に新しい sockfd を使用し終えたら閉じる;listen 状態のソケットは閉じない

【客】#

connect:接続を確立する#

アクティブソケット、最大で一つしか接続できない

  • 画像
  • accept とは異なり:
    • sockfd は bind ()、listen () 処理を経る必要がない
    • 新しいソケットは返さない

⭐ connect と accept は一対で、クライアントとサーバーで実行され、この間に三回のハンドシェイクが完了する

【伝送】#

send:データを送信する#

本質的にはwriteと同じ

  • 画像
  • ❗ sendto は dest_addr と addrlen を多く受け取るが、これは UDP 用である
    • 接続を確立していないため、目的の IP とポートを指定する必要がある
  • flag は一般的に 0 に設定される

recv:データを受信する#

本質的にはreadと同じ

  • 画像
  • 相手が切断した場合、戻り値は 0
  • ❗ recvfrom は src_addr と addrlen を多く受け取るが、これは UDP 用である
    • src_addr はデータ送信元のアドレス情報を格納する
  • デフォルトはブロッキングである

—— 追加 ——#

kill#

プロセスに信号を送信する

  • man 2 kill
  • プロトタイプ
    • 画像
    • プロセス ID と信号ビットマスクに基づく
  • 説明
    • 画像
    • pid の設定にはさまざまな形式がある
    • すべて存在と権限チェックが必要
  • 戻り値
    • 画像
    • 0、成功;-1、エラー
  • kill -l で信号リストを表示
    • 画像
    • 64 種類の信号

signal#

信号の処理方法

  • man signal
  • プロトタイプ
    • 画像
    • sighandler_t 型の関数を定義する必要がある
  • 説明
    • 画像
    • その動作は UNIX のバージョンによって変わる
    • handler には三種類のタイプがある:無視、デフォルト、カスタム
    • カスタムタイプは捕鼠器の原理に関わる:一匹のネズミを捕まえると、後ろのネズミが失われる可能性がある
      • 再設定が必要 [システム操作による]
  • 戻り値
    • 画像
    • handler による

コードデモ#

サーバー#

tcp_server.h

  • 画像
  • 指定されたポートでリスニング状態の配達員を作成する

tcp_server.c

  • 画像
  • 番号に従って読む
  • 注意:head.h にソケット関連のヘッダファイルを追加すること、man マニュアルで探すことができる、ここでは詳述しない

1.server.c

  • 画像
  • 画像
  • accept はクライアントのアドレス情報を取得できる
  • データ伝送専用の子プロセスを作成する
  • 各ステップでエラーチェックに注意する
    • + 接続切断の状況(FIN、recv の戻り値が 0)の処理
  • 送受信戦略が異なる
    • 送信できるだけ送信し、受信できるだけ受信する
    • send は strlen を使用し、recv は sizeof を使用する

クライアント#

tcp_client.h

  • 画像
  • 指定された IP [ドット十進法の ipv4 文字列] とポートに接続する

tcp_client.c

  • 画像
  • 入力に基づいてフォームを記入する

1.client.c

  • 画像
  • 画像
  • 信号の捕捉を追加
  • bzero の使用、buff 変数の初期化

効果の展示#

  • 画像
  • 左:サーバー、右:クライアント [複数ユーザー可能]
  • 接続の確立、アドレスの捕獲、データの伝送、接続の切断
  • netstat を使用してポートのリスニング状態を確認できる
    • 画像
    • -alnt オプションを追加
  • [PS] クラウドホストのコンソール —— セキュリティグループでポート 8888 を開放する必要がある

追加知識点#

  • IP:公共のアドレスサービス、できる限りサービスを提供する。別の意味では、信頼性がない [事故が起こる可能性がある]

三回のハンドシェイク、四回のハンドシェイク#

  • 画像
  • 三回のハンドシェイク [SYN、ACK]
    • 画像
    • 第一次ハンドシェイク:クライアントがサーバーに SYN パケットを送信する [クライアントは SYN_SEND 状態に入り、サーバーの確認を待つ]
    • 第二次ハンドシェイク:サーバーが受信し、クライアントを確認し、ACK を設定し、自身も SYN を設定する、つまり SYN+ACK パケット [サーバーは LISTEN から SYN_RECV 状態に入る]
    • 第三次ハンドシェイク:クライアントがサーバーの SYN+ACK パケットを受信し、サーバーに ACK 確認パケットを送信し、送信が完了した後、クライアントは ESTABLISHED 状態に入り、サーバーが ACK を受信した後も ESTABLISHED 状態に入る
    • 注意:毎回の ACK シーケンス番号は、確認が必要なパケットのシーケンス番号に 1 を加え、確認を示す
  • 四回のハンドシェイク [FIN、ACK]
    • 画像
    • 第一次ハンドシェイク:クライアントが接続を閉じたい場合、クライアントは FIN パケットを送信し、自分にはもう送信するデータがないことを示す [この時まだデータを受信できる][クライアントは FIN_WAIT_1 状態に入る]
    • 第二次ハンドシェイク:サーバーは ACK パケットを返信し、クライアントの接続終了リクエストを受信したことを示すが、自身は接続を閉じるための準備をする必要がある [サーバーは CLOSE_WAIT 状態に入る]
      • クライアントはこの ACK を受信した後、FIN_WAIT_2 状態に入り、サーバーが接続を閉じるのを待つ
    • 第三次ハンドシェイク:サーバーが接続を閉じる準備ができた時、クライアントに FIN を再送信する [サーバーは LAST_ACK 状態に入り、クライアントの確認を待つ]
    • 第四次ハンドシェイク:クライアントがサーバーからの閉鎖リクエストを受信し、ACK パケットを送信する [クライアントは TIME_WAIT 状態に入り、超時再送の FIN パケットが発生する可能性があるため、2 つの MSL時間を待つ]
      • サーバーがこの ACK を受信した後、接続を閉じ、CLOSED 状態に入る
      • クライアントは2 つの MSL後、サーバーの FIN を受信しなければ、サーバーが正常に接続を閉じたと見なし、自分も接続を閉じ、CLOSED 状態に入る;そうでなければ、再度 ACK を送信する
  • 参考三回のハンドシェイクと四回のハンドシェイク—— ブログ [注:第四回のハンドシェイクでクライアントが待っているのは超時再送の FIN であり ACK ではない]

追加:2 つの MSL の意味#

TIME_WAIT はどのように引き起こされ、どのような役割を果たし、プログラミング時にどのような欠点があり、どのように解決するか?

  • 引き起こされる理由:TCP の四回のハンドシェイクでは、最初の三回のハンドシェイクが完了した後、第四回のハンドシェイクでクライアントがサーバーからの FIN を受信し、ACK を送信した後、TIME_WAIT 状態に入る
    • この時、クライアントは最大データセグメントのライフサイクル(Maximum segment lifetime、MSL)の時間を待つ必要があり、その後 CLOSED 状態に入る
  • 存在する理由
    • 遅延データセグメントを防ぐ
      • 各 TCP データセグメントにはユニークなシーケンス番号が含まれており、このシーケンス番号は TCP プロトコルの信頼性を保証する
      • 新しい TCP 接続のデータセグメントが、まだネットワーク内で伝送中の過去の接続のデータセグメントと重複しないようにするため、TCP 接続は新しいシーケンス番号を割り当てる前に、少なくとも静寂データセグメントがネットワーク内で生存できる最長時間、つまり MSL を待つ必要がある
      • これにより、遅延データセグメントが同じソースアドレス、ソースポート、宛先アドレス、宛先ポートを使用する他の TCP 接続に受信されるのを防ぐ
    • 接続の閉鎖を保証する
      • クライアントが待機する時間が十分でない場合、サーバーが ACK メッセージを受信していない間に、クライアントが再度サーバーと TCP 接続を確立しようとすると、次のようなことが発生する:
        • サーバーは ACK メッセージを受信していないため、現在の接続が合法であると考え続ける
        • クライアントが再度 SYN メッセージを送信してハンドシェイクを要求すると、サーバーから RST メッセージが返され、接続確立プロセスが中断される
      • したがって、TCP 接続のリモートが正しく閉じられることを保証するために、受動的に接続を閉じる側が FIN に対応する ACK メッセージを受信するのを待つ必要がある
  • プログラミングへの影響
    • 高並行性のシナリオでは、過剰な TIME_WAIT が発生しやすい
    • MSL の期間は一般的に 60 秒であり、これは受け入れがたい。TCP 接続が数秒間通信するだけであっても、TIME_WAIT は 2 分待つ必要がある
  • 解決方法
    • タイムスタンプ変数に基づき、送信データパケット、最近の受信データパケットの時間を記録する
    • その後、二つのパラメータと組み合わせる
      • reuse:接続を閉じる側が再度相手に接続を開始する際、TIME_WAIT 状態の接続を再利用することを許可する
      • recycle:カーネルは TIME_WAIT 状態の接続を迅速に回収し、RTO 時間 [データパケットの再送超時] を待つだけで済む
  • 参考

C 言語でのソケットプログラミング#

  • サーバー:socket、sockaddr [_in]、bind、listen;accept、send/recv;close
  • クライアント:socket、sockaddr [_in]、connect;send/recv;close
  • 画像
  • TCP に基づくストリームソケット、UDP に基づくデータグラムソケット
    • UDP サーバーも IP とポートを bind する必要があるが、listen は不要で、sendto、recvfrom を使用して情報を送受信する
  • sockaddr [_in]:ソケット情報を保存する構造体、[_in] を使用して情報を記入し、sockaddr に変換する
  • サーバーは二つのソケットを必要とし、一つはリスニング用、もう一つはクライアントの connect から送信されたソケットを受信するためのものである

kaikeba.com を入力し、Enter を押す#

->TCP 接続を確立し、ローカルで最初の request パケットを送信 -> 最初の request パケットを受信するまでに何が起こったか?

  • [マクロレベル] DNS👉TCP 接続 [アプリケーション層、トランスポート層、ネットワーク層、データリンク層]👉サーバーがリクエストを処理👉応答結果を返す
  • DNS
    • ローカル hosts、ローカル DNS リゾルバキャッシュ
    • ローカル DNS
    • 繰り返し / 再帰:ルート DNS サーバー、トップレベル DNS、権威 DNS
    • ドメイン名に対応する IP を見つけるまで
  • TCP 接続
    • アプリケーション層:HTTP リクエストを送信 —— リクエストメソッド、URL、HTTP バージョン
    • トランスポート層:サーバーと三回のハンドシェイクを行う
    • ネットワーク層:ARP プロトコルで IP に対応する MAC アドレスを照会し、同じローカルネットワーク内であれば、MAC アドレスに基づいて直接リクエストを送信;そうでなければ、ルーティングテーブルを使用して次のジャンプアドレスを探し、対応する MAC アドレスにアクセスする
    • データリンク層:イーサネットプロトコル
    • ブロードキャスト:ローカルネットワーク内のすべてのマシンにリクエストを送信し、MAC アドレスを比較する
  • Web サーバー
    • ユーザーリクエストを解析し、どのリソースファイルを調度し、データベース情報を呼び出し、ブラウザクライアントに返す
  • 応答結果を返す
    • 一般的に HTTP ステータスコードがあり、例えば 200、301、404 など、このステータスコードを通じてサーバー側の処理が正常かどうかを知ることができ、具体的なエラーを理解することができる
  • ⭐ 推奨動画:TCP-IP Explained (2000)——Youtube
    • [主に IP 層から展開]
    • 対象:TCP パケット、ICMP Ping パケット、UDP パケット、デッド Ping、ルーター、ルーター交換機...
    • 大まかな流れ
      • ローカル:パケットをカプセル化、ローカル伝送、ローカルルーター選択、スイッチ選択、プロキシチェック、ファイアウォールチェック、ローカル伝送、ルーター選択
      • ——> ネットワーク伝送 ——>
      • 応答端:ファイアウォールチェック [ポートを監視]、プロキシリクエストパケットをチェック、リクエスト端に応じた情報を返す、上記のローカルプロセスと同様 [パケットをカプセル化、...、ルーター選択]

ポート再利用に関する#

一つのポートは異なるサービスに同時にバインドできるか?

  • 可能である。データを受信する際、五つのタプル {トランスポートプロトコル、ソース IP、ソースポート、宛先 IP、宛先ポート} に基づいてデータ属性を判断する
  • 例えば:
    • TCP と UDP トランスポートプロトコルを使用して同じポートをリスニングし、データを受信する際に互いに影響を及ぼさず、衝突しない
    • 同様に、accept は新しいソケットを生成するが、同じポートを使用する
      • 異なるソケットが生成され、これらのソケットに含まれる宛先 IP とポートは変わらず、変わるのはソース IP とポートだけである [ポート再利用]
  • [PS] TCP タイプのソケットは TCP タイプのデータのみを送信する

親子プロセスのソケット関係#

親プロセスからクローンされた子プロセス内のソケットと親プロセスのソケットの関係

  • 同じものであり、同じファイルに対応する
  • データが到着すると、どちらのプロセスが先にデータを受信するかによって、そのデータを持つことになる、もう一方のプロセスは引き続き待機する
  • したがって、一般的に子プロセスが不要なリソースは継承しない方が良い、例えば:close を使用して親プロセスから継承されたソケットを子プロセス内で直接閉じることができる

ヒント#

  • システム / ネットワークプログラミングでは、すべての可能性のあるエラーを考慮する必要がある
  • 信号の知識拡張:自分の sleep 関数を実装する
  • コンパイル時には、すべての関連ソースファイル [*.c] を考慮することを忘れないでください

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。