目次
3つのハンドシェイクと4つの手を振る
スリーウェイハンドシェイク:接続の確立にはスリーウェイハンドシェイクプロセスが必要です
- 4つの波:切断には4つの波が必要です
- TCPは接続指向の安全なデータ送信です
- クライアントとサーバーが確立されると、3方向のハンドシェイクプロセスを実行する必要があります。クライアントとサーバーが切断されると、4つの手を振る必要があります。次の図は、接続、データを確立するためのクライアントとサーバー間の3方向のハンドシェイクを示しています。 4つの波を送信および切断するプロセス全体。
- TCPタイミング:
- グラフの意味
- SYN:リクエストを意味します
- ACK:確認を意味します
- mss:最大セグメントサイズを表します。セグメントが大きすぎて、フレームにカプセル化された後にリンクレイヤーの最大フレーム長を超える場合は、IPレイヤーでフラグメント化する必要があります。この状況を回避するには、クライアントが最大セグメントサイズを宣言します。サーバー側から送信されるセグメントは、この長さを超えないようにすることをお勧めします。
- サーバーから送信されるSYNとクライアント自体から送信されるSYNも1ビットを占有します
- 上の図では、ACKは確認シーケンス番号を表し、確認シーケンス番号の値は、相手から送信されたシーケンス番号の値+データの長さです。
- 特別な注意は、SYNとFIN自体も1つを占めることです。
- 注意:
- SYS ----->同期
- ACK ----->確認
- FIN ------>終了
- スリーウェイハンドシェイクとフォータイムウェーブオブハンドがカーネルに実装されています
TCPデータグラム形式
ウィンドウサイズ:バッファサイズを指します
- SYNフラグは、通信時に不要になり、接続を要求するときにのみ必要になります。
- データ送信時のランダムシーケンス番号seqは、相手に最後に送信されたACKのランダムシーケンス番号の値であり、相手に送信されたACKは、前回相手に送信されたばかりのACKの値です。
- 図で送信されたACK確認パケットは、相手にデータを送信したことの確認を表し、送信したすべてのデータを受信したことを示し、次回はシーケンス番号からデータを送信するように相手に指示します。
- データを送信するたびに相手から確認パケットが届くので、相手が受信したかどうかを確認できます。相手から確認パケットが届かない場合は再送信します。
- mss:最大メッセージ長、一度に最大で受信できる量を相手に伝えます。この長さを超えることはできません
- win:ここで最大キャッシュサイズを相手に伝えることを意味します
スライドウィンドウ(TCPフロー制御)
- 主な機能:スライディングウィンドウは主に流量制御用です
- 次の図を参照してください。送信側の送信速度が速い場合、受信側はデータの受信後にデータの処理を遅くし、受信バッファーのサイズは固定されているため、受信バッファーがいっぱいになり、データが失われます。TCPプロトコルは、「スライディングウィンドウ」メカニズムを通じてこの問題を解決します。
- 送信者は接続を開始し、最大セグメントサイズが1460、初期シーケンス番号が0、ウィンドウサイズが4Kであることを宣言します。これは、「受信バッファには4Kバイトの空き容量があり、送信するデータは4Kを超えてはならない」ことを意味します。受信側は接続要求に応答し、最大セグメントサイズが1024、初期シーケンス番号が8000、ウィンドウサイズが6Kであることを宣言します。送信者が応答し、3者間ハンドシェイクが終了します。
- 送信者は、それぞれ1Kのデータを含むセグメント4〜9を送信します。送信者は、ウィンドウサイズに応じて受信者のバッファがいっぱいであることを認識しているため、データの送信を停止します。
- 受信側のアプリケーションプログラムは2Kデータを取得し、受信バッファは再び2K空きになります。受信側は、セグメント10を送信し、ウィンドウサイズが2Kであることを宣言し、6Kデータを受信したことを応答します。
- 受信側のアプリケーションプログラムは再び2Kデータを取得し、受信バッファには4Kの空き容量があり、受信側はセグメント11を送信してウィンドウサイズを4Kとして再宣言します。
- 送信者は、それぞれ2Kデータを含むセグメント12〜13を送信し、セグメント13にはFINビットも含まれています。
- 受信側は受信した2Kデータ(6145-8192)に応答し、さらにFINビットはシーケンス番号8193を占めるため、応答シーケンス番号は8194であり、接続はハーフクローズ状態であり、受信側もウィンドウサイズが2Kであることを宣言します。
- 受信側のアプリケーションは2Kデータを取得し、受信側はウィンドウサイズを4Kとして再宣言します。
- 受信側のアプリケーションは残りの2Kデータを取得し、受信バッファーは完全に空になり、受信側はウィンドウサイズを6Kとして再宣言します。
- 受信側のアプリケーションは、すべてのデータを取り除いた後、接続を閉じることを決定します。送信セグメント17にはFINビットが含まれ、送信側は応答し、接続は完全に閉じられます。
- 上の図は、受信側の小さな四角を使用して1Kデータを示しています。小さな四角は受信データを示し、破線のボックスは受信バッファを示しています。したがって、図からわかるように、破線のボックス内の白抜きの四角はウィンドウサイズを示しています。 、アプリケーションがデータを取得すると、破線のフレームが右にスライドするため、スライディングウィンドウと呼ばれます。
- この例から、送信者が1Kと1Kでデータを送信し、受信側のアプリケーションが2Kと2Kでデータを引き出すことができることもわかります。もちろん、一度に3Kまたは6Kのデータを引き出すことも、一度にのみ行うこともできます。数バイトのデータ。つまり、アプリケーションから見たデータは全体またはストリームです。基礎となる通信では、これらのデータは送信される多数のデータパケットに分割される場合がありますが、データパケットにはアプリケーションのバイト数が含まれます。プログラムは表示されないため、TCPプロトコルはストリーム指向のプロトコルです。UDPはメッセージ指向のプロトコルです。各UDPセグメントはメッセージです。アプリケーションはメッセージ単位でデータを抽出する必要があり、一度に1バイトのデータを抽出することはできません。これはTCPとは大きく異なります。
- この図では、winは相手側にバッファのサイズを伝えることを意味し、mssは相手に最大で一度に受信できるデータの量を伝えることを意味し、この長さを超えない方がよいでしょう。
- クライアントがパッケージをサーバーに送信するとき、サーバーが応答パケットを返すまで待つ必要はありません。クライアントはサーバーのウィンドウサイズを知っているため、複数回送信を続けることができます。送信されたデータが相手のウィンドウサイズに達すると、再度送信されることはありません。相手が処理するのを待つ必要があり、相手は処理後も送信を続けることができます。
mss和MTU
- MTU:最大伝送ユニット
- MTU:通信用語最大伝送ユニット(最大伝送ユニット、MTU)
- 通信プロトコルの特定のレイヤーを通過できる最大データパケットサイズ(バイト単位)を指します。
- 最大伝送ユニットのパラメータは、通常、通信インターフェース(ネットワークインターフェースカード、シリアルポートなど)に関連します。この値を大きく設定しすぎると、パケットの損失や再送信時に大量のデータが再送信されます。図の最大値は1500です。実際には経験値です。
- MSS:最大メッセージ長は、ちょうどその時、接続が確立されて、私は受け取ることができますどのくらいのデータを相手に伝え、およびデータ通信の過程には、MSSはありません。
機能パッケージのアイデア
- 関数のカプセル化のアイデア-例外の処理
- man-pageとerrnoを組み合わせてカプセル化します。
- 名前をカプセル化するときは、最初の関数名の文字を大文字にすることができます。たとえば、socketをSocketにカプセル化できるため、Shift + kを押して検索できます。Shift+ kは、関数の説明を検索するときに大文字と小文字を区別せず、manページを使用できます。確認してください。manページは大文字と小文字を区別しません。
- 受け入れや読み取りなどのブロッキングを引き起こす可能性のある機能
- 信号によって中断された場合、信号の優先度が高いため、信号が最初に処理されます。信号の処理が完了すると、受け入れまたは読み取りのブロックが解除されて返されます。このとき、戻り値は-1に設定され、errno = EINTRに設定されます。
- errno = ECONNABORTEDは、接続が中断され、異常であることを示します。
- エルノ宏
- /usr/include/asm-generic/errno.hファイルには、errnoのすべてのマクロと対応するエラー記述情報が含まれています。
- wrap.h
#ifndef __WRAP_H_ #define __WRAP_H_ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit(const char *s); int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr); int Bind(int fd, const struct sockaddr *sa, socklen_t salen); int Connect(int fd, const struct sockaddr *sa, socklen_t salen); int Listen(int fd, int backlog); int Socket(int family, int type, int protocol); ssize_t Read(int fd, void *ptr, size_t nbytes); ssize_t Write(int fd, const void *ptr, size_t nbytes); int Close(int fd); ssize_t Readn(int fd, void *vptr, size_t n); ssize_t Writen(int fd, const void *vptr, size_t n); ssize_t my_read(int fd, char *ptr); ssize_t Readline(int fd, void *vptr, size_t maxlen); int tcp4bind(short port,const char *IP); #endif
- wrap.c
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit(const char *s) { perror(s); exit(-1); } int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) { int n; again: if ((n = accept(fd, sa, salenptr)) < 0) { if ((errno == ECONNABORTED) || (errno == EINTR)) goto again; else perr_exit("accept error"); } return n; } int Bind(int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = bind(fd, sa, salen)) < 0) perr_exit("bind error"); return n; } int Connect(int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = connect(fd, sa, salen)) < 0) perr_exit("connect error"); return n; } int Listen(int fd, int backlog) { int n; if ((n = listen(fd, backlog)) < 0) perr_exit("listen error"); return n; } int Socket(int family, int type, int protocol) { int n; if ((n = socket(family, type, protocol)) < 0) perr_exit("socket error"); return n; } ssize_t Read(int fd, void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = read(fd, ptr, nbytes)) == -1) { if (errno == EINTR) goto again; else return -1; } return n; } ssize_t Write(int fd, const void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = write(fd, ptr, nbytes)) == -1) { if (errno == EINTR) goto again; else return -1; } return n; } int Close(int fd) { int n; if ((n = close(fd)) == -1) perr_exit("close error"); return n; } /*参三: 应该读取的字节数*/ ssize_t Readn(int fd, void *vptr, size_t n) { size_t nleft; //usigned int 剩余未读取的字节数 ssize_t nread; //int 实际读到的字节数 char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ((nread = read(fd, ptr, nleft)) < 0) { if (errno == EINTR) nread = 0; else return -1; } else if (nread == 0) break; nleft -= nread; ptr += nread; } return n - nleft; } ssize_t Writen(int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; ptr += nwritten; } return n; } static ssize_t my_read(int fd, char *ptr) { static int read_cnt; static char *read_ptr; static char read_buf[100]; if (read_cnt <= 0) { again: if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) { if (errno == EINTR) goto again; return -1; } else if (read_cnt == 0) return 0; read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1; } ssize_t Readline(int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1) { *ptr++ = c; if (c == '\n') break; } else if (rc == 0) { *ptr = 0; return n - 1; } else return -1; } *ptr = 0; return n; } int tcp4bind(short port,const char *IP) { struct sockaddr_in serv_addr; int lfd = Socket(AF_INET,SOCK_STREAM,0); bzero(&serv_addr,sizeof(serv_addr)); if(IP == NULL){ //如果这样使用 0.0.0.0,任意ip将可以连接 serv_addr.sin_addr.s_addr = INADDR_ANY; }else{ if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){ perror(IP);//转换失败 exit(1); } } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); return lfd; }
スティッキーバッグのコンセプト
- スティッキーパケット:複数のデータ送信、エンドツーエンド接続、受信側は、最初に送信された量と2回目に送信された量を正しく区別できません。
- スティッキーパッケージ問題の分析と解決?
- スキーム1:パケットヘッダー+データ
- 4ビットのデータ長+データなど-----------> 00101234567890
- ここで、0010はデータ長を表し、1234567890は10バイトのデータを表します。
- さらに、送信者と受信者は、より複雑なメッセージ構造をネゴシエートできます。これは、両当事者が合意した合意に相当します。
- オプション2:終了マーカーを追加します。
- たとえば、最後の最後の文字は\ n \ $などです。
- 解決策3:固定長のデータパケット
- 送信者と受信者が毎回128バイトのコンテンツのみを送信することに同意した場合、受信者は128バイトの固定長を受信できます。
並行性の高いサーバー
- 複数のクライアントをサポートする方法---複数の同時サーバーをサポートする
- たとえば、読み取り時にacceptを呼び出して新しい接続を受け入れることはできず、acceptが待機中にブロックされると、データを読み取ることができません。
- 解決策:複数のプロセスを使用して、親プロセスに新しい接続を受け入れさせ、子プロセスにクライアントとの通信を処理させることができます
- アイデア:親プロセスに新しい接続の受け入れを受け入れさせ、次に子プロセスをフォークし、子プロセスに通信を処理させ、子プロセスが処理された後に終了します。親プロセスはSIGCHLD信号を使用して子プロセスをリサイクルします。
- 処理フロー:
1 创建socket, 得到一个监听的文件描述符lfd---socket() 2 将lfd和IP和端口port进行绑定-----bind(); 3 设置监听----listen() 4 进入while(1) { //等待有新的客户端连接到来 cfd = accept(); //fork一个子进程, 让子进程去处理数据 pid = fork(); if(pid<0) { exit(-1); } else if(pid>0) { //关闭通信文件描述符cfd close(cfd); } else if(pid==0) { //关闭监听文件描述符 close(lfd); //收发数据 while(1) { //读数据 n = read(cfd, buf, sizeof(buf)); if(n<=0) { break; } //发送数据给对方 write(cfd, buf, n); } close(cfd); //下面的exit必须有, 防止子进程再去创建子进程 exit(0); } } close(lfd);
- 追加する必要のある機能:親プロセスはSIGCHLD信号を使用して、子プロセスのリサイクルを完了します
- 注:受け入れまたは読み取り機能はブロッキング機能であり、信号によって中断されます。現時点ではエラーと見なされるべきではありません、errno = EINTR
サンプルコード
//多进程版本的网络服务器 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <ctype.h> #include "wrap.h" int main() { //创建socket int lfd = Socket(AF_INET, SOCK_STREAM, 0); //绑定 struct sockaddr_in serv; bzero(&serv, sizeof(serv)); serv.sin_family = AF_INET; serv.sin_port = htons(8888); serv.sin_addr.s_addr = htonl(INADDR_ANY); Bind(lfd, (struct sockaddr *)&serv, sizeof(serv)); //设置监听 Listen(lfd, 128); pid_t pid; int cfd; char sIP[16]; socklen_t len; struct sockaddr_in client; while(1) { //接受新的连接 len = sizeof(client); memset(sIP, 0x00, sizeof(sIP)); cfd = Accept(lfd, (struct sockaddr *)&client, &len); printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port)); //接受一个新的连接, 创建一个子进程,让子进程完成数据的收发操作 pid = fork(); if(pid<0) { perror("fork error"); exit(-1); } else if(pid>0) { //关闭通信文件描述符cfd close(cfd); } else if(pid==0) { //关闭监听文件描述符 close(lfd); int i=0; int n; char buf[1024]; while(1) { //读数据 n = Read(cfd, buf, sizeof(buf)); if(n<=0) { printf("read error or client closed, n==[%d]\n", n); break; } //printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port)); printf("[%d]---->:n==[%d], buf==[%s]\n", ntohs(client.sin_port), n, buf); //将小写转换为大写 for(i=0; i<n; i++) { buf[i] = toupper(buf[i]); } //发送数据 Write(cfd, buf, n); } //关闭cfd close(cfd); exit(0); } } //关闭监听文件描述符 close(lfd); return 0; }
- 解決策:マルチスレッドを使用する
- メインスレッドに新しい接続を受け入れさせ、子スレッドにクライアントとの通信を処理させます。マルチスレッドを使用してスレッドを分離属性に設定し、終了後にスレッドにリソースを再利用させます。
- サーバー開発プロセスのマルチスレッドバージョン
{ 1 创建socket, 得到一个监听的文件描述符lfd---socket() 2 将lfd和IP和端口port进行绑定-----bind(); 3 设置监听----listen() 4 while(1) { //接受新的客户端连接请求 cfd = accept(); //创建一个子线程 pthread_create(&threadID, NULL, thread_work, &cfd); //设置线程为分离属性 pthread_detach(threadID); } close(lfd); } 子线程执行函数: void *thread_work(void *arg) { //获得参数: 通信文件描述符 int cfd = *(int *)arg; while(1) { //读数据 n = read(cfd, buf, sizeof(buf)); if(n<=0) { break; } //发送数据 write(cfd, buf, n); } close(cfd); }
- 子スレッドはlfdを閉じることができますか?
- 子スレッドとメインスレッドはファイル記述子をコピーする代わりに共有するため、子スレッドはリスニングファイル記述子lfdを閉じることができません。
- メインスレッドはcfdを閉じることができますか?
- メインスレッドはcfdを閉じることができません。メインスレッドと子スレッドはcfdをコピーする代わりに共有します。閉じた後、cfdは実際に閉じられます。
- 複数の子スレッドがcfdを共有するため、cfdが最後の接続の値になり、前の値が上書きされます。アイデアの解決
struct INFO { int cfd; pthread_t threadID; struct sockaddr_in client; }; struct INFO info[100]; //初始化INFO数组 for(i=0; i<100; i++) { info[i].cfd=-1; } for(i=0; i<100; i++) { if(info[i].cfd==-1) { //这块内存可以使用 } } if(i==100) { //拒绝接受新的连接 close(cfd); }
サンプルコード
//多线程版本的高并发服务器 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <ctype.h> #include <pthread.h> #include "wrap.h" //子线程回调函数 void *thread_work(void *arg) { int cfd = *(int *)arg; printf("cfd==[%d]\n", cfd); int i; int n; char buf[1024]; while(1) { //read数据 memset(buf, 0x00, sizeof(buf)); n = Read(cfd, buf, sizeof(buf)); if(n<=0) { printf("read error or client closed,n==[%d]\n", n); break; } printf("n==[%d], buf==[%s]\n", n, buf); for(i=0; i<n; i++) { buf[i] = toupper(buf[i]); } //发送数据给客户端 Write(cfd, buf, n); } //关闭通信文件描述符 close(cfd); pthread_exit(NULL); } int main() { //创建socket int lfd = Socket(AF_INET, SOCK_STREAM, 0); //设置端口复用 int opt = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int)); //绑定 struct sockaddr_in serv; bzero(&serv, sizeof(serv)); serv.sin_family = AF_INET; serv.sin_port = htons(8888); serv.sin_addr.s_addr = htonl(INADDR_ANY); Bind(lfd, (struct sockaddr *)&serv, sizeof(serv)); //设置监听 Listen(lfd, 128); int cfd; pthread_t threadID; while(1) { //接受新的连接 cfd = Accept(lfd, NULL, NULL); //创建子线程 pthread_create(&threadID, NULL, thread_work, &cfd); //设置子线程为分离属性 pthread_detach(threadID); } //关闭监听文件描述符 close(lfd); return 0; }
- [注]:dark horse linux C ++チュートリアルを参照してください