5.网络通讯-TCP/IP协议深入浅出分析及疑难杂症

谈起网络通讯及TCP/IP协议大家都可以说出一些自己的看法,但是网络通讯有是一个极为复杂工程,在上篇我简单的从应用路由的角度分析了网络通讯七层模型、HTTP协议,但是这些上层的协议和TCP/IP协议是什么关系?TCP/IP协议在七层模型中所处什么层次?TCP/IP协议到底解决什么问题?.......等等我可以提出一堆自己关于对通讯的疑惑,下面我就和大家一起揭开TCP/IP的神秘面纱。

一、网络通讯的基础是什么?

网络通讯7层模型是网络通讯的整体模型,但是从应用的角度上看,网络通讯的基础是什么?回答这个问题前我们先想一下应用的网络通讯最底层处于7层模型什么位置,答案是处于传输层。那我们在想一下传输层下面一层的网络层最关注什么?IP、端口是网络层完成互联通讯的基础。

二、TCP/IP协议与SOCKET关系?

  • Java中socket实现

       Java语言中socket默认支持的就是tcp/ip协议,java在初始化socket的时候没有给出socket类型选择直接默认为tcp/ip,可能java这样做也是为了统一,java关于socket的设置都是集中在SocketOptions.java类。

5.网络通讯-TCP/IP协议深入浅出分析及疑难杂症_第1张图片 1.socket在通讯中所在层次
  • Linux 中socket实现

   从Linux源码我们可以看出socket与tcp/ip协议是没有直接的联系的。socket可以绕过tcp/ip直接操作原始网络(链路层),可以创造一种新的协议出来。Linux源码中大量的socket初始化就是指支持tcp/ip的socket。

//Linux socket类型定义源码
#include 


enum sock_type {
    //固定长度、无连接的、不可靠的报文传递、主要用于局域网广播场景可能丢包 比如UDP协议
	SOCK_DGRAM	= 1,
    //有序的、可靠的、双向的、面向连接的字节流  主要用于无丢包场景 比如TCP/IP
	SOCK_STREAM	= 2,
    //提供原始网络协议 通过这个可以绕开tcpip协议直接操作链路层,定义自己的协议
	SOCK_RAW	= 3,
    //提供可靠的数据包连接
	SOCK_RDM	= 4,
    //没有找到资料
	SOCK_DCCP	= 6,
    //与网络驱动程序直接通信
	SOCK_PACKET	= 10,
};


//Linux创建一个socket方法
#include 
//socket创建函数
int socket (int domain, int type, int protocol);
//例如socket初始化为一个tcp/ip的链接
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

三、TCP/IP 与UDP区别

Tcp和Udp的区别有:1、udp是无连接的,tcp是面向连接的;2、udp是不可靠传输,tcp是可靠传输;3、udp是面向报文传输,tcp是面向字节流传输。

它有以下几个特点:

1、面向无连接

首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2、有单播,多播,广播的功能

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

3、UDP是面向报文的

发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文

4、不可靠性

首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

5、头部开销小,传输数据报文时是很高效的。

四、TCP/IP协议详解

  • 名词解释

        TCP/IP相关名词解释

             报文状态: SYN(Synchronize Sequence Number)同步序列号表示建立连接,ACK(Acknowledgement)即确认字符表示响应,FIN(Finish)表示关闭连接,PSH(Push)表示DATA数据传输
RST(Reset)表示重置。

             报文生命周期:MSL(最大分段生存期)指明TCP报文在Internet上最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值,RFC1122建议是2分钟。    TTL是IP头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机,RFC793中规定MSL为2分钟,实际应用中常用的是30秒1分钟和2分钟等。    RTT是客户到服务器往返所花时间(round-trip time,简称RTT),TCP含有动态估算RTT的算法,TCP还持续估算一个给定连接的RTT,这是因为RTT受网络传输拥塞程序的变化而变化。               

  • TCP/IP通道特征

       TCP/IP是全双工双通道协议,既可以异步或者同时收、发、关闭,是一个双向认证的过程,数据必须无差错、不丢失、不重复并且按序到达,下面TCP/IP状态4次挥手FIN_WAIT_2、LAST_ACK、  LAST_ACK就是体现双工通道客户端和服务端请求不会被相互阻塞的特征。

  • TCP/IP状态汇总
