IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> LWIP/TCPIP糊涂窗口综合症 -> 正文阅读

[网络协议]LWIP/TCPIP糊涂窗口综合症

?????????TCP协议栈基于滑动窗口动态调整机制进行流量控制会导致一种被称为“糊涂窗口综合症SWS (Silly WindowSyndrome)"的状况。当TCP接收方通告了一个小窗口,并且TCP发送方立即发送数据填充该小窗口时,糊涂窗口综合症SWS (Silly WindowSyndrome)就会发生。

????????糊涂窗口综合症能够导致网络性能严重下降。当TCP的双方都是以小窗口通告和小报文段发送来实现通信,会使TCP数据流包含很多非常小的报文段,而不是满长度的报文段;而小单元报文段中IP首部和TCP首部这些字段占了大部分空间,会导致真正有效的TCP数据却很少,因此小报文的传输浪费了网络的大量带宽,从而网络性能严重下降。

????????糊涂窗口综合症可以由TCP连接双方中的任何一方引起。这是由于接收方可以通告一个小的窗口(而不是一直等到有大的窗口时才通告),而发送方也可以发送少量的数据(而不是等待更多的数据以便发送一个大的报文段)。为了避免SWS的发生,发送方和接收方必须设法消除这种情况。解决措施:接收方不必通告小窗口更新,而发送方不必发送小的报文段。在任何一方采取措施,都可以消除糊涂窗口综合症的发生。

????????对于接收端的优化

????????1. 应用程序上层读取数据后,TCP接收窗口会变大,但是接收方在小窗口更新的情况下不对发送方进行窗口通告,而是等待窗口大小满足一定的条件之后【能够接收一个最大报文段,或者增加接收缓冲区的一半】,再来发送窗口通告,这样就不会产生小报文。我们看看lwip对这一优化的实现

/* 更新通告窗口大小 */
u32_t tcp_update_rcv_ann_wnd(struct tcp_pcb *pcb)
{
    /* 计算出新窗口右边界值 */
    u32_t new_right_edge = pcb->rcv_nxt + pcb->rcv_wnd;

    /* 当前窗口大小更新大于一个mss或者接收窗口大小的一半 */
    if (TCP_SEQ_GEQ(new_right_edge, pcb->rcv_ann_right_edge + LWIP_MIN((TCP_WND / 2), pcb->mss))) 
    {
        /* 更新新的通告窗口 */
        pcb->rcv_ann_wnd = pcb->rcv_wnd;
        return new_right_edge - pcb->rcv_ann_right_edge;
    } 
    else
    {
        /* 下一个期望接收的数据已经大于右边界值,表示接收窗口大小为0*/
        if (TCP_SEQ_GT(pcb->rcv_nxt, pcb->rcv_ann_right_edge)) 
        {
            /* 设定接收窗口大小为 0*/
              pcb->rcv_ann_wnd = 0;
        } 
        return 0;
    }
}

????????2. 推迟确认(Delay ACK)的方法避免SWS,这时的做法是当接收窗口未达到满足要求的通告大小时,TCP推迟确认的发送(数据发送也会捎带ack)。推迟确认需要把握好推迟的时间,否则可能导致接收方因为等不到确认而重传报文段,TCP标准中规定,TCP实现对确认最多只能推迟500ms。同时,为了不影响发送方的RTT估计,接收方最好能保证每隔一个报文段进行一次确认。在LwIP作为接收方时,它采用了推迟确认的方法来避免SWS的发生。在控制玦的flags字段的TF_ACK_DELAY位表示了当前有ACK被延迟,我们来看看宏tcp_ack的定义。

#define tcp_ack(pcb)
do {
    if((pcb)->flags & TF_ACK_DELAY) {
        (pcb)->flags &=?TF_ACK_DELAY;
        (pcb)->flags |= TF_ACK_NOW;
        tcp_output(pcb);
    }
    else 
    {
        (pcb)->flags |= TF_ACK_DELAY;
    }
} while (0)

????????tcp_ack被调用时,会根据flags中TF_ACK_DELAY和TF_ACK_NOW两位的状况来决定是否发送ACK。若控制块上有ACK被延迟,则在内核的250ms周期性函数tcp_fasttmr中,会在该控制块上立即发送ACK。

????????对于发送端的优化

????????1. 对于从应用程序中接收到的第一块数据【没有任何等待ACK的包】,立即发送,一个字节的数据也需要发送出去。

? ? ? ? 2.推迟小报文段的发送,TCP尽量组织后续数据成为一个大的报文段发送。

????????那么一个小报文段在被发送出去之前能够推迟多久呢? TCP也无法预计应用程序发送数据的时间和规律,等待时间太久会导致应用程序的延时过大,而太短会使报文段依旧很小,浪费网络带宽。通常发送端使用了一种自适应的方法,利用确认的到来去触发其余分组的传输。

????????在连接上仍有己传输数据还未被确认的情况下,如果发送应用程序又产生了新的数据发送,那么TCP会照常把这些数据放入到输出缓冲中,但此时并不立即发送报文段,而是等待这些数据能够填满一个最大长度的报文段之后,才把缓冲区中的数据组织成一个报文段发送出去,这种策略适用于任何情况,包括推操作在内。这就是Nagle算法。来看看在报文段发送时,LwIP是如何避免糊涂窗口的,报文段的发送在函数tcp_output中完成。

