JoeChenzzz

导航

TCP

1.TCP首部

2.三次握手

建立一个TCP连接时会发生下列步骤:

1)服务器被动打开:服务器做好接受外来的连接的准备,通过调用socket、bind和listen函数来完成,从CLOSED态转为LISTEN态

2)客户端主动打开:客户端通过调用connect发起主动打开,向服务器发送一个SYN(即一个在TCP首部的SYN字段置位的数据包),它告诉服务器客户端想连接的端口号和要传输数据的初始序列号ISN(c)(SYN分节不携带数据,只含有一个IP首部、一个TCP首部),从CLOSED态转为SYN_SENT态

3)服务器确认:服务器收到客户端的SYN,于是确认(ACK)客户端的SYN,其ACK值为ISN(c)+1;同时自己也发送一个SYN,它告诉客户端服务器要传输的数据的初始序列号ISN(s),从LISTEN态转为SYN_RCVD态

4)客户端确认:客户端确认服务器的SYN,将ISN(s)+1作为返回的ACK值发送给服务器,从SYN_SENT态转为ESTABLISHED态

5)服务器收到ACK,从SYN_RCVD态转为ESTABLISHED态,连接已建立,可以开始传输数据

注意

  • 整个过程需要三个分组,因此称为三次握手
  • 客户端和服务器每发送一个SYN,消耗一个序列号
  • 客户端给服务器发送的ACK,可以携带数据,也可以不携带数据,不携带数据则不消耗序列号
  • 客户端最后一个ACK丢失了,怎么办:当客户端端收到服务器的SYN+ACK应答后,其状态变为ESTABLISHED,并发送ACK包给服务器,如果此时ACK在网络中丢失,那么服务器保持其状态为SYN_RECV,并且依次等待3秒、6秒、12秒……后重新发送SYN+ACK包,以便客户端重新发送ACK包(服务器重发SYN+ACK包的次数,可以通过设置/proc/sys/net/ipv4/tcp_synack_retries修改,默认值为5)如果重发指定次数后,仍然未收到ACK应答,那么一段时间后,服务器自动关闭这个连接。但是客户端认为这个连接已经建立,如果客户端端向服务器写数据,服务器端将以RST包(这是一个指明TCP连接异常终止的数据包)响应,方能感知到服务器的错误

  • SYN洪泛攻击:客户端发送一大堆SYN请求,然后服务器相应回复SYN+ACK,此时服务器会对这些SYN请求分配大量TCB(TCP 传输控制块,是一种包含一个连接所有信息的传输协议数据结构,通常一个TCB至少280字节),然而这时候客户端收到这些SYN+ACK后,一个ACK都不回给服务器,根据上面的分析,服务器就会在超时时间内对每一个SYN+ACK进行重发,相当消耗CPU,而且维持大量TCB容易导致服务器资源消耗殆尽,非常容易发生崩溃
  • 解决
  1. 缩短SYN Timeout时间
  2. 设置SYN Cookie,就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续收到某个IP的重复SYN报文,就认定是受到了攻击,以后从这个IP地址来的包会被一概丢弃
  • 为什么是三次握手:防止已经失效的连接请求报文突然又到了服务端,从而引发错误,用上图来举例:

  • 服务器listen之后不accept,会是怎样一种情况,客户端connect会返回吗?
  1. 实际上只要服务端开启listen,在有限数量下,客户端就可以连接成功,并可以发送数据
  2. 在被动状态的socket有两个队列,一个是正在进行三次握手的socket队列,一个是已完成三次握手的socket队列。握手完成后会从正在握手队列移到握手完成的队列,此时已经建立连接。accept是从已完成三次握手的socket队列里面取,不accept客户端能完成连接,数量为正在进行三次握手的队列的大小(也就是listen函数中的第二个参数backlog,最大值为128)
  3. 不accpet只是不能继续通讯而已,三次握手是协议栈做的,不是api控制的

3.TCP的半开启

  如果在未告知另一端的情况下,通信的一端关闭,则称此时TCP处于半开启状态;这种情况通常发生在通信一方的主机崩溃的情况下

4.四次挥手

1)客户端主动关闭:客户端调用close关闭套接字,发送一个FIN,表示数据发送完毕,从ESTABLISHED态转为FIN_WAIT_1态