tcp/ip状态汇总
阶段 TCP/IP状态 TCP/IP状态含义
三次握手 LISTEN 服务器端的某个SOCKET处于监听状态,可以接受客户端的连接
SYN_SENT 第一次握手 客户端调用Connect,发送 SYN报文给服务器端。若服务端不能连接,则直接进入初始的CLOSED状态
SYN_RCVD 第二次握手 服务端接收到客户端的SYN报文,服务端由 LISTEN 进入 SYN_RCVD状态,同时回应一个SYN+ACK报文给客户端
ESTABLISHED

第三次握手 客户端接收到服务端的SYN+ACK报文后,回应一个 ACK报文后,客户端进入 ESTABLISHED 状态,表明客户端已经准备好,服务端收到客户端的 ACK报文之后会从 SYN_RCVD 状态转移到 ESTABLISHED 状态,表明服务端已经准备好,此时服务端可以和客户端通讯

四次挥手 FIN_WAIT_1 第一次挥手 主动关闭方(客户端或服务端)终止连接时,调用 close()发送FIN报文给对方,等待对方返回ACK
CLOSE_WAIT

接收到FIN报文后,被动关闭的一方进入此状态,接收到FIN,同时发送 ACK

​TCP关闭是全双工过程,这里主动关闭方执行了关闭,被动方也需要调用close() 关闭,CLOSE_WAIT就是处于这个状态,等待发送 FIN,发送了FIN被动方进入LAST_ACK状态

FIN_WAIT_2 主动关闭方先发送FIN,然后接收到被动方返回的 ACK 后进入此状态
LAST_ACK 被动方发起FIN,进入此状态。当接收到主动关闭方回应的ACK 时进入CLOSED状态
CLOSING 两边同时发起关闭请求时,会由FIN_WAIT_1 进入此状态,等待返回ACK,上面的挥手其状态在接受到ACK前也是CLOSING
TIME_WAIT

共有三个状态会进入该状态

1.由CLOSING进入:同时发起关闭情况下,当主动端接收到ACK后,进入此状态,实际上这里的同时是这样的情况:客户端发起关闭请求,发送FIN之后等待服务器端回应ACK,但此时服务器端同时也发起关闭请求,也发送了FIN,并且客户端先于服务端接收到ACK
2.由FIN_WAIT_1进入:发起关闭后,发送了FIN,等待ACK的时候,正好被对方(服务器端)也发起关闭请求,发送了FIN,这时客户端接收到了先前ACK,也收到了对方的FIN,然后发送ACK(对对方FIN的回应),与CLOSING进入的状态不同的是接收到FIN和ACK的先后顺序
3.由FIN_WAIT_2进入:这是不同时的情况,主动方在完成自身发起的主动关闭请求后,接收到了对方发送过来的FIN,然后回应ACK。此处参考图5

5.网络通讯-TCP/IP协议深入浅出分析及疑难杂症_第2张图片 4.tcp/ip状态扭转线性示意图,由于tcp/ip是全双工不可能按照这样的顺序

5.网络通讯-TCP/IP协议深入浅出分析及疑难杂症_第3张图片 5.tcp/ip状态机

五、TCP/IP粘包半包原因分析

为什么UDP没有粘包?

粘包拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,UDP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区,因此粘包拆包问题只发生在TCP协议中。

粘包拆包发生场景

因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包

