コース内容#
ソケットとは何か?ネットワークプログラミングは何をするのか?
- 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$ 個のポート]
- IP をバインド:その IP アドレスからのデータを受信できる [ローカル]
- 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関数を使用してドット区切り 10 進数を in_addr 構造体に変換する:
- ドット区切り 10 進数表現 [文字列形式] の方が便利
- inet_ntoa はその逆
+ ホストバイトオーダー & ネットワークバイトオーダー#
- ホストバイトオーダー:ビッグエンディアン、小エンディアン
- 一般的には小エンディアンの機械で、低位バイトがメモリの低アドレスに配置される
- ネットワークバイトオーダー:4 バイトの 32bit 値について、最初に 0〜7 ビットを送信し、...、最後に 24〜31 ビットを送信する
- 整数のバイトオーダー変換関数
- htonl:32 ビットのホストバイトオーダーからネットワークバイトオーダーへの変換
- htons:16 ビットのホストバイトオーダーからネットワークバイトオーダーへの変換
- ntohl、ntohs はその逆
listen:リスニング状態に設定#
ソケットをアクティブ(デフォルト)からパッシブに切り替える [まず bind でポートをバインドする必要がある]
- 注意:第二引数の実際の意味は完了キューの長さ
- ① TCP 接続プロセスには二つのキューが存在
- 未完了キュー:クライアントが SYN を送信し、サーバーが SYN+ACK で応答した後、サーバーは現在 SYN_RECV 状態にあり、この時の接続は未完了キューにある
- 完了キュー:クライアントが ACK で応答した後、双方が ESTABLISHED 状態にあり、この時接続は未完了キューから移動して完了キューに入る
- 👉 サーバーが accept を呼び出すと、接続は完了キューから削除される
- ② 注意事項:適切な backlog を設定する;サーバーは新しい接続をできるだけ早く accept する必要がある
- ① TCP 接続プロセスには二つのキューが存在
accept:接続を受け入れる#
新しい宅配便の配達員を生成する [さらに複数の接続を確立できる]
- ① 渡された sockfd は socket ()、bind ()、listen () 処理を経ている必要がある
- ② addr は出力パラメータで、クライアントのアドレスを格納するために使用
- 戻り値
- 成功した場合、新しいsockfdを返し、元の sockfd は依然として accept に使用できる
- 失敗した場合、-1 を返す
- [PS] 一般的に新しい scokfd は使用後に閉じる;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 [ドット区切り 10 進数の 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 メッセージを受信するのを待つ必要がある
- クライアントが待機する時間が十分でない場合、サーバーが ACK メッセージを受信していない間に、クライアントが再度サーバーと TCP 接続を確立しようとすると、次のようなことが発生する:
- ①遅延データセグメントを防ぐ
- プログラミングへの影響
- 高い同時接続のシナリオでは、過剰な 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] を考慮することを忘れないでください