記事ディレクトリ
エコークライアントの完璧な実装
エコークライアントの問題
前のセクションでは、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更新中~~~