六、TCP/IP协议设计原因

  • TCP/IP为什么要三次握手四次挥手?

       三次握手的原因:

           通讯初始化序号原因: 三次握手设计是为了应对网络通讯需要保证数据可靠传输,又要提高传输的效率,而3次握手最低的限度满足网络通讯的需求。回答这个问题,我先抛出一个问题,tcp/ip协议如何验证彼此消息的合法性?答案是通讯的初始化序号,什么是通讯初始化序号?【RFC793( 传输控制协议)介绍,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。我们假设我们的TCP Segment在网络上的存活时间不会超过MSL(Maximum Segment Lifetime ),只要MSL的值小于4.55小时,我们就不会重用到ISN。】三次握手的过程其实就是同步客户端和服务端的SIN序号,如果是少于3次握手客户端和服务端是不知道彼此是否已经收到对方的信号。

           半连接队列原因:在tcp/ip协议中握手次数少于3次的连接称为半连接队列,握手次数达到3次的链接称为全连接队列此时连接是可以正常通讯的。服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除,每次重传等待的时间不一定相同。半连接队列作用是和全连接解耦保证通讯的安全,但是半连接也有自己的问题,下面讲针对半连接的攻击。

       四次挥手原因:

           四次挥手原因:在主动断连一方发送FIN报文后,接收方受到后并不会立即发送FIN + ACK而只能先发送ACK,这时因为被动方还需要等待应用层处理完毕,才会发送FIN,正是由于被动方FIN和ACK是分开的,且连接是全双工的,需要连接两段发别发送FIN且接收到ACK,所以这才产生了4次挥手。

           四次挥手是否能改成3次挥手? 答案是可以的。主动方发送FIN报文后,接收方接收到后发送ACK+FIN报文合并成一个包发送,这样4此挥手就变成3次挥手。

TIME-WAIT必须等待2ML原因:

​​​​​​​TIME-WAIT必须等待2MLS是因为等待2MLS可以保证客户端最后一个报文段能够到达服务器,如果未到达,服务器则会超时重传连接释放报文段,使得客户端、服务器都可以正常进入到CLOSE关闭状态。

七、TCP/IP常见问题       

  • 问题一、SYN超时与攻击与检查预防怎么办?

  • SYN超时及预防:

       Client发送SYN包给 Server后立刻挂了,Server回给Client的SYN-ACK一直没收到Client 的 ACK 确认,这个时候这个连接既没建立起来,也不能算失败。这就需要一个超时时间让 Server 将这个连接断开,否则这个连接就会一直占用 Server 的 SYN 连接队列中的一个位置,大量这样的连接就会将 Server 的 SYN 连接队列耗尽,让正常的连接无法得到处理。目前,Linux 下默认会进行 5 次重发 SYN-ACK 包,重试的间隔时间从 1s 开始,下次的重试间隔时间是前一次的双倍,5 次的重试时间间隔为 1s,2s, 4s, 8s,16s,总共 31s,第 5 次发出后还要等 32s 都知道第 5 次也超时了,所以,总共需要 1s + 2s +4s+ 8s+ 16s + 32s =63s,TCP 才会把断开这个连接。

        由于SYN 超时需要 63 秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的 SYN 包给 Server(俗称 SYN flood 攻击),用于耗尽 Server 的 SYN 队列。对于应对 SYN 过多的问题,linux 提供了几个 TCP 参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。

        tcp_syncookies作用:YN Cookie是对TCP服务器端的三次握手做一些修改,专门用来防范SYN Flood的一种手段。它的原理是,在TCP服务器接收到TCP SYN包并返回TCP SYN + ACK包时,不分配一个专门的数据区,而是根据这个SYN包计算出一个cookie值。这个cookie作为将要返回的SYN ACK包的初始序列号。当客户端返回一个ACK包时,根据包头信息计算cookie,与返回的确认序列号(初始序列号 + 1)进行对比,如果相同,则是一个正常连接,分配资源建立连接。

       tcp_synack_retries作用:对于远端SYN连接请求,内核会发送SYN+ACK数据包来确认收到了上一个SYN连接请求包,然后等待远端的确认(ack数据包)。该值则指定了内核会向远端发送tcp_synack_retires次SYN+ACK数据包。默认设定值是5,可以调整为1。

       tcp_max_syn_backlog作用:指定所能接受SYN同步包的最大客户端数量,即半连接上限,默认值是128,即SYN_REVD状态的连接数。如果值很小会出现丢包值很大消耗系统资源。

       tcp_abort_on_overflow作用:0表示三次握手第三步的时候全连接队列满了那么server扔掉client 发过来的ack,1表示第三步的时候如果全连接队列满了,server发送一个reset包给client。

       tcp_fastopen作用:通过握手开始时的SYN包中的TFO cookie来验证一个之前连接过的客户端。如果验证成功,它可以在三次握手最终的ACK包收到之前就开始发送数据,这样便跳过了一个绕路的行为,更在传输开始时就降低了延迟。