2)服务器被动关闭:服务器收到客户端的FIN,告知应用进程客户端已经提出了关闭的请求,然后确认ACK,ACK值为客户端FIN序列号Seq+1,从ESTABLISHED态转为CLOSE_WAIT态

3)客户端收到ACK,从FIN_WAIT_1态转为FIN_WAIT_1态

4)一段时间后,服务器发送完了剩余的所有数据后,再调用close关闭套接字,发送一个FIN,从CLOSE_WAIT态转为LAST_ACK态

5)客户端收到FIN,发送确认ACK,从FIN_WAIT_1态转为TIME_WAIT态,经过2MSL时间,再转为CLOSED态

6)服务器收到ACK,从LAST_ACK态转为CLOSED态

注意

  • 整个过程通常需要四个分组,因此称为四次挥手,“通常”是因为:某些情形下步骤1的FIN随数据一起发送;另外,步骤2和步骤3发送有可能被合并成一个分组
  • 客户端和服务器每发送一个FIN,消耗一个序列号

5.TCP的半关闭

5.1概念

  仅关闭数据流的一个传输方向

5.2用shutdown实现长时间的TCP半关闭

1)在多进程并发服务器模型中,close把套接字描述符的引用计数减1,只有在引用计数为0时才关闭套接字,而shutdown不管引用计数是否为0,直接切断共享套接字的所有连接,那些试图读的进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号(在读进程已终止时进行写操作,系统会产生此信号)

2)close同时关闭读和写两个方向,而shutdown可以调整其参数实现只关闭读或者只关闭写或者读写都关闭,通信的某端调用shutdown只关闭一个方向的数据传输,则实现了TCP的半关闭

6.TCP状态转换图

7.TCP网络编程一般步骤

  • 服务端:socket -> bind -> listen -> accept -> recv/send -> close
  • 客户端:socket -> connect -> send/recv -> close

注意

  • 服务器一定要bind:因为服务器总是被动方,需要在一个众所周知的端口上等待连接请求,而且作为服务器它的端口号应该是固定的,服务器bind一个端口就表示会在这个端口提供一些特殊的服务
  • 客户端通常不bind,它也可以bind,但是却不建议对客户端bind:客户端它是主动发起方,我们并不关心是客户端的哪个端口和服务器建立了连接。内核会自动为我们分配一个随机的不冲突的端口号,如果我们对客户端bind的话,反而有可能导致端口冲突
//TCP服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8888                        /*侦听端口地址*/
#define BACKLOG 2                        /*侦听队列长度*/

/*服务器对客户端的处理*/
void process_conn_server(int s)
{
    ssize_t size = 0;
    char buffer[1024];                            /*数据的缓冲区*/
    
    for(;;){                                    /*循环处理过程*/        
        size = read(s, buffer, 1024);            /*从套接字中读取数据放到                                                    缓冲区buffer中*/
        if(size == 0){                            /*没有数据*/
            return;    
        }
        
    write(s, buffer, size);                    /*发给客户端*/
    }    
}

int main(int argc, char *argv[])
{
    int ss,sc;        /*ss为服务器的socket描述符,sc为客户端的socket描述符*/
    struct sockaddr_in server_addr;    /*服务器地址结构*/
    struct sockaddr_in client_addr;    /*客户端地址结构*/
    int err;                            /*返回值*/
    pid_t pid;                            /*分叉的进行ID*/

    /*建立一个流式套接字*/
    ss = socket(AF_INET, SOCK_STREAM, 0);
    if(ss < 0){                            /*出错*/
        printf("socket error\n");
        return -1;    
    }
    
    /*设置服务器地址*/
    bzero(&server_addr, sizeof(server_addr));            /*清零*/
    server_addr.sin_family = AF_INET;                    /*协议族*/
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);    /*本地地址0.0.0.0*/
    server_addr.sin_port = htons(PORT);                /*服务器端口*/
    
    /*绑定地址结构到套接字描述符*/
    err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if(err < 0){/*出错*/
        printf("bind error\n");
        return -1;    
    }
    
    /*设置侦听*/
    err = listen(ss, BACKLOG);
    if(err < 0){                                        /*出错*/
        printf("listen error\n");
        return -1;    
    }
    
        /*主循环过程*/
    for(;;)    {
        socklen_t addrlen = sizeof(struct sockaddr);
        
        sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen); 
        /*接收客户端连接*/
        if(sc < 0){                            /*出错*/
            continue;                        /*结束本次循环*/
        }    
        
        /*建立一个新的进程处理到来的连接*/
        pid = fork();                        /*分叉进程*/
        if( pid == 0 ){                        /*子进程中*/
            process_conn_server(sc);        /*处理连接*/
            close(ss);                        /*在子进程中关闭服务器的侦听*/
        }else{
            close(sc);                        /*在父进程中关闭客户端的连接*/
        }
    }
}
//TCP客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888                                /*侦听端口地址*/

