Linux-TCP

Linux-TCP_第1张图片
image.png

1. TIME_WAIT

1.1 TIME_WAIT状态如何产生?

由上面的变迁图,首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP连接在2MSL连接等待期间,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。

1.2 TIME_WAIT状态产生的原因

  1. 为实现TCP全双工连接的可靠释放
    由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,该TCP连接才能恢复初始的CLOSED状态。如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭连接过程,并非异常。

  2. 为使旧的数据包在网络因过期而消失
    为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。

  3. 总结
    具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。

1.3 TIME_WAIT状态如何避免

首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。

2. TCP参数

2.1 TCP_NODELAYED

默认情况下, TCP发送数据采用Nagle算法.。Nagle算法是解决小数据的频繁发送问题,比如1个字节的数据,在封包后会加上几十字节的首部,相当浪费资源。Nagle的做法是发送方发送的数据不会立即发出,而是先放在缓冲区,等待缓冲区达到一定的大小,或者是缓冲达到一定的时间后再一批发出。 发送完一批数据后, 会等待接收方对这批数据的回应,然后再发送下一批数据.。Negle算法适用于发送方需要发送大批量数据, 并且接收方会及时作出回应的场合, 这种算法通过减少传输数据的次数来提高通信效率。

如果发送方持续地发送小批量的数据, 并且接收方不一定会立即发送响应数据, 那么Negle算法会使发送方运行很慢。对于GUI 程序, 如网络游戏程序(服务器需要实时跟踪客户端鼠标的移动), 这个问题尤其突出.。客户端鼠标位置改动的信息需要实时发送到服务器上,由于Negle算法采用缓冲, 大大减低了实时响应速度, 导致客户程序运行很慢。

我们可以通过设置TCP_NODELAYED来禁用Negle算法。

2.2 so_keepalive

so_keepalive是TCP的心跳机制,保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。设置该选项后,如果2小时内在此套接口的任一方向都没有数据交换,TCP就自动给对方 发一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节。它会导致以下三种情况:

  1. 对方接收一切正常:以期望的ACK响应,2小时后,TCP将发出另一个探测分节。
  2. 对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET,套接 口本身则被关闭。
  3. 对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到一个响应。在发出第一个探测分节11分钟15秒后若仍无响应就放弃。套接口的待处理错误被置为ETIMEOUT,套接口本身则被关闭。如ICMP错误是“host unreachable(主机不可达)”,说明对方主机并没有崩溃,但是不可达,这种情况下待处理错误被置为 EHOSTUNREACH。

SO_KEEPALIVE有三个参数,其详细解释如下:

  1. tcp_keepalive_intvl,保活探测消息的发送频率。默认值为75s。发送频率tcp_keepalive_intvl乘以发送次数tcp_keepalive_probes,就得到了从开始探测直到放弃探测确定连接断开的时间,大约为11min。

  2. tcp_keepalive_probes,TCP发送保活探测消息以确定连接是否已断开的次数。默认值为9(次)。值得注意的是,只有设置了SO_KEEPALIVE套接口选项后才会发送保活探测消息。

  3. tcp_keepalive_time,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测消息的时间,即允许的持续空闲时间。默认值为7200s(2h)。

2.3 backlog

更详细的信息参见:https://www.jianshu.com/p/a81ec7a47959
对于TCP连接,内核维护两个队列:
1. 未完成连接的队列,此队列维护着那些已收到了客户端SYN分节信息,等待完成三路握手的连接,socket的状态是SYN_RCVD。
2. 已完成的连接的队列,此队列包含了那些已经完成三路握手的连接,socket的状态是ESTABLISHED,但是等待accept。

2.3.1 backlog

也可
backlog在linux2.2之后表示队列2(已经完成连接,等待accept调用)。

backlog的值太小会导致在大量连接的时候不能处理,丢弃客户端发送的ack,此时如果客户端认为连接建立继续发送数据,就会出现满请求。backlog过大会导致连接积压,性能下降。

调用listen监听的时候可以设置backlog的值,然backlog 并不是按照你调用listen的所设置的backlog大小,实际上取的是backlog和somaxconn的最小值。somaxconn的值定义在/proc/sys/net/core/somaxconn,默认是128,可以把这个值修改更大以满足高负载需求。

2.3.2 syn_backlog

syn_backlog指队列1(半连接SYN_RCVD阶段)。

这个值在/proc/sys/net/ipv4/tcp_max_syn_backlog ,可以对其进行调整。但是一般情况下处于syn_rcvd阶段的不会太多,除非遇到SYN_FLOOD攻击。

SYN FLOOD:

SYN Flood利用的是TCP协议缺陷,发送大量伪造的TCP连接请求,从而使得被攻击方资源耗尽(CPU满负荷或内存不足)的攻击方式。在被攻击主机用netstat可以看见80端口存在大量的半连接状态(SYN_RECV),用tcpdump抓包可以看见大量伪造IP发来的SYN连接,S也不断回复SYN+ACK给对方,可惜对方并不存在(如果存在则S会收到RST这样就失去效果了),所以会超时重传。这个时候如果有正常客户A请求S的80端口,它的SYN包就被S丢弃了,因为半连接队列已经满了,达到攻击目的。

2.4 MSS

MTU:数据链路层所能传输数据(扣除了数据链路层的协议头等信息)大小。例如,以太网的MTU是1500。
MSS:TCP 为每个 TCP 连接控制这个最大大小,称为最大段大小 (MSS)。对于直接连接的网络,TCP 计算 MSS 的方法是使用网络接口的 MTU 大小然后减去协议头得到 TCP 信息包中数据的大小。例如,MTU 为 1500 的以太网在减去 20 字节的 IPv4 头和 20 字节的 TCP 头后得到的MSS 为 1460

2.4.1 MSS的协商

2.4.1.1建立连接时协商

TCP在三次握手建立连接过程中,会在SYN报文中使用MSS(Maximum Segment Size)选项功能,协商交互双方能够接收的最大段长MSS值。
MSS是传输层TCP协议范畴内的概念,顾名思义,其标识TCP能够承载的最大的应用数据段长度,因此,MSS=MTU-20字节TCP报头-20字节IP报头,那么在以太网环境下,MSS值一般就是1500-20-20=1460字节。
客户端与服务器端分别根据自己发包接口的MTU值计算出相应MSS值,并通过SYN报文告知对方,我们还是通过一个实际环境中捕获的数据报文来看一下MSS协商的过程:

Linux-TCP_第2张图片
点击查看原图
这是整个报文交互过程的截图,我们再来看一下客户端的报文详细解码:
Linux-TCP_第3张图片
点击查看原图

上图为客户端的SYN报文,在其TCP选项字段,我们可以看到其通告的MSS值为1460;我们在看看服务器端的SYN/ACK报文解码:
Linux-TCP_第4张图片
点击查看原图

上图为服务器端给客户端回应的SYN/ACK报文,查看其TCP选项字段,我们可以发现其通告的MSS值为1440。
交互双方会以双方通告的MSS值中取最小值作为发送报文的最大段长。在此TCP连接后续的交互过程中,我们可以清楚的看到服务器端向客户端发送的报文中,TCP的最大段长度都是1440字节,如下图解码所示:

Linux-TCP_第5张图片
点击查看原图

**通过在TCP连接之初,协商MSS值巧妙的解决了避免端系统分片的问题,但是在复杂的实际网络环境下,影响到IP报文分片的并不仅仅是发送方和接收方,还有路由器、防火墙等中间系统,假设在下图的网络环境下: **
Linux-TCP_第6张图片
点击查看原图

中间路径上的MTU问题,端系统并不知道,因此需要一个告知的机制,这种机制一共有两种:

  • 在建立连接时中间路由器也参与协商,但是该协商过程只在建立连接时进行。
  • 路径MTU发现(PMTUD: Path MTU Discovery ),该机制利用了IP
    报文禁止分段和ICMP报文实现了在建立连接之后动态的调整MTU。缺点如下:
    1. 因为一些网络会封掉ICMP,所以该机制也有其缺点。如果ICMP被封掉,IP报文又禁止分段,就会导致故障。
    2. 值得一提的是PMTUD仅TCP支持,UDP并不支持PMTUD。
2.4.1.1.1 建立连接时中间路由器也参与协商的过程

由于PMTUD可能存在ICMP差错报文被过滤的情况,很多中间设备的接口支持adjust tcp mss设置功能,思科路由器一般是在接口模式下使用命令“ip tcp adjust-mss 1400 ”来做设置,其他的品牌产品的相关设置大家可在实际工作环境下自查相关品牌和产品的使用手册。
这个功能主要是通过由中间设备修改经过其转发的TCP SYN报文中的MSS值,让中间设备参与进TCP 三次握手时SYN报文的MSS协商来避免分片。
需要注意的是,该功能不像MTU值,只针对出接口,此功能一旦开启,其将针对该接口的收发双向有效。
我做一个简化环境下的工作过程图示以便于大家理解其工作过程:

Linux-TCP_第7张图片
点击查看原图