err_t tcp_output(struct tcp_pcb *pcb)
{
    ......
    //取得第一个报文段
    seg = pcb->unsent;
    //若整个报文段在有效发送窗口内
    while (seg != NULL && ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) 
    {
        // nagle 算法阻止发送报文段,当有内存标志错误过着fin标志时,nagle算法失效
        if((tcp_do_output_nagle(pcb) = 0) &&
        ((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) = 0))
        {
            break;
        }
        //从队列中删除报文段
        pcb-〉unsent = seg->next;

        if(pcb->state != SYN_SENT) 
        {
            //设置 ACK 标志
            TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);
            //清除相关标志
            pcb->flags &=?(TF_ACK_DELAY | TF_ACK_NOW);
        }
        //发送报文段
        tcp_output_segment(seg, pcb);
        //取得下一个报文段
        seg = pcb->unsent;
        ......
    }
    ......
}

? ? ? ? while语句中的循环条件,它要求待发送的所有报文段数据都必须在有效发送窗口内,这样即使对方通告了一个很小的窗口,大报文段也不会被发送出去。对于其他情况,则可以利用Nagle算法来判断是否发送报文段。注意,如果flags的TF_NAGLEMEMERR和TF_FIN标志置位,Nagle算法失效,即任何报文都不会被Nagle算法阻止。TF_NAGLEMEMERR可以理解为内存错误,它是在tcp_enqueue函数组装报文时,出现可用发送缓存空间不足时置位的,当这种情况发生时,己经组装好的报文段需要尽快被发送,为后续发送预留出空间;TF_FIN标志置位时说明上层应用发出了关闭连接命令,此时也应尽快发送连接上的报文段。

Nagle算法是通过宏tcp do output nagle来实现的,代码如下:

#define tcp_do_output_nagle(tpcb) ((((tpcb)->unacked = NULL) || \
((tpcb)->flags & (TF_NODELAY | TF_INFR)) || \
(((tpcb)->unsent != NULL) && (((tpcb)->unsent->next != NULL) || \
((tpcb)->unsent->len >= (tpcb)->mss)))) ? 1 : 0)

????????Nagle算法有效阻止报文段发送的情况具体是:unacked队列不为空,且Nagle算法已使能(TF_NODELAY未置位)、控制块不处于快速重传状态(TF_INFR未置位),且unsent发送队列上的报文段少于两个,且第一个报文段的长度小于最大报文段长度时。当上述条件都成立时,tcp do output nagle取值为0,在tcp_output函数调用时没有报文段被发送。而在tcp_enqueue每次组装报文段的过程中,它都会试图将新报文段和未发送队列上的前一个报文段进行合并。很多情况下需要禁止Nagle算法,这可以直接通过设置控制块flags字段中的TF_NODELAY标志来实现。

? ? ? ? 我们在做网络开发中,会经常使用协议来进行数据数据交互,我们经常会遇到粘包现象,造成这个原因有两个:1.接收端的粘包,?如果接收端来不及从socket缓冲区中读取数据, 那么就会造成粘包;2.接收端的粘包, 则主要归因于TCP nagle算法。

? ? ? ? 对于粘包,我们可以在发送端禁止nagle算法,接收快速从socket中读取数据,但是这也并不一定能解决粘包问题,定义在协议数据传输中的数据,我们可以使用单字节数据解析来解决粘包问题。例如:

while(wifi_running)
		{
            /* 未解析完的数据放到数据头部,等待下一次数据读取解析,也可以设置超时数据归零 */
			if(data_backup_len > 0)
			{
				rt_memcpy(ap_trans_buf[ap_trans_buf_choise], data_backup_buf, data_backup_len);
				data_len = data_backup_len;
				
				data_backup_len = 0;
				rt_memset(data_backup_buf, 0, sizeof(data_backup_buf));
			}
			
            /* 从缓冲区读取数据 */
			read_cnt = rt_device_read(ap_trans_dev, 0 , ap_trans_buf[ap_trans_buf_choise]+data_len, buf_len-data_len);
			if(read_cnt)
			{
				data_len += read_cnt;
				wifi_recv_len += read_cnt;
			}
			else
			{
				if(data_len > 0)
				{
					if(rt_memcmp(ap_trans_buf[ap_trans_buf_choise], wifi_error_data, sizeof(wifi_error_data)) != 0)
					{
                        /* 循环单字节解析数据 */
						for(index = 0; index < data_len; index++)
						{
							ret_flag = inf_wifi_parse_char(ap_trans_buf[ap_trans_buf_choise][index]);
							if((ret_flag == 0x01) || (ret_flag == 0x02))
							{
								recv_tick_time_count = 0;
								connect_indicator_flag = 0x02;

								if((index+1) != data_len)
								{
									data_backup_len = data_len - (index+1);
									rt_memcpy(data_backup_buf, ap_trans_buf[ap_trans_buf_choise]+(index+1), data_backup_len);
								}
								get_parse_buffer(ap_trans_buf[ap_trans_buf_choise],buf_len);
								get_parse_total_len(&send_len);
								if(ret_flag == 0x01)
								{
									rt_device_write(uwb_trans_dev, 0 ,ap_trans_buf[ap_trans_buf_choise], send_len);
								}
								else if(ret_flag == 0x02)
								{
									wifi_set_storage(ap_trans_buf[ap_trans_buf_choise],ap_trans_dev);
								}
								
								
								ap_trans_buf_choise++;
								if (ap_trans_buf_choise > (WIFI_BUFFER_NUM - 1))
								{
									ap_trans_buf_choise = 0;
								}
								break;
							}
						}
					}
					data_len = 0;
					rt_memset(ap_trans_buf[ap_trans_buf_choise], 0, buf_len);
				}
			}
			
			rt_thread_delay(1);
		}

????????文档内容参考了TCPIP详解、老衲五木(朱升林)的微博。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-11 12:47:32  更:2021-08-11 12:49:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 19:35:41-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码