1. ネットワーク内のプロセス間で通信する方法
プロセス通信の概念は、もともとスタンドアロン システムから生まれました。各プロセスは独自のアドレス範囲内で実行されるため、相互に通信する 2 つのプロセスが相互に干渉せず、協調して動作することを保証するために、オペレーティング システムは、
BSD などのプロセス通信に対応する機能を提供します。 : パイプ (pipe)、名前付きパイプ (named Pipe) ソフト割り込み信号 (シグナル)
UNIX システム V には、メッセージ (メッセージ)、共有記憶領域 (共有メモリ)、セマフォ (セマフォ) などがあります。
それらはすべてのみですネイティブプロセス間の通信に使用されます。インターネット プロセス通信は、異なるホスト プロセス間の相互通信の問題を解決することを目的としています (同じマシン上のプロセス通信は特殊なケースとみなすことができます)。そのためには、まずネットワーク間のプロセス識別の問題を解決する必要があります。同じホスト上では、異なるプロセスはプロセス ID によって一意に識別できます。しかし、ネットワーク環境では、各ホストが独自に割り当てたプロセス番号ではプロセスを一意に識別できません。たとえば、ホスト A はあるプロセスにプロセス番号 5 を割り当てますが、プロセス番号 5 はホスト B にも存在する可能性があります。したがって、「プロセス番号 5」という文は意味がありません。次に、オペレーティング システムは多くのネットワーク プロトコルをサポートしており、プロトコルが異なれば動作方法も異なり、アドレス形式も異なります。したがって、ネットワーク間のプロセス通信では、複数のプロトコルを識別するという問題も解決する必要があります。
実際、TCP/IP プロトコル スイートは、この問題の解決に役立ちました。ネットワーク層の「IP アドレス」はネットワーク内のホストを一意に識別でき、トランスポート層の「プロトコル + ポート」は一意に識別できます。ホスト内のアプリケーション (プロセス)。このように、トリプレット (IP アドレス、プロトコル、ポート) を使用してネットワーク プロセスを識別でき、ネットワーク内のプロセス通信ではこのマークを使用して他のプロセスと対話できます。
TCP/IP プロトコルを使用するアプリケーションは通常、アプリケーション プログラミング インターフェイス、つまり UNIX BSD のソケットと UNIX System V (すでに廃止されました) の TLI を使用して、ネットワーク プロセス間の通信を実現します。今のところ、ほとんどすべてのアプリケーションがソケットを使用しており、ネットワーク上のプロセス通信が普及しているのはこのためです。
2. TCP/IP と UDP とは
TCP/IP (Transmission Control Protocol/Internet Protocol) は、伝送制御プロトコル/インターネット プロトコルのセットです。ワイド エリア ネットワーク (WAN) 用に設計されています。
TCP/IP プロトコルは OS に存在し、TCP/IP をサポートするシステムコール (Socket、Connect、Send、Recv など) が OS を通じて提供されます。
UDP(User Data Protocol(ユーザーデータグラムプロトコル))は、TCPに相当するプロトコルです。これは、TCP/IP プロトコル スイートのメンバーです。図に示すように:
TCP/IP プロトコル スイートにはトランスポート層、ネットワーク層、リンク層が含まれており、ソケットの位置は図に示すとおりです。ソケットは、中間のソフトウェア抽象化層です。アプリケーション層と TCP/IP プロトコル スイート間の通信。
3. ソケットとは何ですか? 1. ソケットは Unix に由来しており、Unix/Linux の基本理念の 1 つは「すべてがファイルであり、開くことができる」というものです。 「オープン –> 読み取りおよび書き込み書き込み/読み取り –> クローズ」モードで動作します。ソケットはこのモードの実装であり、ソケット関数の一部はそのファイルに対する操作 (読み取り/書き込み IO、オープン、クローズ) です。率直に言うと、ソケットはアプリケーション層および TCP/IP プロトコル ファミリです。通信のための中間ソフトウェア抽象化層。インターフェイスのセットです。設計モードでは、Socket は実際にはファサード モードであり、複雑な TCP/IP プロトコル ファミリを Socket インターフェイスの背後に隠し、ユーザーにとっては一連の単純なインターフェイスだけで、指定されたプロトコルを満たすように Socket がデータを編成できます。
注: 実際、ソケットにはレイヤーの概念がありません。これは、プログラミングを容易にするファサード設計パターンの単なる適用です。ソフトウェア抽象化レイヤーです。ネットワーク プログラミングでは、多くのソケットを使用します。
2. ソケット記述子
私たちがよく知っている 3 つのハンドルは、実際には 0、1、2 です。0 は標準入力、1 は標準出力です。 2は標準エラー出力です。 0、1、2 は整数で表され、対応する FILE * 構造は stdin、stdout、stderr で表されます
ソケット API は元々 UNIX オペレーティング システムの一部として開発されたため、ソケット API はシステムの他の I/O デバイスが統合されているのと同じです。特に、アプリケーションがインターネット通信用のソケットを作成すると、オペレーティング システムはソケットを識別するための記述子として小さな整数を返します。次に、アプリケーションは記述子をパラメーターとして渡し、関数を呼び出して何らかの操作 (ネットワーク経由でのデータ送信や受信データの受信など) を完了します。
多くのオペレーティング システムでは、ソケット記述子とその他の I/O 記述子が統合されているため、アプリケーションはファイルに対してソケット I/O または I/O 読み取り/書き込み操作を実行できます。
アプリケーションがソケットを作成したい場合、オペレーティング システムは記述子として小さな整数を返し、アプリケーションはこの記述子を使用してソケットを参照します。I/O リクエストを必要とするアプリケーションは、オペレーティング システムにソケットを開くように要求します。書類。オペレーティング システムは、アプリケーションがファイルにアクセスするためのファイル記述子を作成します。アプリケーションの観点から見ると、ファイル記述子は、アプリケーションがファイルの読み取りと書き込みに使用できる整数です。以下の図は、オペレーティング システムが内部データ構造を指すポインターの配列としてファイル記述子を実装する方法を示しています。
プログラムシステムごとに個別のテーブルがあります。正確に言うと、システムは実行中のプロセスごとに個別のファイル記述子テーブルを維持します。プロセスがファイルを開くと、システムはファイルの内部データ構造へのポインタをファイル記述子テーブルに書き込み、テーブルのインデックス値を呼び出し元に返します。アプリケーションはこの記述子を記憶し、将来ファイルを操作するときにそれを使用するだけで済みます。オペレーティング システムは、この記述子をインデックスとして使用してプロセス記述子テーブルにアクセスし、ポインタを使用してファイルに関するすべての情報を保持するデータ構造を見つけます。
ソケットのシステム データ構造:
1) ソケット API には、ソケットの作成に使用される関数ソケットがあります。ソケット設計の一般的な考え方は、ソケットは非常に一般的なため、1 つのシステム コールで任意のソケットを作成できるというものです。ソケットが作成されたら、アプリケーションは他の関数を呼び出して特定の詳細を指定する必要があります。たとえば、socket を呼び出すと、新しい記述子エントリが作成されます。
2) ソケットの内部データ構造には多くのフィールドが含まれていますが、システムがソケットを作成した後、フィールドのほとんどは埋められません。アプリケーションがソケットを作成した後、ソケットを使用するには、他のプロシージャを呼び出してこれらのフィールドに値を設定する必要があります。
3. ファイル記述子とファイル ポインターの違い:
ファイル記述子: Linux システムでファイルを開くと、小さな正の整数であるファイル記述子が取得されます。各プロセスは、ファイル記述子テーブルを PCB (プロセス制御ブロック) に保存します。ファイル記述子は、このテーブルのインデックスであり、開いているファイルへのポインタを持ちます。
ファイルポインタ: ファイルポインタは、C言語におけるI/Oのハンドルとして使用されます。ファイルポインタは、プロセスのユーザー領域にある FILE 構造と呼ばれるデータ構造を指します。 FILE 構造にはバッファとファイル記述子が含まれます。ファイル記述子はファイル記述子テーブルへのインデックスであるため、ある意味、ファイル ポインタはハンドルのハンドルです (Windows システムでは、ファイル記述子はファイル ハンドルと呼ばれます)。
4. 基本的な SOCKET インターフェース機能
人生において、A が B に電話したい、A が番号をダイヤル、B が着信音を聞いて電話を取る、そして A と B が接続を確立します。そうすればBは話せるようになります。通話が終了したら電話を切って会話を終了します。 この通話では、これがどのように機能するかを「オープン-書き込み/読み取り-クローズ」モードという簡単な方法で説明しました。
サーバーは最初にソケットを初期化し、次にポートにバインドし、ポートをリッスンし、accept を呼び出してブロックし、クライアントが接続するのを待ちます。このとき、クライアントがSocketを初期化してからサーバーに接続すると、接続に成功するとクライアントとサーバー間の接続が確立されます。クライアントがデータ要求を送信し、サーバーが要求を受信して処理し、次に応答データをクライアントに送信し、クライアントがデータを読み取り、最後に接続を閉じて対話が終了します。
これらのインターフェースの実装はカーネルによって完了します。実装方法の詳細については、Linux カーネル
4.1、socket() function
intソケット(int protofamily, int type, int protocol);//return sockfd
sockfd を参照してください。記述子。
ソケット関数は通常のファイルを開く操作に相当します。通常のファイルオープン操作はファイル記述子を返し、socket() はソケットを一意に識別するソケット記述子 (ソケット記述子) を作成するために使用されます。このソケット記述子はファイル記述子と同じであり、後続の操作でいくつかの読み取りおよび書き込み操作を実行するためのパラメーターとして使用されます。
異なるパラメータ値を fopen に渡して異なるファイルを開くことができるのと同じように。ソケットを作成するときに、異なるパラメーターを指定して異なるソケット記述子を作成することもできます。 ソケット関数の 3 つのパラメーターは次のとおりです:
protofamily: つまり、プロトコル ファミリー (ファミリー) とも呼ばれるプロトコル ドメイン。一般的に使用されるプロトコル ファミリには、AF_INET (IPV4)、AF_INET6 (IPV6)、AF_LOCAL (または AF_UNIX、Unix ドメイン ソケット)、AF_ROUTE などが含まれます。プロトコル ファミリによってソケットのアドレス タイプが決定され、対応するアドレスが通信で使用される必要があります。たとえば、AF_INET は ipv4 アドレス (32 ビット) とポート番号 (16 ビット) の組み合わせを使用するかどうかを決定し、AF_UNIX は決定します。絶対パスをアドレスとして使用します。
type: ソケットのタイプを指定します。一般的に使用されるソケット タイプには、SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET などが含まれます (ソケットのタイプは何ですか?)。
プロトコル: その名前のとおり、指定されたプロトコルを意味します。一般的に使用されるプロトコルには、IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC などがあり、それぞれ TCP 送信プロトコル、UDP 送信プロトコル、STCP 送信プロトコル、TIPC 送信プロトコルに対応します (このプロトコルについては別途説明します)。
注: 上記のタイプとプロトコルは自由に組み合わせることができません。たとえば、SOCK_STREAM と IPPROTO_UDP を組み合わせることはできません。プロトコルが 0 の場合、タイプ type に対応するデフォルトのプロトコルが自動的に選択されます。
socket を呼び出してソケットを作成すると、返されたソケット記述子はプロトコル ファミリ (アドレス ファミリ、AF_XXX) 空間に存在しますが、特定のアドレスを持ちません。アドレスを割り当てたい場合は、bind() 関数を呼び出す必要があります。そうしないと、connect() または listen() を呼び出すときにシステムが自動的にポートをランダムに割り当てます。
4.2. binding() 関数
上で述べたように、bind() 関数はアドレス ファミリ内の特定のアドレスをソケットに割り当てます。たとえば、AF_INET および AF_INET6 に対応して、ipv4 または ipv6 アドレスとポート番号の組み合わせがソケットに割り当てられます。
int binding(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
関数の 3 つのパラメータは次のとおりです:
sockfd: ソケット記述子。socket() 関数を通じて作成され、一意に識別されます。 .ソケット。 bind() 関数は、名前をこの記述子にバインドします。
addr: const struct sockaddr * sockfd にバインドされるプロトコル アドレスを指すポインター。このアドレス構造は、アドレスがソケットを作成するときのアドレス プロトコル ファミリによって異なります。たとえば、ipv4 は次のようになります。ネットワークバイトオーダー */
struct in_addr sin_addr; /* インターネットアドレス */
/* 対応する ipv6 は次のとおりです。
struct sockaddr_in6 { /* AF_INET6 */
in_port_t sin6_port; /* ポート番号 */
uint32_t sin6_flowinfo; /* IPv6 フロー情報 */
struct in6_addr sin6_addr; /* IPv6 アドレス */
uint32_t sin6_scope_id; /* スコープ ID (2.4 の新機能) */
};
unsigned char s6_addr[16]; /* IPv6 アドレス */
};
Unix ドメインは :
に対応します。
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
addrlen: アドレスの長さに対応します。
通常、サーバーはサービスの提供を開始するときに既知のアドレス (IP アドレス + ポート番号など) にバインドされ、顧客はそれを介してサーバーに接続できます。クライアントはそれを指定する必要はありません。システムは、1 つのポート番号と独自の IP アドレスの組み合わせを自動的に割り当てます。このため、サーバーは通常、リッスンする前にbind()を呼び出しますが、クライアントはそれを呼び出さず、代わりにconnect()のときにシステムがランダムにbind()を生成します。
ネットワークのバイトオーダーとホストのバイトオーダー
ホストのバイトオーダーは、通常ビッグエンディアンモードとリトルエンディアンモードと呼ばれるものです。異なるCPUには異なるバイトオーダータイプがあり、これらのバイトオーダーはメモリ内の整数を参照し、それらが保存される順序を指します。をホスト順序といいます。ビッグエンディアンとリトルエンディアンの標準的な定義は以下のように引用されています:
a) リトルエンディアンとは、下位バイトがメモリの下位アドレス端に配置され、上位バイトがメモリの下位アドレス端に配置されることを意味します。メモリの上位アドレスの端。
b)ビッグエンディアンとは、上位バイトがメモリの下位アドレス端に配置され、下位バイトがメモリの上位アドレス端に配置されることを意味します。
ネットワークバイトオーダー: 4バイトの32ビット値は次の順序で送信されます: 最初に0~7ビット、次に8~15ビット、次に16~23ビット、最後に24~31ビット。この転送順序はビッグエンディアンと呼ばれます。 TCP/IP ヘッダー内のすべての 2 進整数は、ネットワーク経由で送信されるときにこの順序である必要があるため、ネットワーク バイト オーダーとも呼ばれます。名前が示すように、バイト オーダーは 1 バイト型を超えるデータがメモリに格納される順序です。1 バイトのデータには順序の問題はありません。
つまり: アドレスをソケットにバインドするときは、まずホストのバイトオーダーをネットワークのバイトオーダーに変換し、ホストのバイトオーダーがネットワークのバイトオーダーと同様にビッグエンディアンを使用すると想定しないでください。この問題が原因で殺人事件も起きています!この問題は、同社のプロジェクト コードに多くの不可解な問題を引き起こしているため、ホストのバイト オーダーについては何も仮定せず、ソケットに割り当てる前に必ずネットワーク バイト オーダーに変換してください。
4.3、listen()、connect()関数
あなたがサーバーの場合、socket()、bind()を呼び出した後、クライアントがconnect()を呼び出すと、listen()が呼び出され、ソケットをリッスンします。今回は接続リクエストを発行し、サーバーはこのリクエストを受信します。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen 関数の最初のパラメーターはリッスンするソケット記述子であり、 2 番目のパラメータは、リッスンするソケット記述子です。各パラメータは、対応するソケットによってキューに入れることができる接続の最大数です。デフォルトでは、socket() 関数によって作成されたソケットはアクティブ型ですが、listen 関数はソケットをパッシブ型に変更し、クライアントの接続要求を待ちます。
connect 関数の最初のパラメータはクライアントのソケット記述子、2 番目のパラメータはサーバーのソケット アドレス、3 番目のパラメータはソケット アドレスの長さです。クライアントは、connect 関数を呼び出して TCP サーバーとの接続を確立します。
4.4、accept()関数
TCPサーバーはsocket()、bind()、listen()を順番に呼び出した後、指定されたソケットアドレスをリッスンします。 TCP クライアントは、socket() と connect() を順番に呼び出した後、TCP サーバーに接続要求を送信します。 TCP サーバーはこのリクエストをリッスンした後、accept() 関数を呼び出してリクエストを受信し、接続が確立されます。その後、通常のファイルの読み取りおよび書き込み I/O 操作と同様のネットワーク I/O 操作を開始できます。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //Return connection connect_fd
parameter sockfd
parameter sockfd は、上で説明した listen ソケットです。クライアントがサーバーに接続するときは、このポート番号が使用され、このポート番号はこのソケットに関連付けられます。もちろん、クライアントはソケットの詳細を知りません。知っているのはアドレスとポート番号だけです。
パラメータ addr
これは、戻り値を受け取るために使用される結果パラメータです。もちろん、この戻り値は、アドレス構造体を通じて記述される必要があります。これは構造です。顧客のアドレスに興味がない場合は、この値を NULL に設定できます。
パラメータ len
皆さんもお分かりかと思いますが、これは上記の addr 構造体のサイズを受け取るために使用されます。addr 構造体が占有するバイト数を指定します。同様に、NULL に設定することもできます。
accept が正常に返った場合、サーバーとクライアントは正しく接続を確立しています。この時点で、サーバーは accept によって返されたソケットを介してクライアントとの通信を完了します。
注:
Accept は、クライアント接続が確立されて戻るまで、デフォルトでプロセスをブロックします。これは、新しく利用可能なソケット (接続ソケット) を返します。
この時点で、2 種類のソケットを区別する必要があります。
リスニング ソケット: リスニング ソケットは、accept のパラメーター sockfd と同じであり、listen 関数を呼び出した後、サーバーがソケット() 関数の呼び出しを開始することによって生成されます。これは、リスニング ソケット記述子と呼ばれます。 (リスニングソケット Word)
ソケットの接続: ソケットは、アクティブに接続されているソケットからリスニングソケットに変換され、accept 関数は、すでに接続されているポイントツーポイント接続を表す、接続されたソケット記述子 (接続されたソケット) を返します。ネットワーク上に存在します。
サーバーは通常、リスニングソケット記述子のみを作成します。これはサーバーのライフサイクル中に常に存在します。カーネルは、サーバー プロセスによって受け入れられたクライアント接続ごとに接続されたソケット記述子を作成します。サーバーがクライアントへのサービスを完了すると、対応する接続されたソケット記述子が閉じられます。
当然の疑問は、「なぜ 2 種類のソケットがあるのですか?」ということです。理由は単純です。ディスクリプタを使用すると、機能が多すぎるため、その使用が非常に直感的ではなくなります。同時に、そのような新しいディスクリプタがカーネル内で生成されることになります。
ソケットsocketfd_newの接続は、クライアントとの通信に新しいポートを占有しません。リスニングソケットsocketfd
4.5、read()、write()およびその他の関数と同じポート番号を使用します
すべての準備が整いました。東風だけに借りがあるので、サーバーとクライアントは接続を確立しました。ネットワーク I/O は読み取りおよび書き込み操作のために呼び出すことができます。これは、ネットワーク内の異なるプロセス間の通信が実現されることを意味します。ネットワーク I/O 操作には次のグループがあります:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom( )/sendto()
私は、recvmsg()/sendmsg() 関数を使用することをお勧めします。これらの 2 つの関数は、実際、上記の他のすべての関数をこれら 2 つの関数に置き換えることができます。それらの宣言は次のとおりです:
#include
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
# include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void * buf , size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
s size_t recvfrom( int sockfd、void *buf , size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recv msg(int sockfd, struct msghdr *msg, int flags);
read 関数は、fd からコンテンツを読み取る役割を果たします。読み取りが成功すると、read は実際に読み取られたバイト数を返します。戻り値が 0 の場合は、ファイルの終わりが読み取られたことを意味します。 0 未満の場合は、エラーが発生したことを意味します。エラーが EINTR の場合は、読み取りが割り込みによって行われたことを意味します。ECONNREST の場合は、ネットワーク接続に問題があることを意味します。
write 関数は、buf の nbytes バイトの内容をファイル記述子 fd に書き込みます。成功すると、書き込まれたバイト数を返します。失敗すると、-1 が返され、errno 変数が設定されます。 ネットワーク プログラムでは、ソケット ファイル記述子に書き込むときに 2 つの可能性があります。 1) write の戻り値は 0 より大きく、データの一部またはすべてが書き込まれたことを示します。 2) 戻り値が 0 未満であり、エラーが発生しました。エラーの種類に応じて対処する必要があります。エラーが EINTR の場合は、書き込み中に割り込みエラーが発生したことを意味します。 EPIPE の場合は、ネットワーク接続に問題がある (相手が接続を閉じている) ことを意味します。
これらの I/O 関数のペアを 1 つずつ紹介することはしません。詳細についてはマニュアルを参照するか、次の例では Baidu または Google を使用します。
4.6. close() 関数
サーバーがクライアントとの接続を確立した後、読み取りおよび書き込み操作が完了した後、fclose を呼び出すなど、対応するソケット記述子を閉じる必要があります。ファイルを開く操作を行った後、開いているファイルを閉じます。
#include
int close(int fd);
close のデフォルトの動作は、ソケットをクローズ済みとしてマークし、すぐに呼び出しプロセスに戻ります。この記述子は呼び出しプロセスでは使用できなくなります。つまり、読み取りまたは書き込みの最初のパラメーターとして使用できなくなります。
注: クローズ操作では、対応するソケット記述子の参照カウントが -1 だけ減少します。参照カウントが 0 の場合にのみ、TCP クライアントはサーバーに終了要求を送信します。
5. ソケットでの TCP の確立 (スリーウェイ ハンドシェイク)
TCP プロトコルは、3 つのメッセージ セグメントを通じて接続の確立を完了します。ハンドシェイク) のプロセスを次の図に示します。
最初のハンドシェイク: 接続を確立するとき、クライアントは syn パケット (syn=j) をサーバーに送信し、SYN_SEND 状態に入り、サーバーによる確認を待ちます。
2 回目のハンドシェイク: サーバーは syn パケットを受信し、クライアントの SYN (ack=j+1) を確認する必要があります。同時に、SYN パケット (syn=k)、つまり SYN+ACK パケットも送信します。このとき、サーバーは SYN_RECV ステータスに入ります。
3 回目のハンドシェイク: クライアントはサーバーから SYN+ACK パケットを受信し、パケットが送信された後、確認パケット ACK (ack=k+1) をサーバーに送信します。クライアントとサーバーは ESTABLISHED 状態に入り、3 回の握手が完了します。
完全な 3 ウェイ ハンドシェイクは、要求、応答、再度確認というものです。
対応する関数インターフェース:
図からわかるように、クライアントが connect を呼び出すと、接続要求がトリガーされ、SYN J パケットがサーバーに送信されます。このとき、connect はブロッキング状態になります。 ; サーバーは接続を監視します。つまり、SYN J パケットを受信し、accept 関数を呼び出して、SYN K、ACK J+1 をクライアントに送信します。クライアントはサーバーの SYN K、ACK J+1 を受信し、この時点で connect が戻り、SYN K を確認します。サーバーが ACK K+1 を受信すると、accept が戻ります。この時点で、3 ウェイ ハンドシェイクが完了し、接続が確立されます。設立。
ネットワーク パケット キャプチャを通じて特定のプロセスを表示できます:
たとえば、サーバーはポート 9502 を開きます。 tcpdump を使用してパケットをキャプチャします:
tcpdump -iany tcp port 9502
次に、telnet 127.0.0.1 9502 を使用して接続を開きます。:
telnet 127.0.0.1 9502
14:12 :45.104687 IP localhost.39870 > localhost.9502: フラグ [S]、seq 2927179378、win 32792、オプション [mss 16396、sackOK、TS val 255474104 ecr 0、nop、wscale 3]、長さ 0 (1)
14: 12: 45.104701 IP localhost.9502 > localhost.39870: フラグ [S.]、seq 1721825043、ack 2927179379、win 32768、オプション [mss 16396、sackOK、TS val 255474104 ecr 255474104、nop 、wscale 3]、長さ 0 ( 2)
14:12:45.104711 IP localhost.39870 > localhost.9502: フラグ [.]、ack 1、win 4099、オプション [nop,nop,TS val 255474104 ecr 255474104]、長さ 0 (3)
14: 13:01.415407 IP localhost.39870 > localhost.9502: フラグ [P.]、シーケンス 1:8、ack 1、win 4099、オプション [nop,nop,TS val 255478182 ecr 255474104]、長さ 7
14: 13: 01.415432 IP localhost.9502 > localhost.39870: フラグ [.]、ack 8、win 4096、オプション [nop,nop,TS val 255478182 ecr 255478182]、長さ 0
14:13:01.415747 IP localhost.9502 > ; localhost .39870: フラグ [P.]、シーケンス 1:19、ack 8、win 4096、オプション [nop,nop,TS val 255478182 ecr 255478182]、長さ 18
14:13:01.415757 IP localhost.39870 > .9502 : Flags [.]、ACK 19、Win 4097、オプション [NOP、NOP、TS Val 255478182 ECR 255478182]、長さ 0
114: 12: 45.104687 時間 (正確から繊細まで)
ローカルホスト39870 > localhost.9502 は通信の流れを示し、39870 はクライアント、9502 はサーバーです
[S] はこれが SYN リクエストであることを意味します
[S.] はこれが SYN+ACK 確認パッケージであることを意味します:
[.] は、これが ACT 確認パケットであることを意味します。(クライアント)SYN->(サーバー)SYN->(クライアント)ACT は、3 ウェイ ハンドシェイク プロセスです。
[P] は、これがデータ プッシュであることを意味します、サーバーから送信できます。クライアントからクライアントへ、またはクライアントからサーバーへプッシュできます。
[F] は、これが FIN パケットであることを意味し、クライアント/サーバーが接続を終了する操作を開始する可能性があります。
[R] は、これが RST パッケージであることを意味し、F パッケージと同じ効果がありますが、RST は、接続が閉じられたときにまだ処理されていないデータが存在することを示します。接続を強制的に切断すると理解できます
win 4099はスライディングウィンドウのサイズを指します
length 18はデータパケットのサイズを指します
(1)(2)(3)が3 つの手順は、tcp:
最初のハンドシェイクを確立することです:
14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378
クライアント IP localhost.39870 (通常、クライアントのポートは自動的に割り当て済み)をサーバーに localhost .9502 syn パッケージ (syn=j) をサーバーに送信》
syn パッケージ (syn=j): syn seq= 2927179378 (j=2927179378)
2 回目のハンドシェイク:
14: 12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.]、seq 1721825043、ack 2927179379、
リクエストを受信して確認: サーバーは syn パケットを受信し、クライアントの SYN (ack=j+1) を確認する必要があります)、同時に SYN パケット (syn=k)、つまり SYN+ACK パケットも送信します:
このとき、サーバー ホスト自身の SYN: seq: y= syn seq 1721825043。
ACK は j+1 = (ack=j+1) =ack 2927179379
3 回目のハンドシェイク:
14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1 .
クライアントはサーバーからSYN+ACKパケットを受信し、確認パケットACK(ack=k+1)をサーバーに送信します
クライアントとサーバーがESTABLISHED状態になると、通信データを交換できるようになります。この時間は、accepte インターフェイスとは関係ありません。accepte がなくても、3 ウェイ ハンドシェイクは完了します。
连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。
紫色背景的部分:
IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
客户端向服务器发送长度为7个字节的数据,
IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
服务器向客户确认已经收到数据
IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
然后服务器同时向客户端写入数据。
IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0
客户端向服务器确认已经收到数据
这个就是tcp可靠的连接,每次通信都需要对方来确认。
6. Linux における SOCKET プログラミングの詳細な説明
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
Linux における SOCKET プログラミングの詳細な説明如图:
过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
7. Socket编程实例
服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。
/* File Name: server.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #define DEFAULT_PORT 8000 #define MAXLINE 4096 int main(int argc, char** argv) { int socket_fd, connect_fd; struct sockaddr_in servaddr; char buff[4096]; int n; //初始化Socket if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){ printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } //初始化 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。 servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT //将本地地址绑定到所创建的套接字上 if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){ printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } //开始监听是否有客户端连接 if( listen(socket_fd, 10) == -1){ printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } printf("======waiting for client's request======\n"); while(1){ //阻塞直到有客户端连接,不然多浪费CPU资源。 if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){ printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; } //接受客户端传过来的数据 n = recv(connect_fd, buff, MAXLINE, 0); //向客户端发送回应数据 if(!fork()){ /*紫禁城*/ if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1) perror("send error"); close(connect_fd); exit(0); } buff[n] = '\0'; printf("recv msg from client: %s\n", buff); close(connect_fd); } close(socket_fd); }
客户端:
/* File Name: client.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #define MAXLINE 4096 int main(int argc, char** argv) { int sockfd, n,rec_len; char recvline[4096], sendline[4096]; char buf[MAXLINE]; struct sockaddr_in servaddr; if( argc != 2){ printf("usage: ./client <ipaddress>\n"); exit(0); } if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ printf("create socket error: %s(errno: %d)\n", strerror(errno),errno); exit(0); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8000); if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){ printf("inet_pton error for %s\n",argv[1]); exit(0); } if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){ printf("connect error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } printf("send msg to server: \n"); fgets(sendline, 4096, stdin); if( send(sockfd, sendline, strlen(sendline), 0) < 0) { printf("send msg error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) { perror("recv error"); exit(1); } buf[rec_len] = '\0'; printf("Received : %s ",buf); close(sockfd); exit(0); }
inet_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet_addr的扩展。
int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:
第一个参数af是地址族,转换后存在dst中
af = AF_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
af =AF_INET6:src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。
测试:
编译server.c
gcc -o server server.c
启动进程:
./server
显示结果:
======waiting for client's request======
并等待客户端连接。
编译 client.c
gcc -o client server.c
客户端去连接server:
./client 127.0.0.1
等待输入消息
发送一条消息,输入:c++
此时服务器端看到:
客户端收到消息:
其实可以不用client,可以使用telnet来测试:
telnet 127.0.0.1 8000
注意:
在ubuntu 编译源代码的时候,头文件types.h可能找不到。
使用dpkg -L libc6-dev | grep types.h 查看。
如果没有,可以使用
apt-get install libc6-dev安装。
如果有了,但不在/usr/include/sys/目录下,手动把这个文件添加到这个目录下就可以了。