2.4.1.2 路径MTU发现

  1. DF标志位的作用
    一旦DF位置一,将不允许中间设备对该报文进行分片,那么在遇到IP报文长度超过中间设备转发接口的MTU值时,该IP报文将会被中间设备丢弃。在丢弃之后,中间设备会向发送方发送ICMP差错报文。
    为了简单直观的展示这个交互的过程,我做了下面这个图示:

    Linux-TCP_第8张图片
    点击查看原图

    我找了一个实际环境下捕获的ICMP需要分片但DF位置一的差错报文,下图为其解码格式:
    Linux-TCP_第9张图片
    点击查看原图

    我们可以看到其差错类型为3,代码为4,并且告知了下一跳的MTU值为1478。在ICMP差错报文里封装导致此差错的原始IP报文的报头(包含IP报头和四层报头)。
    一旦出现这种因DF位置一而引起丢包,如果客户端无法正常处理的话,将会导致业务应用出现异常,外在表现为页面无法打开、页面打开不全、某些大文件无法传输等等,这将严重影响业务的正常运行。
    实际上通过该方式可以动态的通知和协商MTU,这也是路径MTU发现的基本原理。

  2. MTU具体过程
    TCP的路径MTU发现按如下方式进行:在连接建立时, TCP使用输出接口或对端声明的MSS中的最小M T U作为起始的报文段大小。路径M T U发现不允许T C P超过对端声明的M S S。如果对端没有指定一个M S S,则默认为5 3 6。
    一旦选定了起始的报文段大小,在该连接上的所有被T C P发送的I P数据报都将被设置D F比特。如果某个中间路由器需要对一个设置了D F标志的数据报进行分片,它就丢弃这个数据报,并产生一个I C M P的“不能分片”差错。如果收到这个I C M P差错, T C P就减少段大小并进行重传。如果路由器产生的是一个较新的该类I C M P差错,则报文段大小被设置为下一跳的M T U减去I P和T C P的首部长度。如果是一个较旧的该类I C M P差错,则必须尝试下一个可能的最小M T U(见图2 - 5)。当由这个I C M P差错引起的重传发生时,拥塞窗口不需要变化,但要启动慢启动。由于路由可以动态变化,因此在最后一次减少路径M T U的一段时间以后,可以尝试使用一个较大的值(直到等于对端声明的M S S或输出接口M T U的最小值)。RFC 11 9 1推荐这个时间间隔为1 0分钟(Solaris 2.2使用一个3 0分钟的时间间隔)。在对非本地目的地,默认的M S S通常为5 3 6字节,路径M T U发现可以避免在通过M T U小于5 7 6(这非常罕见)的中间链路时进行分片。对于本地目的主机,也可以避免在中间链路(如以太网)的M T U小于端点网络(如令牌环网)的情况下进行分片。但为了能使路径M T U更加有用和充分利用M T U大于5 7 6的广域网,一个实现必须停止使用为非本地目的制定的5 3 6的M T U默认值。M S S的一个较好的选择是输出接口的M T U(当然要减去I P和T C P的首部大小)
    (大多数的实现都允许系统管理员改变这个默认的M S S值,一些操作系统针对非本地目的地址使用默认值)。
    TCP/IP协议卷1中:涉及B S D / 3 8 6和S V R 4的M S S为1 0 2 4,这是因为许多B S D的实现版本需要M S S为5 1 2的倍数。其他的系统,如SunOS 4.1.3、Solaris 2.2 和AIX 3.2.2,当双方都在一个本地以太网上时都规定M S S为1 4 6 0。[Mogul 1993] 的比较显示了在以太网上1 4 6 0的M S S在性能上比1 0 2 4的M S S更好。如果目的I P地址为“非本地的( n o n l o c a l )”,M S S通常的默认值为5 3 6。而区分地址是本地还是非本地是简单的,如果目的I P地址的网络号与子网号都和我们的相同,则是本地的;如果目的I P地址的网络号与我们的完全不同,则是非本地的;如果目的I P地址的网络号与我们的相同而子网号与我们的不同,则可能是本地的,也可能是非本地的。大多数T C P实现版都提供了一个配置选项(附录E和图E - 1),让系统管理员说明不同的子网是属于本地还是非本地。这个选项的设置将确定M S S可以选择尽可能的大(达到外出接口的M T U长度)或是默认值5 3 6。
    现在TCP实现上针对非本地目的地址不使用默认MTU 576,使用输出接口的MTU)
    支持路径发现机制的主机,发送的TCP报文都指定DF位置1.一旦防火墙过滤ICMP报文,路径MTU发现机制就废了。

    Linux-TCP_第10张图片
    image.png

2.5 RST

四次握手不是关闭 TCP连接的唯一方法. 有时,如果主机需要尽快关闭连接(或连接超时,端口或主机不可达),RST (Reset)包将被发送. 注意在,由于RST包不是TCP连接中的必须部分, 可以只发送RST包(即不带ACK标记). 但在正常的TCP连接中RST包可以带ACK确认标记。
如果收到RST信号,说明连接已经出现问题,此时不能走正常的四次握手关闭连接。
下面以客户端收到服务端的RST信号举例:
客户端收到RST信号,说明连接需要异常关闭,此时客户端再向socket写或读事件时会触发异常。
对于Netty,收到RST,向该socket写时会触发异常。同时RST信号到达会触发OP_READ,HeadHandler在处理OP_READ时异常,然后fireExceptionCaught事件。

2.6 PUSH

和NODELAY是相近的

你可能感兴趣的:(Linux-TCP)