TCP IP ネットワーク プログラミング (5) TCP ベースのサーバーとクライアント (補足)

エコークライアントの完璧な実装

エコークライアントの問題

前のセクションでは、TCP ベースのサーバーとエコー クライアントに問題があると説明しました。

データが大きすぎる場合、オペレーティング システムはデータを複数のパケットに分割してクライアントに送信し、クライアントはすべてのデータ パケットを受信する前に読み取り関数を呼び出すことがあります。

問題はサーバーではなくクライアントにあります。まず、クライアント側とサーバー側の I/O 関連コードを比較してみましょう。

サービスターミナル

while((str_len = read(cknt_sock, message, BUF_SIZE)) != 0)
    write(clnt_sock, message, str_len);

クライアント

write(sock, message, strlen(message))
str_len = read(sock, message, BUF_SIZE - 1);

クライアントとサーバーの両方が読み取り関数と書き込み関数を呼び出します。実際、Echo クライアントは送信するデータを 100% 受け入れますが、データの受信時にユニットに問題があります。

echoクライアントが送信するのは文字列であり、write関数を呼び出して一度送信した後、read関数を呼び出して送信した文字列の受信を待ちます。

ここで問題は、クライアントがすべての文字列データを受信するときに発生します。どれくらいの時間待つ必要があるでしょうか? 待ってからread関数を呼び出すと一気に読み込みが完了するのでしょうか?

Echo クライアントの問題解決

受信データのサイズは事前に決定できるため、この問題を解決することは難しくありません。以前に 100 バイト長の文字列を送信したことがある場合、受信時に read 関数がループ内で呼び出され、100 バイトのクラスを読み取ります。 。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len, recv_len, recv_cnt;
	struct sockaddr_in serv_adr;

	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 0);   
	if(sock==-1)
		error_handling("socket() error");
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));
	
	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("connect() error!");
	else
		puts("Connected...........");
	
	while(1) 
	{
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
			break;

		str_len=write(sock, message, strlen(message));
		
		recv_len=0;
		while(recv_len<str_len)
		{
			recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
			if(recv_cnt==-1)
				error_handling("read() error!");
			recv_len+=recv_cnt;
		}
		
		message[recv_len]=0;
		printf("Message from server: %s", message);
	}
	
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

前の例では、read 関数は 1 回だけ呼び出されていましたが、上記の例では、送信されたすべてのデータを受信するために、ループ内で read 関数を呼び出します。

while(recv_len<str_len)
{
	recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
	if(recv_cnt==-1)
		error_handling("read() error!");
	recv_len+=recv_cnt;
}

上記のコードを次のように変更すると、

while(recv_len != str_len)
{
	recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
	if(recv_cnt==-1)
		error_handling("read() error!");
	recv_len+=recv_cnt;
}

受信データのサイズは送信データと同じである必要があるため、recv_len に保存されたデータは str_len に保存されたデータと等しくなり、while ループが終了します。読者は、このループの書き込み方法がより論理的であると考えるはずですが、ループ処理なので、例外が発生しても無限ループにならないよう、可能な限りwhile(recv_len<str_len)を使用してください。

TCPの原則

TCPソケットでのI/Oバッファリング

TCP ソケットのデータ送受信はボーダレスです。サーバーが 40 バイトのデータを送信するために write 関数を 1 回呼び出したとしても、クライアントは 4 回の read 関数呼び出しで毎回 10 バイトを読み取る可能性があります。しかし、問題は再び発生します。サーバーは 40 バイトは一度に送信されますが、クライアントはそれをバッチでゆっくりと受信できます。クライアントが 10 バイトを受信した後、残りの 30 バイトはどこで待機しているのでしょうか?

実際には、write 関数が呼び出された直後にデータが送信されるわけではなく、read 関数が呼び出された直後にデータが受信されるわけでもありません。より正確には、書き込み関数が呼び出された瞬間にデータが出力バッファに移動され、読み取り関数が呼び出された瞬間にデータが入力バッファから読み取られます。

ここに画像の説明を挿入します

write 関数が呼び出されると、データは出力バッファに移動され、適切なタイミングで相手の入力バッファに渡されますが、このとき相手は read 関数を呼び出して入力バッファからデータを読み取ります。

I/Oバッファの特性は以下のとおりです。

  • I/O バッファリングは各 TCP ソケット内に個別に存在します。
  • I/Oバッファリングはソケットの作成時に自動的に生成されます
  • 出力バッファに残ったデータは、ソケットが閉じられても配信され続けます。
  • ソケットを閉じると入力バッファ内のデータが失われます

TCPの内部構造1:相手のソケットへの接続

TCP ソケットの作成から消滅までのプロセスは、次の 3 つのステップに分かれています。

1. 相手のソケットとの接続を確立する