/*客户端的处理过程*/
void process_conn_client(int s)
{
    ssize_t size = 0;
    char buffer[1024] = "TCP TEST";                /*发送给服务器的测试数据*/

    write(s, buffer, 1024);                /*发送给服务器*/
    read(s, buffer, 1024);        /*从服务器读取数据*/
    printf("recved:%s\n",buffer);            /*打印数据*/
}

int main(int argc, char *argv[])
{
    int s;                                        /*s为socket描述符*/
    struct sockaddr_in server_addr;            /*服务器地址结构*/
    
    s = socket(AF_INET, SOCK_STREAM, 0);         /*建立一个流式套接字 */
    if(s < 0){                                    /*出错*/
        printf("socket error\n");
        return -1;
    }    
    
    /*设置服务器地址*/
    bzero(&server_addr, sizeof(server_addr));    /*清零*/
    server_addr.sin_family = AF_INET;                    /*协议族*/
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);    /*本地地址*/
    server_addr.sin_port = htons(PORT);                /*服务器端口*/
    
    /*将用户输入的字符串类型的IP地址转为整型*/
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);    
    /*连接服务器*/
    connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
    process_conn_client(s);                        /*客户端处理过程*/
    close(s);                                    /*关闭连接*/
    return 0;
}

运行结果:

8.TIME_WAIT状态

8.1如何产生TIME_WAIT状态

  率先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入TIME_WAIT状态,TIME_WAIT状态将保持2MSL秒,MSL值是数据包的最大生存时间;在此期间,这个连接的五元组(客户端IP地址和端口,服务端IP地址和端口号,协议号)将不能被使用

8.2为什么产生TIME_WAIT状态

1)可靠地实现TCP全双工连接的终止

  假设发起主动关闭的客户端最后发送的ACK在网络中丢失,执行被动关闭的服务器将会重发其FIN,在该FIN到达客户端之前,客户端必须维护状态信息(这条TCP连接的客户端IP地址和端口不能被立即释放),如果不维护这样一个TIME_WAIT状态,那么当服务器重发的FIN到达时,客户端会用RST包(reset包,用来异常地关闭连接)来响应,这会被服务器认为是有错误发生,然而这并非是异常发生了,只是ACK丢失了

2)让旧的数据包因过期而在网络中消失

  假设当前有一条TCP连接,使用完毕后被关闭;过了一段时间在相同的IP地址和端口之间建立一条新连接,后一个连接称为前一个连接的化身;此时如果前一个连接的数据包再现,否则前后两次连接的数据将错乱,所以TCP规定:将不会给处于TIME_WAIT状态的连接生成化身;这也就是为什么TIME_WAIT状态将保持2MSL秒,因为这个时间足以让某个方向的数据包最多存活MSL秒被丢弃,另一方向的应答最多存活MSL秒也被丢弃,这就保证了老的重复的数据包在网络中消失了

8.3如何避免TIME_WAIT状态占用资源

1)假设是客户端主动关闭,一般不用担心资源占有问题,因为客户端一般选用临时端口,再次创建连接会新分配一个临时端口

2)假设是服务器主动关闭,可能会出现大量TIME_WAIT状态,主要发生在高并发短连接的TCP服务器上:

  • 高并发使服务器在短时间范围内同时使用了大量socket
  • 短连接表示“业务处理+传输数据的时间远远小于TIME_WAIT超时的时间”的连接,1秒钟的短连接处理完业务,主动关闭这个连接之后,这个socket会停留在TIMEWAIT状态几分钟(默认为240s)
  • 持续的到达一定量的高并发短连接,会使服务器因socket不足而拒绝为一部分客户服务