#tcp_syncookies 1表示在新连接压力比较大时启用SYN Cookies, 2表示始终使用SYN Cookies
[admin@vm-xaj-elasticsearch-t01 ~]$ cat /proc/sys/net/ipv4/tcp_syncookies
1


#tcp_synack_retries、net.ipv4.tcp_max_syn_backlog
[admin@vm-xaj-elasticsearch-t01 ~]$ cat /etc/sysctl.conf
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_max_syn_backlog = 262144


#tcp_abort_on_overflow
[admin@vm-xaj-elasticsearch-t01 ~]$ cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0
  • SYN指标检查:   

       全队列和半队列溢出检查:24456表示全连接溢出次数为24456次,24459表示半连接溢出24459次

[logview@vm-xy-kafka-p04 ~]$ netstat -s | egrep "listen|LISTEN"
    // 全连接队列溢出次数
    24456 times the listen queue of a socket overflowed
    // 半连接队列溢出次数
    24459 SYNs to LISTEN sockets ignored

       待接收包与待发送包统计:Recv-Q网络接收队列表示收到的数据已经在本地接收缓冲,但是还有多少没有被进程取走,如果接收队列Recv-Q一直处于阻塞状态,可能是遭受了拒绝服务攻击。表示网路发送队列对方没有收到的数据或者说没有Ack的,还是本地缓冲区,如果发送队列Send-Q不能很快的清零,可能是有应用向外发送数据包过快,或者是对方接收数据包不够快。

[logview@vm-xy-kafka-p02 ~]$ ss -lnt
State       Recv-Q Send-Q                                      Local Address:Port                                        Peer Address:Port                                                 *:*
LISTEN      0      100                                             127.0.0.1:25                                                     *:*
LISTEN      0      100                                                     *:19100                                                  *:*
LISTEN      0      128                                                     *:10050                                                  *:*
LISTEN      0      65535                                                   *:1988                                                                                                    *:*
LISTEN      0      65535                                                   *:9100                                                   *:*
  • 问题二、为什么需要TIME_WAIT状态是否可以去掉

  • TIME_WAIT是用来解决什么问题?

       解决ACK包延时到达:主动关闭方需要进入TIME_WAIT以便能够重发丢掉的被动关闭方 FIN 包的 ACK。如果主动关闭方不进入 TIME_WAIT,那么在主动关闭方对被动关闭方 FIN 包的 ACK 丢失了的时候,被动关闭方由于没收到自己 FIN 的 ACK,会进行重传 FIN 包,这个 FIN 包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个 FIN 包,协议栈会认为对方疯了,都还没建立连接你给我来个 FIN 包?,于是回复一个 RST 包给被动关闭方,被动关闭方就会收到一个错误(我们见的比较多的:connect reset by peer,这里顺便说下 Broken pipe,在收到 RST 包的时候,还往这个连接写数据,就会收到 Broken pipe 错误了),原本应该正常关闭的连接,给提示错误。     

       防止包污染:现实中有的路由器会缓存包,路由器每一跳ttl会减一,当ttl值小于0,包被丢弃。比如:已经断开的连接A中在链路中残留的FIN包终止掉新的连接 B(重用了连接 A的所有的 5 元素【源 IP、目的 IP、TCP、源端口、目的端口】),这个概率比较低,因为涉及到一个匹配问题,迟到的 FIN 分段的序列号必须落在连接B 的一方的期望序列号范围之内,虽然概率低,但是确实可能发生,因为初始序列号都是随机产生的,并且这个序列号是 0-2^3{^2{^{}}}回绕,造成当前通讯干扰。

  • TIME_WAIT会带来什么问题?

       TIME_WAIT是指一连个接进入TIME_WAIT会等待2*MSL周期才会释放所占用的资源,这个周期大约为4分钟,如果大规模的链接进入TIME_WAIT,对服务端的影响导致大量的select阻塞,对客户端的影响占用客户端大量的端口导致端口资源被耗尽。(Linux端口数最大数:65535)

  • 如何解决TIME_WAIT带来的问题

       TIME_WAIT快速回收:

#Linux 涉及TIME_WAIT参数之连接快速回收
net.ipv4.tcp_tw_recycle = 0  表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_timestamps = 0  tw_reuse,tw_recycle 必须在客户端和服务端 timestamps 开启时才管用(默认打开)

#如何查看修改上面的参数
[admin@vm-lw-vmc-admin-t04 ~]$ cat /proc/sys/net/ipv4/tcp_timestamps
0
[admin@vm-lw-vmc-admin-t05 ~]$  cat /proc/sys/net/ipv4/tcp_tw_recycle
1

       tcp_tw_recycle、tcp_timestamps参数开启可以达到快速回收TIME-WAIT连接的作用,但是在NAT(Network Address Translation 网络地址转换协议,大量局域网连接公网)网络下 ,会导致大量的TCP连接建立错误。所以开启此参数前要了解自己的网络架构环境,要谨慎做大量的测试。

       TIME_WAIT重用:

#Linux 涉及TIME_WAIT参数之连接重用
net.ipv4.tcp_tw_reuse = 0    表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_timestamps = 0  tw_reuse,tw_recycle 必须在客户端和服务端 timestamps 开启时才管用(默认打开)


#如何查看修改上面的参数
[admin@vm-lw-vmc-admin-t04 ~]$ cat /proc/sys/net/ipv4/tcp_timestamps
0
[admin@vm-lw-vmc-admin-t05 ~]$  cat /proc/sys/net/ipv4/tcp_tw_recycle
1

     tcp_tw_reuse、tcp_timestamps参数开启可以达到重用tcp连接的作用。开启这个参数可能会导致不等待超时重用连接的话,新的连接可能会建不上。正如官方文档上说的一样“It should not be changed without advice/request of technical experts”),使用此参数要设置一个http的keep-alive保证在这个时间段连接不断开,让网关重用连接,也防止网关长期不断开连接。 

     HTTP的keep-alive和tcp的KeepAlive不是一回事。HTTP的keey-alive是为了复用tcp连接,而tcp的KeepAlive是tcp用于检测连接两端的心跳,Linux默认是2h,也就是7200s,当TCP连接两段长时间没有数据传输,此时存活一方就会发送keepalive探针,探测连接是否存活,避免一方未发送FIN就断连导致连接一直被占用的情况出现。

#我的tcp keepalive_time
[admin@vm-lw-vmc-admin-t04 ~]$ cat /proc/sys/net/ipv4/tcp_keepalive_time
30

     总结:这两类操作都不是tcp/ip协议规定的,可能会导致各种奇怪的问题,time_wait笔者认为只是IO通讯一条请求记录,会占用少量的内存,现在计算机资源丰富的今天也没有必要为节省内存导致系统不稳定,除非在特定的场景下使用,笔者认为在内网环境下可以使用,但是内网环境也有很优秀的其他策略可以使用。 

  • 如何清理掉TIME_WAIT       

       tcp_max_tw_buckets参数设置:tcp_max_tw_buckets默认值为18000,系统同时处理TIME_WAIT sockets数目。如果一旦TIME_WAIT tcp连接数超过了这个数目,系统会强制清除并且显示警告消息。设立该限制,主要是防止那些简单的DoS攻击,加大该值有可能消耗更多的内存资源。