2.相手のソケットとデータをやり取りする

3. 相手のソケットから切断する

TCP は実際の通信プロセス中に 3 つの対話プロセスを経ることになるため、このプロセスは 3 ウェイ ハンドシェイクとも呼ばれます。

ここに画像の説明を挿入します

ソケットは全二重モードで動作します。これは、ソケットが両方向にデータを転送できることを意味します。

1.接続を要求するホスト A は、次の情報をホスト B に渡します。

[SYN] SEQ: 1000、ACK: -

このメッセージの SEQ は 1000 で、ACK は空です。SEQ 1000 の意味は次のとおりです。

現在送信中のデータパケットのシリアル番号は100です。正しく受信できましたら、データパケットNo.1001をお送りするようお知らせください。

2.これは、初めて接続を要求するときに使用されるメッセージであり、SYN とも呼ばれ、データの送受信の前に送信される同期メッセージを表します。次に、ホスト B は次のメッセージを A に送信します。

[SYN+ACK] SEQ: 2000、ACK: 1001

このとき、SEQは2000、ACKは1001であり、SEQ2000の意味は以下の通りです。

現在送信中のデータパケットのシリアル番号は2000です。受信が正しければ、データパケット番号2001をお送りするようご連絡ください。

ACK 1001 の意味は次のとおりです。

先ほど送信した SEQ 1000 のデータ パケットは正しく受信されました。今度は SEQ 1001 のデータ パケットを渡してください。

ホスト A の初回送信時のデータパケット確認メッセージ (ACK 1001) とホスト B のデータ送信準備のための同期メッセージ (SEQ 200) をまとめて送信するため、この種のメッセージは SYN + ACK とも呼ばれます。

3.データを送受信する前に、データパケットにシーケンス番号を付与し、相手にシーケンス番号を通知することで、データの損失を防ぐ準備が整います。TCPは、データパケットにシーケンス番号を付与して確認することで、データ損失時に即座に確認・再送できるため、確実なデータ伝送を実現します。最後に、ホスト A から B に送信されたメッセージを観察します。

[ACK] SEQ:1001、ACK:2001

TCP 接続中にデータ パケットを送信する場合、シーケンス番号を割り当てる必要があります。これは、前のシーケンス番号 100 に基づいて +1 です。このとき、データ パケットは次のメッセージを配信します。

送信された SEQ 2000 のデータ パケットは正しく受信され、SEQ 2001 のデータ パケットを送信できるようになりました。

TCP の内部動作原理 2: 他のホストとのデータ交換

最初の 3 ウェイ ハンドシェイク プロセスにより、データ交換の準備が完了し、正式なデータの送受信が開始されます。

ここに画像の説明を挿入します

まず、ホスト A が 1 データ パケットで 100 バイトのデータを送信します。データ パケットの SEQ は 1200 です。これを確認するために、ホスト B はホスト A に ACK1301 メッセージを送信します。

このときの ACK 番号は 1201 ではなく 1301 です。これは、ACK 番号の増加分が送信データのバイト数であるためです。データ パケットのデータ パケットが確認できる場合と確認できない場合があります。100 バイトが正しく送信されていないか、失われたことは明らかなので、ACK メッセージは次の式に従って送信されます。

ACK 番号 - SEQ 番号 + 転送バイト数 + 1

3ウェイハンドシェイクプロトコルと同様、最後の+1は次回送信するSEQ番号を相手に通知します。

送信中のデータ パケットの消失を分析してみましょう。

ここに画像の説明を挿入します

SEQ1301 データ パケットは 100 バイトのデータをホスト B に送信しますが、途中でエラーが発生し、ホスト B はそれを受信しませんでした。一定の時間が経過しても、ホスト A は依然として SEQ 1301 に対する ACK 確認を受信しなかったため、データパケットが再送信されました データを完了するためにパケットが再送信されます TCP ソケットは ACK 応答を待つタイマーを開始します 対応するタイマーがタイムアウトすると、パケットは再送信されます

TCP の内部動作 3: ソケットからの切断

まず、ソケット A がソケット B に切断メッセージを送信し、ソケット B が確認メッセージを送信し、次にソケット A に切断メッセージを送信します。

ここに画像の説明を挿入します

データ パケット内の FIN は切断を示します。双方が FIN メッセージを 1 回送信してから切断します。このプロセスは 4 つの段階を経るため、4 ウェイ ハンドシェイクとも呼ばれます。

要約する

「TCP/IP ネットワーク プログラミング」コラムの 3 回目の記事です。読者の皆様もぜひご購読ください。

詳細については、クリックしてリンクの説明を追加してください。読者はスターにアクセスしてください。

⭐学術交流グループQ 754410389更新中~~~

おすすめ

転載: blog.csdn.net/m0_63743577/article/details/132778867