解决

  • 通过调整内核参数解决,vim /etc/sysctl.conf加入以下内容:
  net.ipv4.tcp_syncookies = 1  //1表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
  net.ipv4.tcp_tw_reuse = 1  //允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
  net.ipv4.tcp_tw_recycle = 1  //1表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
  net.ipv4.tcp_fin_timeout = 30  //修改系統默认的TIMEOUT时间
  然后执行 /sbin/sysctl -p 让参数生效
  • 还可以可以通过socket的选项SO_REUSEADDR来通知内核:进程可以使用处于TIME_WAIT状态的端口

9.CLOSE_WAIT状态

9.1如何产生CLOSE_WAIT状态

  在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。

9.2出现大量close_wait的现象

1)通常来讲,CLOSE_WAIT状态的持续时间应该很短。但是在一些特殊情况下,就会出现连接长时间处于CLOSE_WAIT状态的情况,出现大量close_wait的现象,原因主要是出在程序本身:对方关闭了socket连接,但是我方忙于别的处理,没有关闭连接

2)解决:代码需要判断read,一旦读到0,断开连接;read返回负,检查一下errno,如果不是EAGAIN,就断开连接

10.TCP的可靠性保障

   理想的传输条件有一下两个理想条件

1)传输信道不产生差错(包括数据包发生改变、丢失、没有按序)

2)不管发送方发送数据的速度合理,能让接收方总是来得及处理收到的数据

  实际网络不具备以上两个理想条件,但TCP通过序号、确认号、重传机制来解决第一个问题,用滑动窗口机制来解决第二个问题

10.1确认和重传机制

1)发送方发送完一个分组之后,必须在收到相应确认之前保留已发送分组的副本,为可能发生的重传提供便利

2)分组和确认都必须编号,这样才能明确发送出去的分组和收到的确认之间的对应关系,当某一端开始自己的TCP会话时,它的初始序列号是随机的,可能是0和4,294,967,295之间的任意值,然而,像Wireshark这种工具,通常显示的都是相对序列号/确认号,而不是实际序列号/确认号,客户端和服务器端分别有自己的序列号

3)发送方设置超时计时器,只要超过一段时间还没有收到相应确认,就认为刚才发送的分组丢失了,于是重传分组(超时重传

10.2流量控制

1)TCP流量控制采用滑动窗口机制,让发送方的发送速率不要太快,要让接收方来得及接受

2)发送窗口swnd:发送方维持一个一定大小(单位为字节)的发送窗口,位于发送窗口得到分组都可以连续发送,不需要等待确认,这样提高了信道利用率

3)接收方采用累积确认:接收方不对收到的分组逐个发确认,而是在收到几个分组后,对按序到达的最后一个分组发送确认

4)发送方每收到一个确认,就把发送窗口向前滑动到相应分组的位置

5)发送窗口后沿有两种变化情况:不动(没有收到新的确认)和前移(收到了);前沿:通常是不断向前,也可能不动(收到新的确认但窗口缩小了正好可以不动),也可能向后(窗口缩小了),但TCP标准强烈不赞成后沿向后,因为窗口中的分组可能已经发送了但现在又要收缩窗口不发送它们,这会产生错误

6)持续计时器:考虑这么一种情况,接收方向发送方发送了零窗口值的通知报文段不久,其接收缓存又有了一些存储空间,于是又向发送方发送了非零窗口值的报文段,然而这个报文段却丢失了;于是此时发送方就一直在等接收方非零窗口的通知,接受方也一直在等发送方的发送数据,形成死锁局面

  为解决这个死锁,TCP为每个连接设有一个持续计时器,只要连接的某一方收到了零窗口通知,就开始启动持续计时器,若到了设置的时间,则这一方发送一个零窗口探测报文段(仅携带1字节数据),而对方就在确认这个探测报文时给出现在的窗口值,如果窗口值为0,则又启动持续计时器,如果不为0,则开始正常发送数据,破除死锁

11.TCP的拥塞控制

1)所谓拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载

2)流量控制是点对点通信量的控制,是个端到端的问题,而拥塞控制是对整个网络的一个全局性过程