[admin@vm-lw-vmc-admin-t04 ~]$ cat /proc/sys/net/ipv4/tcp_max_tw_buckets
10000

       模拟RST包:根据tcp/ip规范收到任何的发送到未侦听端口、已经关闭的连接的数据包、连接处于任何非同步状态(LISTEN,SYS-SENT,SYN-RECEIVED)并且收到的包的 ACK 在窗口外,或者安全层不匹配,都要回执以 RST 响应,当RST响应后服务器就会回收这条连接,客户端出现connect reset by peer错误。RST包对tcp/ip有效对udp无效。这也是传说中的RST攻击,RST攻击条件需要知道对方的IP+端口模拟发送RST包。解决办法也很简单,遇到异常的情况下防火墙直接拒绝RST包即可。

  • 问题三、TCP/IP延迟确认机制及算法

       TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。Linux 实现中,有延迟 ACK 和快速 ACK,并根据当前的包的收发情况来在这两种 ACK 中切换。一般情况下,ACK 并不会对网络性能有太大的影响,延迟 ACK 能减少发送的分段从而节省了带宽。快速 ACK 能及时通知发送方丢包,避免滑动窗口停等提升吞吐率。

Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释):

1、如果包长度达到MSS,则允许发送;
2、如果该包含有FIN,则允许发送;
3、设置了TCP_NODELAY选项,则允许发送;
4、未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
5、上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
  • 问题四、TCP/IP重传和超时

  • TCP/IP超时时间确认

       TCP/IP在通讯交互中如果没有收到ACK需要一直等待下去吗?肯定不能一直等待下去如果这样做了,ACK丢包的话肯定永远也不会通知服务端。既然不能一直等待下去,设定一个固定的等待时间是否可以?如果等待时间设置太短的话导致ACK还在路途中,已经重发新的ACK浪费网络资源,如果等待时间太久才重发浪费通讯效率。网络情况是千变万化的,有没有一种算法自动算出等待时间RTO的时长,RFC793经典等待时长算法 和 Karn/Partridge算法就是动态计算等待时长的算法。

     RFC793经典等待时长算法:

1、先采样RTT,记下最近好几次的RTT值。RTT——Round Trip Time,定义为数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间。

2、然后做平滑计算SRTT( Smoothed RTT),公式为:(其中的 α 取值在0.8 到 0.9之间,这个算法英文叫Exponential weighted moving average,中文叫:加权移动平均)
SRTT = ( α * SRTT ) + ((1- α) * RTT)

3、开始计算RTO,公式如下:
RTO = min[UBOUND, max[LBOUND, (β * SRTT)]]

备注:
UBOUND是最大的timeout时间上限值,LBOUND是最小的timeout时间下限值,β 值一般在1.3到2.0之间,这个计算公式就是将 (SRTT)·β 的值作为RTO,只不过另外限制了RTO的上下限

公式解释:这是一种平滑算法,就是求最大值中的最小值。步骤:先求max [ LBOUND, (β * SRTT) ]总的最大值然后记为Y,后求最小min(UBOUND,Y)。

     Karn/Partridge算法:

     大家有没有发现RFC793算法在下面两种常见下有缺陷,如果是图(a) ack没回来导致重传,计算第一次发送和ACK的时间,那么明显算大了。如果是图(b) ack回来慢了,导致了重传,但刚重传不一会儿,之前ACK就回来了。如果你是算重传的时间和ACK回来的时间的差就会算短了。

     1987年的时候,搞了一个叫Karn Partridge Algorithm用于解决上面这个问题,这个算法的最大特点是——忽略重传,不把重传的RTT做采样。这样一来,又会引发一个大BUG,如果在某一时间,网络闪动突然变慢了,产生了比较大的延时,这个延时导致要重转所有的包(因为之前的RTO很小),于是,因为重转的不算,所以,RTO就不会被更新,这是一个灾难。 于是Karn算法用了一个取巧的方式——只要一发生重传,就对现有的RTO值翻倍(这就是所谓的 Exponential backoff),很明显,这种死规矩对于一个需要估计比较准确的RTT也不靠谱。Karn/Partridege算法解决了由于重传导致的RTT计算不准的问题,但是应对突发的网络抖动翻倍RTO这种做法不太成熟。

5.网络通讯-TCP/IP协议深入浅出分析及疑难杂症_第4张图片 RFC793经典等待时长算法问题示意图

   

     Jacobson/Karels 算法:

     前面两种算法用的都是“加权移动平均”,这种方法最大的毛病就是如果RTT有一个大的波动的话,很难被发现,因为被平滑掉了。所以1988年又有人推出来了一个新的算法,这个算法叫Jacobson / Karels Algorithm(参看RFC6289)。这个算法引入了最新的RTT的采样和平滑过的SRTT的差距做因子来计算。  