3)为了集中精力讨论拥塞控制,假定:数据单方向传递,另一方向只传确认;接收方总有足够大的空间,因此发送窗口的大小由网络拥塞程度决定

4)拥塞窗口cwnd:发送方维持一个拥塞窗口变量,让自己的发送窗口=拥塞窗口

5)慢开始:一开始发送方设置cwnd=1,每经过一个传输轮次(每收到一次确认),cwnd就加倍(指数增长),发送窗口也就随之加倍

6)慢开始门限ssthresh用法:

  • cwnd < ssthresh 时,使用慢开始算法
  • cwnd > ssthresh 时,改用拥塞避免算法
  • cwnd = ssthresh 时,慢开始与拥塞避免算法任意

7)拥塞避免:每经过一个传输轮次(每收到一次确认),cwnd就+1(加法增长),发送窗口也就随之+1

8)网络拥塞:发送方没有按时(不是那个超时重传时间哦)收到确认视为网络拥塞

9)发生拥塞时,将ssthresh设为当前cwnd的一半,然后把cwnd赋值为1(乘法减小),重新执行慢开始算法

10)只采用慢开始和拥塞避免算法是过去TCP的做法,现在的TCP又在这两种算法的基础上添加了快重传快恢复

11)快重传:当发送方没有按时收到接收方的确认,并不将ssthresh设为当前cwnd的一半,然后把cwnd赋值为1,而是让接收方发出重复确认:如图,M3在传输过程中丢失了,接收方没有收到M3就直接收到了M4,由于失序,接收方并不会对M4进行确认,而是重复发送三次上一次的确认,即M2的确认;发送方一连收到三个重复确认则立即重传丢失的报文(注意这里丢失报文号和重复确认号的关系,看图哦)

  快重传使得发送方尽早重传丢失的报文,提高吞吐量

12)快恢复:当发送方一连收到三个重复确认时,再将ssthresh设为当前cwnd的一半,然后把cwnd赋值为此时的ssthresh值,再执行拥塞避免算法,因为此时发送方认为网络很可能没有拥塞(如果拥塞,就不可能一连有好几个报文段连续到达接收方,也就不会导致接收方连续发送重复确认),所以不将cwnd赋值为1,这样就恢复得更快

13)接收窗口rwnd:回到第3点的假定,当时我们接收方总有足够大的空间,因此发送窗口的大小由网络拥塞程度决定,但实际上接收方的缓存空间总是有限的,接收方会根据自己的接受能力设定接收窗口值,写到TCP首部的窗口字段来告诉发送方,接收窗口又叫作通知窗口;所以,有公式:

发送窗口的上限值 = Min [ 接收窗口rwnd 拥塞窗口cwnd  ]

12.糊涂窗口综合症

1)糊涂窗口综合症是指当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小, 极端情况下,有效载荷可能只有1个字节,传输开销却有40字节(20字节的IP头+20字节的TCP头),导致网络上存在很多小分组,容易发生网络拥塞

12.1发送方

1)发送方引发糊涂窗口:如果发送端的应用程序产生数据很慢(典型的有telnet应用),例如,一次产生一个字节,这个应用程序一次将一个字节的数据写入发送端的TCP的缓存,如果发送端的TCP没有特定的指令,它就产生只包括一个字节数据的报文段,结果有很多41字节的IP数据报就在互连网中传来传去

2)发送方解决方法:Nagle算法

  • Nagle算法主要做两件事:
  1. 只有上一个分组得到确认,才会发送下一个分组
  2. 在等待确认期间如果产生了很多小数据块(小于MSS的任何数据块,Max Segment Size,TCP数据包每次能够传输的最大数据部分的长度),则收集它们,在一个确认到来时一起发送
  • 具体规则:
  1. 如果包长度达到MSS,则允许发送
  2. 如果该包含有FIN,则允许发送
  3. 设置了TCP_NODELAY选项,则允许发送
  4. 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送
  5. 上述条件都未满足,但发生了超时(一般为200ms),则立即发送
  • Nagle算法只允许一个未被ACK的包存在于网络,它并不管包的大小,因此它事实上就是一个扩展的停-等协议,只不过它是基于包停-等的,而不是基于字节停-等的。Nagle算法完全由TCP协议的ACK机制决定,这会带来一些问题,比如如果对端ACK回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低

12.2接收方

1)接收方引发糊涂窗口:如果接收端的应用程序消耗数据很慢,例如,一次消耗一个字节,假定接收端的接收缓冲区为4000字节,通知发送方接收窗口为4000字节,发送方发送一个4000字节的数据,然后缓冲区满了,于是通知窗口大小为零,发送端停止发送数据;接收方从缓冲区中读取1字节的数据,现在缓冲区有了1字节的空间,接收方通知其窗口大小为1字节,发送方只能发送包括1字节数据的报文段,这样的过程一直继续下去,数据1字节1字节地被消耗,结果有很多41字节的IP数据报就在互连网中传来传去

2)接收方解决方法1:Clark法

  • 简单来说,Clark法就是只要有数据到达就发送确认,但通知的窗口大小为零,直到缓冲区的空余空间已达MSS,或者缓冲区的一半已经空闲了

3)接收方解决方法2:延迟ACK

  • 当一个分组到达时接收方并不立即发送确认,直到缓冲区有足够的空余空间为止
  • 延迟ACK防止了发送方滑动其窗口,当发送方发送完其数据后,它就停下来了,这样就防止了这种症状。
  • 延迟ACK还能减少了通信量,接收端不需要确认每一个报文段。
  • 但延迟ACK也有一个缺点:由于发送方迟迟收不到确认,有可能迫使发送方重传其未被确认的报文段,可以用协议来平衡这个优点和缺点,例如现在定义了确认的延迟不能超过500毫秒

13.Nagle算法与CORK算法

1)所谓的CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置TCP_CORK选项后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据(不可能让数据一直等待吧)

2)TCP_CORK选项的实现可能并不像你想象的那么完美,在限制时间内没有拼接成一个大包(努力接近MTU)的话,内核就会无条件发送,也就是说如果应用层程序发送小包数据的间隔很长,TCP_CORK就没有一点作用,小包由于超时仍会被发送,反而失去了数据的实时性(因为每个小包数据都会延时一定时间再发送)

3)CORK算法是Nagle算法的设计初衷不同

4)Nagle算法的主旨在于避免网络因为太多的小包(协议头的比例非常之大)而拥塞,Nagle算法只是阻止了发送大量的小包,但还是会有小包发送

5)而CORK算法则是为了提高网络的利用率,使数据包协议头占用的比例尽可能的小,侧重于彻底阻止小包的发送,让所有小包组成大包发送

6)UDP_CORK选项:UDP没有确认和重传机制,因此没有设定超时时间,因此它是真正承诺的CORK,除非你在应用层手工拔掉塞子,否则数据将不会发出

14.TCP“黏包”

1)TCP是面向字节流,它把所有的消息都看成一连串的字节,没有消息边界

2)“黏包”现象是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾

3)造成“黏包”现象的原因:

  • 发送方:TCP默认使用Nagle算法,所以有可能造成“黏包”现象
  • 接收方:接收方接收分组的速度大于处理缓冲区数据,造成多个包存至缓冲区,首尾黏在了一起

4)什么时候需要处理“黏包”现象?

  • 当发送的数据有好几种结构时,要处理“黏包”:
  1. 结构1:"hellogive me sth abour yourself"
  2. 结构2:"Don'tgive me sth abour yourself"
  3. 如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hellogive me sth abour yourselfDon't give me sth abour yourself",这样接收方就傻了,因为协议没有规定这么诡异的字符串,所以双方要提前协商好把包分开的处理机制

4)处理“黏包”:

  • 发送方:使用TCP_NODELAY选项来关闭Nagle算法
  • 应用层处理:
  1. 以特殊字符作为消息的分界符
  2. 在消息头部加入整个消息的长度信息

5)UDP面向报文,不会对数据进行合并,对应用层交付下来的数据,直接加上UDP首部,就交给IP层,也就是说UDP有消息边界

参考资料:

https://www.cnblogs.com/zhaoyl/archive/2012/09/20/2695799.html

https://www.cnblogs.com/qiaoconglovelife/p/5733247.html

https://www.cnblogs.com/kex1n/p/6502002.html

posted on 2019-02-27 17:19  JoeChenzzz  阅读(297)  评论(0编辑  收藏  举报