#此公式目前没有看懂
1、SRTT = SRTT + α (RTT – SRTT) -- 计算平滑RTT
2、DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) -- 计算平滑RTT和真实的差距(加权移动平均)
3、RTO= µ * SRTT + ∂ *DevRTT -- 神一样的公式

备注:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 -- 这就是算法中的“调得一手好参数”,nobody knows why, it just works。
最后的这个算法在被用在今天的TCP协议中,其中的DevRTT是Deviation RTT的意思
  • TCP/IP重传机制

             超时重传机制

             TCP要保证所有的数据包都可达,当检测到包丢失时必需要有重传机制。             

             超时重传机制

             发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,ack不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。可行的一种方式是不回ack死等3,当发送方发现收不到3的ack超时后重传3。一旦接收方收到3后,会ack回4就意味着3和4都收到了。一种方式:是仅重传timeout的包也就是第3份数据。另一种方式:是重传timeout后所有的数据,也就是第3,4,5这三份数据。

             快速重传机制

             针对超时重传需要等待时间可能稍长,TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而根据收到的TCP报文决定是否重传。如果发送方连续收到3次相同的ack序号,则认为需要重传那个序号的分组。   例如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2。后面的4和5都到了,但是还是ack回2,因为2还是没有收到。于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了于是ack回6

         

  • 问题五、TCP/IP流量控制

       TCP通过一个定时采样了RTT并计算RTO,如果网络上的延时突然增加,TCP对这个事做出的应对只有重传数据,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“网络风暴”,TCP这个协议就会拖垮整个网络,网络通讯将会崩溃。

       TCP设计不能忽略当前网络情况,一个劲地重发数据将会对网络造成更大的伤害。TCP的设计理念:TCP不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。TCP的设计用于防止网络因为大规模的通信负载而瘫痪。TCP拥塞控制基本原理可以比喻为给网络传输踩刹车,在网络即将进入或已经进入拥塞状态时,减缓TCP传输。TCP拥塞控制的难点在于准确的判断何时需要减缓、以及何时需要恢复其原有速度。目前TCP/IP拥有以下特征 慢启动、拥塞避免、拥塞发生、快速恢复,这几个特征都有自己的算法本文就不在赘述,因为这些算法我没有办法去验证。

      拥塞窗口介绍:确保发送窗口大小不超过接收端接收能力和网络传输能力,即TCP发送端的发送速率等于接受速率和传输速率的较小者,拥塞窗口公式:W = min(cwnd, awnd)。 如何计算拥塞窗口

      拥塞窗口计算过程:

八、TCP/IP参数设置

net.ipv4.tcp_max_tw_buckets = 10000
#表示系统同时保持TIME_WAIT套接字的最大数量

net.ipv4.tcp_timestamps = 0
#关闭TCP时间戳
#以一种比重发超时更精确的方法(请参阅 RFC 1323)来启用对 RTT 的计算;为了实现更好的性能应该启用这个选项

net.ipv4.tcp_tw_recycle = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

net.ipv4.tcp_tw_reuse = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_retries2 = 1
#活动TCP连接重传次数,超过次数视为掉线,放弃连接。缺省值:15,建议设为 2或者3.
    
net.ipv4.tcp_fin_timeout = 1
#FIN_WAIT状态的TCP连接的超时时间

推荐文档

  • TCP/IP抓包工具下载
  • 深入浅出TCP中的SYN-Cookies
  • TCP/IP连接参数控制
  • Linux网络状态工具ss命令使用详解
  • TCP 三次握手原理,你真的理解吗?
  • 性能分析之TCP全连接队列占满问题分析及优化过程
  • 开启tcp_tw_recycle参数引发的系统问题
  • TCP参数设置
  • TCP Nagle算法&&延迟确认机制
  • TCP/IP详解--滑动窗口、拥塞窗口、慢启动、Negle算法

你可能感兴趣的:(#,网络通讯,网络)