老铁意向+ 很多tcp的总结

刚拿到了铁手意向书了,效率真快,一周全完事,hr面后一天就给意向了。
秋招以来好多TCP 的问题都不会,之后复盘详细看了相关资料,写了不少tcp的问题,比较基础的握手挥手就没记下来


tcp 大礼包

连接到底是什么?

所谓的连接其实只是双方都维护了一个状态,通过每一次通信来维护状态的变更,使得看起来好像有一条线关联了对方。

TCP 协议头

图片说明

序号:用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。

确认号:期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。

数据偏移:指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。

控制位:八位从左到右分别是 CWR,ECE,URG,ACK,PSH,RST,SYN,FIN。

CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,ECE 标志为 1 时,则通知对方已将拥塞窗口缩小;

ECE:若其值为 1 则会通知对方,从对方到这边的网络有阻塞。在收到数据包的 IP 首部中 ECN 为 1 时将 TCP 首部中的 ECE 设为 1;

URG:该位设为 1,表示包中有需要紧急处理的数据,对于需要紧急处理的数据,与后面的紧急指针有关;

ACK:该位设为 1,确认应答的字段有效,TCP规定除了最初建立连接时的 SYN 包之外该位必须设为 1;

PSH:该位设为 1,表示需要将收到的数据立刻传给上层应用协议,若设为 0,则先将数据进行缓存;

RST:该位设为 1,表示 TCP 连接出现异常必须强制断开连接;

SYN:用于建立连接,该位设为 1,表示希望建立连接,并在其序列号的字段进行序列号初值设定;

FIN:该位设为 1,表示今后不再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。

每个主机又对对方的 FIN 包进行确认应答之后可以断开连接。不过,主机收到 FIN 设置为 1 的 TCP 段之后不必马上回复一个 FIN 包,而是可以等到缓冲区中的所有数据都因为已成功发送而被自动删除之后再发 FIN 包;

窗口:窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。

TCP/UDP伪首部的理解

其目的是让UDP两次检查数据是否已经正确到达目的地,具体是那两次呢?我们注意伪首部字段:32位源IP地址、32位目的IP地址、8位协议、16位UDP长度。由此可知,第一次,通过伪首部的IP地址检验,UDP可以确认该数据报是不是发送给本机IP地址的;第二,通过伪首部的协议字段检验,UDP可以确认IP有没有把不应该传给UDP而应该传给别的高层的数据报传给了UDP。从这一点上,伪首部的作用其实很大。

SYN 超时了怎么处理?

也就是 client 发送 SYN 至 server 然后就挂了,此时 server 发送 SYN+ACK 就一直得不到回复,慢慢重试,阶梯性重试, 在 Linux 中就是默认重试 5 次,并且就是阶梯性的重试,间隔就是1s、2s、4s、8s、16s,再第五次发出之后还得等 32s 才能知道这次重试的结果,所以说总共等63s 才能断开连接

SYN Flood 攻击

可以开启 tcp_syncookies,那就用不到 SYN 队列了。

SYN 队列满了之后 TCP 根据自己的 ip、端口、然后对方的 ip、端口,对方 SYN 的序号,时间戳等一波操作生成一个特殊的序号(即 cookie)发回去,如果对方是正常的 client 会把这个序号发回来,然后 server 根据这个序号建连。

或者调整 tcp_synack_retries 减少重试的次数,设置 tcp_max_syn_backlog 增加 SYN 队列数,设置 tcp_abort_on_overflow SYN 队列满了直接拒绝连接。

IP 、UDP和TCP,每一种格式的首部中均包含一个检验和。对每种分组,说明检验和包括IP数据报中的哪些部分,以及该检验和是强制的还是可选的

除了UDP的检验和,其他都是必需的。IP检验和只覆盖了IP首部,而其他字段都紧接着IP首部开始。

为什么所有Internet协议收到有检验和错的分组都仅作丢弃处理?

源IP地址、源端口号或者协议字段可能被破坏了。

为什么会发生 TCP 粘包、拆包?

  • 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
  • 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
  • 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

粘包、拆包解决办法

由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:

  • 消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  • 设置消息边界:服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如 FTP 协议。
  • 将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段。
  • 更复杂的应用层协议比如 Netty 中实现的一些协议都对粘包、拆包做了很好的处理。

TCP 提供了一种字节流服务,而收发双方都不保持记录的边界。应用程序如何提供它们自己的记录标识?

很多Internet应用使用一个回车和换行来标记每个应用记录的结束。这是NVT ASCII采用的编码。另外一种技术是在每个记录之前加上一个记录的字节计数,DNS和Sun RPC采用了这种技术。

为什么在 TCP 首部的开始便是源和目的的端口号?

一个ICMP差错报文必须至少返回引起差错的IP数据报中除了IP首部的前8个字节。当TCP收到一个ICMP差错报文时,它需要检查两个端口号以决定差错对应于哪个连接。因此,端口号必须包含在TCP首部的前8个字节里。

为什么TCP 首部有一个首部长度字段而UDP首部中却没有?

TCP首部的最后有一些选项,但UDP首部中没有选项。

SYN 初始值 ISN 规律

当一端为建立连接而发送它的S Y N时,它为连接选择一个初始序号。ISN随时间而变化,因此每个连接都将具有不同的I S N。RFC 793 指出ISN可看作是一个 32 比特的计数器,每4 ms加1。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它作错误的解释。所以 ISN 变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法份子猜到 ISN。

全双工的体现

既然一个T C P连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。收到一个FIN只意味着在这一方向上没有数据流动。一个TCP连接在收到一个FIN后仍能发送数据。而这对利用半关闭的应用来说是可能的,尽管在实际应用中只有很少的T C P应用程序这样做。

图片说明

MSS 确定(商议)的?

它并不是任何条件下都可协商。当建立一个连接时,每一方都有用于通告它期望接收的MSS选项(MSS选项只能出现在SYN报文段中)。如果一方不接收来自另一方的MSS值,则MSS就定为默认值536字节(这个默认值允许20字节的I P首部和20字节的TCP首部以适合576字节I P数据报)。

TCP 最大段大小

最大段大小是指 TCP 协议所允许的从对方接收到的最大报文段,因此这也是通信对方在发送数据时能够使用的最大报文段。根据 [RFCO879],最大段大小只记录 TCP 数据的字节数而不包括其他相关的 TCP 与 IP 头部。当建立一条 TCP 连接时,通信的每一方都要在 SYN 报文段的 MSS 选项中说明自已允许的最大段大小。这 16 位的选项能够说明最大段大小的数值。在没有事先指明的情况下,最大段大小的默认数值为 536 字节。任何主机都应该能够处理至少 576 字节的 IPv4 数据报。如果接照最小的 IPv4 与 TCP 头部计算, TCP 协议要求在每次发送时的最大段大小为 536 字节,这样就正好能够组成一个 576 (20+20+536=576)字节的 IPv4 数据报。

最大段大小的数值为 1460。 这是 IPv4 协议中的典型值,因此 IPv4 数据报的大小也相应增加 40 个字节(总共 1500 字节,以太网中最大传输单元与互联网路径最大传输单元的典型数值): 20 字节的 TCP 头部加 20 字节的 IP 头部。

当使用 IPv6 协议时,最大段大小通常为 1440 字节。由于 IPv6 的头部比 IPv4 多 20 个字节,因此最大段大小的数值相 应减少 20 字节。在 [RFC2675] 中 65535 是一个特殊数值,与 IPv6 超长数据报一起用来指定一个表示无限大的有效最大段大小值。在这种情况下,发送方的最大段大小等于路径 MTU 的数值减去 60 字节(40 字节用于 IPv6 头部, 20 字节用于 TCP 头部)。值得注意的是,最大段大小并不是 TCP 通信双方的协商结果,而是一个限定的数值。当通信的一方将自已的最大段大小选项发送给对方时,它已表明自已不愿意在整个连接过程中接收任何大于该尺寸的报文段。

time_wait 的坏处

当 TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在Time_wait状态停留的时间为2倍的M S L。这样可让TCP再次发送最后的A C K以防这个ACK丢失(另一端超时并重发最后的F I N)。

这种2 MSL等待的另一个结果是这个TCP连接在2 MSL等待期间,定义这个连接的插口(客户的I P地址和端口号,服务器的I P地址和端口号)不能再被使用。这个连接只能在2 MSL结束后才能再被使用。服务器通常执行被动关闭,不会进入TIME_WAIT状态。这暗示如果我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地端口。这不会带来什么问题,因为客户使用本地端口,而并不关心这个端口号是什么。然而,对于服务器,情况就有所不同,因为服务器使用熟知端口。如果我们终止一个已经建立连接的服务器程序,并试图立即重新启动这个服务器程序,服务器程序将不能把它的这个熟知端口赋值给它的端点,因为那个端口是处于2 MSL连接的一部分。在重新启动服务器程序前,它需要在1 ~ 4分钟。

RST 复位什么时候发出

一般说来,无论何时一个报文段发往基准的连接( referenced connection)出现错误, TCP都会发出一个复位报文段(这里提到的“基准的连接”是指由目的 IP 地址和目的端口号以及源 IP 地址和源端口号指明的连接。这就是为什么RFC 793称之为插口)。

产生复位的一种常见情况是当连接请求到达时,目的端口没有进程正在听。

发送一个复位报文段而不是F I N来中途释放一个连接。有时称这为异常释放(abortive release)。异常终止一个连接对应用程序来说有两个优点:(1)丢弃任何待发数据并立即发送复位报文段;(2)R S T的接收方会区分另一端执行的是异常关闭还是正常关闭。应用程序使用的A P I必须提供产生异常关闭而不是正常关闭的手段,R S T报文段中包含一个序号和确认序号。需要注意的是R S T报文段不会导致另一端产生任何响应,另一端根本不进行确认。收到R S T的一方将终止该连接,并通知应用层连接复位。

TCP 选项有什么

图片说明

  1. 窗口扩大选项使 TCP 的窗口定义从16 bit增加为32 bit。这并不是通过修改TCP首部来实现的, T C P首部仍然使用16 bit ,而是通过定义一个选项实现对16 bit 的扩大操作 来完成的。于是T C P在内部将实际的窗口大小维持为32 bit的值。
  2. 时间戳选项使发送方在每个报文段中放置一个时间戳值。接收方在确认中返回这个数值,从而允许发送方为每一个收到的A C K计算RT T(我们必须说“每一个收到的A C K”而不是“每一个报文段”,是因为T C P通常用一个A C K来确认多个报文段)。我们提到过目前许多实现为每一个窗口只计算一个RT T,对于包含8个报文段的窗口而言这是正确的。然而,较大的窗口大小则需要进行更好的RT T计算。
  3. 最大报文传输段(Maximum Segment Size ---MSS)
  4. 选择确认选项(Selective Acknowledgements --SACK)

半打开连接和半关闭连接的区别是什么?

在一个半关闭的连接上,一个端点已经发送了一个FIN,正等待另一端的数据或者一个FIN。

一个半打开的连接是当一个端点崩溃了,而另一端还不知道的情况。

未连接队列:在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到 SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于Syn_RCVD状态,当服务器收到客户的确认包时,删除该条目, 服务器进入ESTABLISHED状态。

ACK延迟确认机制

接收方在收到数据后,并不会立即回复ACK,而是延迟一定时间。一般ACK延迟发送时间为200ms,但是这个200ms并非收到数据后需要延迟的时间。系统有一个固定的定时器每隔200ms会来检查是否需要发送ACK包。这样做有2个目的:

  1. 这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。
  2. 如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量。

SACK(Selective Acknowledgment)

SACK是一个TCP的选项,来允许TCP单独确认非连续的片段,用于告知真正丢失的包,只重传丢失的片段。要使用SACK,2个设备必须同时支持SACK才可以,建立连接的时候需要使用SACK Permitted的option,如果允许,后续的传输过程中TCP segment中的可以携带SACK option,这个option内容包含一系列的非连续的没有确认的数据的seq range

TCP收到乱序数据后会将其放到乱序序列中,然后发送重复ACK给对端。对端如果收到多个重复的ACK,认为发生丢包,TCP会重传最后确认的包开始的后续包。这样原先已经正确传输的包,可能会重复发送,降低了TCP性能。为改善这种情况,发展出SACK技术,使用SACK选项可以告知发包方收到了哪些数据,发包方收到这些信息后就会知道哪些数据丢失,然后立即重传丢失的部分。

SACK 重传

  1. 未启用 SACK 时,TCP 重复 ACK 定义为收到连续相同的 ACK seq。[RFC5681]

  2. 启用 SACK 时,携带 SACK 的 ACK 也被认为重复 ACK。[RFC6675]

    SACK option格式 Kind 5 Length 剩下的都是没有确认的segment的range了 比如说segment 501-600 没有被确认,那么Left Edge of 1st Block = 501,Right Edge of 1st Block = 600,TCP的选项不能超过40个字节,所以边界不能超过4组

Nagle算法

在局域网上,小分组(被称为微小分组)通常不会引起麻烦,因为局域网一般不会出现拥塞。但在广域网上,这些小分组则会增加拥塞出现的可能。

该算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他的小分组。相反, TCP收集这些少量的分组,并在确认到来时以一个分组的方式发出去。该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快。而在希望减少微小分组数目的低速广域网上,则会发送更少的分组。插口API用户可以使用 TCP_NODELAY 选项来关闭Nagle算法。

Karn算法

当一个超时和重传发生时,在重传数据的确认最后到达之前,不能更新RT T估计器,因为我们并不知道A C K对应哪次传输(也许第一次传输被延迟而并没有被丢弃,也有可能第一次传输的A C K被延迟)并且,由于数据被重传, RTO已经得到了一个指数退避,我们在下一次传输时使用这个退避后的RTO。对一个没有被重传的报文段而言,除非收到了一个确认,否则不要计算新的RTO。

Karn 算法在分组丢失时可以不测量 RTT 就能解决重传的二义性问题。

快重传:3次相同的ack后会进入慢启动吗?

No,在这种情况下没有执行慢启动的原因是由于收到重复的ACK不仅仅告诉我们一个分组丢失了。由于接收方只有在收到另一个报文段时才会产生重复的ACK,而该报文段已经离开了网络并进入了接收方的缓存。也就是说,在收发两端之间仍然有流动的数据,而我们不想执行慢启动来突然减少数据流。

流程

1) 当收到第3个重复的ACK 时,将ssthresh 设置为当前拥塞窗口 cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。

2) 每次收到另一个重复的ACK时, cwnd增加1个报文段大小并发送1个分组(如果新的 cwnd允许发送)。

3) 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个 ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第1个重复的A C K之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。

保活计时器的作用?

TCP的Keepalive,目的在于看看对方有没有发生异常,如果有异常就及时关闭连接。当传输双方不主动关闭连接时,就算双方没有交换任何数据,连接也是一直有效的。保活定时器每隔一段时间会超时,超时后会检查连接是否空闲太久了,如果空闲的时间超过了设置时间,就会发送探测报文。然后通过对端是否响应、响应是否符合预期,来判断对端是否正常,如果不正常,就主动关闭连接,而不用等待HTTP层的关闭了。

SYN Cookies

在最常见的SYN Flood攻击中,攻击者在短时间内发送大量的TCP SYN包给受害者。受害者(服务器)为每个TCP SYN包分配一个特定的数据区,只要这些SYN包具有不同的源地址(攻击者很容易伪造)。这将给TCP服务器造成很大的系统负担,最终导致系统不能正常工作。

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

cookie的计算:服务器收到一个SYN包,计算一个消息摘要mac。

CLOSE_WAIT过多的解决方法

系统产生大量“Too many open files”

原因分析:在服务器与客户端通信过程中,因服务器发生了 socket 未关导致的 closed_wait 发生,致使监听 port 打开的句柄数到了 1024 个,且均处于 close_wait 的状态,最终造成配置的port被占满出现 “Too many open files”,无法再进行通信。

解决办法:有两种措施可行

一、解决:
原因是因为调用 ServerSocket 类的 accept() 方法和 Socket 输入流的 read() 方法时会引起线程阻塞,所以应该用 setSoTimeout() 方法设置超时(缺省的设置是0,即超时永远不会发生);超时的判断是累计式的,一次设置后,每次调用引起的阻塞时间都从该值中扣除,直至另一次超时设置或有超时异常抛出。比如,某种服务需要三次调用 read(),超时设置为1分钟,那么如果某次服务三次 read()调用的总时间超过 1 分钟就会有异常抛出,如果要在同一个 Socket 上反复进行这种服务,就要在每次服务之前设置一次超时。

二、规避:
调整系统参数,包括句柄相关参数和TCP/IP的参数;

  1. open files 参数值 加大
  2. 当客户端因为某种原因先于服务端发出了 FIN 信号,就会导致服务端被动关闭,若服务端不主动关闭 socket 发 FIN 给 Client,此时服务端 Socket 会处于 CLOSE_WAIT 状态(而不是 LAST_ACK 状态)。通常来说,一个 CLOSE_WAIT 会维持至少 2 个小时的时间(系统默认超时时间的是 7200 秒,也就是 2 小时)。如果服务端程序因某个原因导致系统造成一堆 CLOSE_WAIT 消耗资源,那么通常是等不到释放那一刻,系统就已崩溃。因此,解决这个问题的方法还可以通过修改 TCP/IP 的参数来缩短这个时间,于是修改 tcp_keepalive_*系列参数: /proc/sys/net/ipv4/tcp_keepalive_time , /proc/sys/net/ipv4/tcp_keepalive_probes ,/proc/sys/net/ipv4/tcp_keepalive_intvl

短连接,并行连接,持久连接与长连接

短连接

短连接多用于操作频繁,点对点的通讯,而且连接数不能太多的情况。每个 TCP 连接的建立都需要三次握手,每个 TCP 连接的断开要四次挥手。适用于并发量大,但是每个用户又不需频繁操作的情况。

但是在用户需要频繁操作的业务场景下(如新用户注册,网购提交订单等),频繁的使用短连接则会使性能时延产生叠加。

用户登录这些不频繁的操作可以考虑用短连接。

并行连接

针对短连接,人们想出了优化的办法,连接多条,形成并行连接。

并行连接允许客户端打开多条连接,并行地执行多个事务,每个事务都有自己的TCP连接。这样可以克服单条连接的空载时间和带宽限制,时延可以重叠起来,而且如果单条连接没有充分利用客户端的网络带宽,可以将未用带宽分配来装载其他对象。

在PC时代,利用并行连接来充分利用现代浏览器的多线程并发下载能力的场景非常广泛。

但是并行连接也会产生一定的问题,首先并行连接不一定更快,因为带宽资源有限,每个连接都会去竞争这有限的带宽,这样带来的性能提升就很小,甚至没什么提升。

一般机器上面并行连接的条数 4 - 6 条

持久连接

HTTP1.0 版本以后,允许 HTTP 设备在事务处理结束之后将 TCP 连接保持在打开状态,以便为未来的 HTTP 请求重用现存的连接。在事务处理结束之后仍然保持在打开状态的 TCP 连接被称为持久连接。

持久连接的时间参数,通常由服务器设定,比如 nginx 的 keepalivetimeout,keepalive timout 时间值意味着:一个 http 产生的 tcp 连接在传送完最后一个响应后,还需要 hold 住 keepalive_timeout 秒后,才开始关闭这个连接;

在 HTTP 1.1 中 所有的连接默认都是持续连接,除非特殊声明不支持。HTTP 持久连接不使用独立的 keepalive 信息,而是仅仅允许多个请求使用单个连接。然而,Apache 2.0 httpd 的默认连接过期时间是仅仅 15 秒,对于 Apache 2.2 只有 5 秒。短的过期时间的优点是能够快速的传输多个 web 页组件,而不会绑定多个服务器进程或线程太长时间。

持久连接与并行连接相比,带来的优势如下:

  1. 避免了每个事务都会打开/关闭一条新的连接,造成时间和带宽的耗费;
  2. 避免了 TCP 慢启动特性的存在导致的每条新连接的性能降低;
  3. 可打开的并行连接数量实际上是有限的,持久连接则可以减少建立的连接的数量;

长连接

长连接与持久连接本质上非常的相似,持久连接侧重于 HTTP 应用层,特指一次请求结束之后,服务器会在自己设置的 keepalivetimeout 时间到期后才关闭已经建立的连接。长连接则是 client 方与 server 方先建立连接,连接建立后不断开,然后再进行报文发送和接收,直到有一方主动关闭连接为止。

长连接的适用场景也非常的广泛:

  1. 监控系统:后台硬件热插拔、LED、温度、电压发生变化等;
  2. IM 应用:收发消息的操作;
  3. 即时报价系统:例如股市行情 push 等;
  4. 推送服务:各种 App 内置的 push 提醒服务;

TIME_WAIT快速回收与重用

https://blog.csdn.net/dog250/article/details/13760985

Linux实现了一个TIME_WAIT状态快速回收的机制,即无需等待两倍的MSL这么久的时间,而是等待一个Retrans时间即释放,也就是等待一个重传时间(一般超级短,以至于你都来不及能在netstat -ant中看到TIME_WAIT状态)随即释放。释放了之后,一个连接的tuple元素信息就都没有了。在快速释放掉TIME_WAIT连接之后,peer依然保留着。丢失的仅仅是端口信息。不过有了peer的IP地址信息以及TCP最后一次触摸它的时间戳就足够了,TCP规范给出一个优化,即一个新的连接除了同时触犯了以下几点,其它的均可以快速接入,即使它本应该处在TIME_WAIT状态(但是被即快速回收了):
1.来自同一台机器的TCP连接携带时间戳;
2.之前同一台peer机器(仅仅识别IP地址,因为连接被快速释放了,没了端口信息)的某个TCP数据在MSL秒之内到过本机;
3.新连接的时间戳小于peer机器上次TCP到来时的时间戳,且差值大于重放窗口戳。

建议:如果前端部署了三/四层NAT设备,尽量关闭快速回收,以免发生NAT背后真实机器由于时间戳混乱导致的SYN拒绝问题。

TW重用 有相关的规范,即:如果能保证以下任意一点,一个TW状态的四元组(即一个socket连接)可以重新被新到来的SYN连接使用:

1.初始序列号比TW老连接的末序列号大
2.如果使能了时间戳,那么新到来的连接的时间戳比老连接的时间戳大

BBR 算法

BBR 全称 bottleneck bandwidth and round-trip propagation time。基于包丢失检测的 Reno、NewReno 或者 cubic 为代表,其主要问题有 Buffer bloat 和长肥管道两种。和这些算法不同,bbr 算***时间窗口内的最大带宽 max_bw 和最小 RTT min_rtt,并以此计算发送速率和拥塞窗口。

当没有足够的数据来填满管道时,RTprop 决定了流的行为;当有足够的数据填满时,那就变成了 BtlBw 来决定。这两条约束交汇在点 inflight =BtlBw*RTprop,也就是管道的 BDP(带宽与时延的乘积)。当管道被填满时,那些超过的部分(inflight-BDP)就会在瓶颈链路中制造了一个队列,从而导致了 RTT 的增大。当数据继续增加直到填满了缓存时,多余的报文就会被丢弃了。拥塞就是发生在 BDP 点的右边,而拥塞控制算法就是来控制流的平均工作点离 BDP 点有多远。

TCP 参数

  • Backlog参数:表示未连接队列的最大容纳数目。
  • SYN-ACK 重传次数: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同。
  • 半连接存活时间:是指半连接队列的条目存活的最长时间,也即服务从收到SYN包到确认这个报文无效的最长时间,该时间值是所有重传请求包的最长等待时间总和。有时我们也称半连接存活时间为Timeout时间、SYN_RECV存活时间。
  • 开启SYN Cookies : net.ipv4.tcp_syncookies = 1
  • 开启timewait重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0 :net.ipv4.tcp_tw_reuse = 1
  • 开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭;net.ipv4.tcp_tw_recycle = 1
  • tw 时间 : net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
  • 当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟(20*60s)
    net.ipv4.tcp_keepalive_time = 1200

tcp 异常

试图与一个不存在的端口建立连接

这符合触发发送RST分节的条件,目的为某端口的SYN分节到达,而端口没有监听,那么内核会立即响应一个RST,表示出错。客户端TCP收到这个RST之后则放弃这次连接的建立,并且返回给应用程序一个错误。正如上面所说的,建立连接的过程对应用程序来说是不可见的,这是操作系统帮我们来完成的,所以即使进程没有启动,也可以响应客户端。

试图与一个不存在的主机上面的某端口建立连接

这也是一种比较常见的情况,当某台服务器主机宕机了,而客户端并不知道,仍然尝试去与其建立连接。根据上面的经验,这次主机已经处于未启动状态,操作系统也帮不上忙了,那么也就是连RST也不能响应给客户端,此时服务器端是一种完全没有响应的状态。那么此时客户端的TCP会怎么办呢?据书上介绍,如果客户端TCP没有得到任何响应,那么等待6s之后再发一个SYN,若无响应则等待24s再发一个,若总共等待了75s后仍未收到响应就会返回ETIMEDOUT错误。这是TCP建立连接自己的一个保护机制,但是我们要等待75s才能知道这个连接无法建立,对于我们所有服务来说都太长了。更好的做法是在代码中给connect设置一个超时时间,使它变成我们可控的,让等待时间在毫秒级还是可以接收的。

Server进程被阻塞

由于某些情况,服务器端进程无法响应任何请求,比如所在主机的硬盘满了,导致进程处于完全阻塞,通常我们测试时会用gdb模拟这种情况。上面提到过,建立连接的过程对应用程序是不可见的,那么,这时连接可以正常建立。当然,客户端进程也可以通过这个连接给服务器端发送请求,服务器端TCP会应答ACK表示已经收到这个分节(这里的收到指的是数据已经在内核的缓冲区里准备好,由于进程被阻塞,无法将数据从内核的缓冲区复制到应用程序的缓冲区),但永远不会返回结果。

我们杀死server

这是线上最常见的操作,当一个模块上线时,OP同学总是会先把旧的进程杀死,然后再启动新的进程。那么在这个过程中TCP连接发生了什么呢。在进程正常退出时会自动调用close函数来关闭它所打开的文件描述符,这相当于服务器端来主动关闭连接——会发送一个FIN分节给客户端TCP;客户端要做的就是配合对端关闭连接,TCP会自动响应一个ACK,然后再由客户端应用程序调用close函数,也就是我们上面所描述的关闭连接的4次挥手过程。接下来,客户端还需要定时去重连,以便当服务器端进程重新启动好时客户端能够继续与之通信。

当然,我们要保证客户端随时都可以响应服务器端的断开连接请求,就必须不能让客户端进程再任何时刻阻塞在任何其他的输入上面。比如,书上给的例子是客户端进程会阻塞在标准输入上面,这时如果服务器端主动断开连接,显然客户端不能立刻响应,因为它还在识图从标准输入读一段文本……当然这在实际中很少遇到,如果有多输入源这种情况的话开通通常会用类似select功能的函数来处理,可以同时监控多个输入源是否准备就绪,可以避免上述所说的不能立即响应对端关闭连接的情况。

Server进程所在的主机关机

实际上这种情况不会带来什么更坏的后果。在系统关闭时,init进程会给所有进程发送SIGTERM信号,等待一段时间(5~20秒),然后再给所有仍在运行的进程发送SIGKILL信号。当服务器进程死掉时,会关闭所有文件描述符。带来的影响和上面杀死server相同。

Server进程所在的主机宕机

这是我们线上另一种比较常见的状况。即使宕机是一个小概率事件,线上几千台服务器动不动一两台挂掉也是常有的事。主机崩溃不会像关机那样会预先杀死上面的进程,而是突然性的。那么此时我们的客户端准备给服务器端发送一个请求,它由write写入内核,由TCP作为一个分节发出,随后客户阻塞于read的调用(等待接收结果)。对端TCP显然不会响应这个分节,因为主机已经挂掉,于是客户端TCP持续重传分节,试图从服务器上接收一个ACK,然而服务器始终不能应答,重传数次之后,大约4~10分钟才停止,之后返回一个ETIMEDOUT错误。

这样尽管最后还是知道对方不可达,但是很多时候我们希望比等待4~10分钟更快的知道这个结果。可以为read设置一个超时时间,就得到了一个较好的解决方法。但是这样还是需要等待一个超时时间,事实上TCP为我们提供了更好的方法,用SO_KEEPALIVE的套接字选项——相当于心跳包,每隔一段时间给对方发送一个心跳包,当对方没有响应时会一更短的时间间隔发送,一段时间后仍然无响应的话就断开这个连接。

服务器进程所在的主机宕机后重启

在客户端发出请求前,服务器端主机经历了宕机——重启的过程。当客户端TCP把分节发送到服务器端所在的主机,服务器端所在主机的TCP丢失了崩溃前所有连接信息,即TCP收到了一个根本不存在连接上的分节,所以会响应一个RST分节。如果开发的代码足够健壮的话会试图重新建立连接,或者把这个请求转发给其他服务器。

当TCP连接的进程在忘记关闭Socket而退出、程序崩溃、或非正常方式结束进程的情况下(Windows客户端),会导致TCP连接的对端进程产生“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误

当TCP连接的进程机器发生死机、系统突然重启、网线松动或网络不通等情况下,连接的对端进程可能检测不到任何异常,并最后等待“超时”才断开TCP连接

当TCP连接的进程正常关闭Socket时,对端进程在检查到TCP关闭事件之前仍然向TCP发送消息,则在Send消息时会产生“32: Broken pipe”(Linux下)或“10053: An established connection was aborted by the software in your host machine”(Windows下)错误

当TCP连接的对端进程已经关闭了Socket的情况下,本端进程再发送数据时,第一包可以发送成功(但会导致对端发送一个RST包过来):
之后如果再继续发送数据会失败,错误码为“10053: An established connection was aborted by the software in your host machine”(Windows下)或“32: Broken pipe,同时收到SIGPIPE信号”(Linux下)错误;
之后如果接收数据,则Windows下会报10053的错误,而Linux下则收到正常关闭消息

TCP连接的本端接收缓冲区中还有未接收数据的情况下close了Socket,则本端TCP会向对端发送RST包,而不是正常的FIN包,这就会导致对端进程提前(RST包比正常数据包先被收到)收到“10054: An existing connection was forcibly closed by the remote host”(Windows下)或“104: Connection reset by peer”(Linux下)错误

PSH和URG

PSH 标志的作用就在这里,当PSH 被置为1 时, 会被立即推出,不会等待其他数据进入缓冲区。当接受端收到 PSH 被置 1 的数据包时,立即将该分段上交到对应的应用程序。即有如下作用:

  • 通知发送方立即发送数据。
  • 接收方立即将数据推送到应用程序。

这个标志是在TCP层清空发送缓存,并将报文段交给IP层的时候设置的(相当于表示一次TCP层的发送操作)。(还有一点需要注意:大多数的API没有向应用层提供通知TCP层设置PUSH标志的方法,据说是因为很多实现程序认为PUSH标志已经过时,而一个好的TCP实现能够自行决定何时设置这个标志。

URG 标志用于通知接收方,数据段内某些数据是需要紧急处理的,应该被优先考虑。接收方收到 URH标志有效的数据报时,回去检测 TCP 报头中的16 位字段 紧急指针。 该字段指示从第一个字节计数的段中的数据时多少紧急处理的。

端口号

标准的端口号由 Internet 号码分配机构(IANA)分配。这组数字被划分为特定范围,包括
熟知端口号(0 - 1023)、注册端口号(1024 - 49151)和动态/私有端口号(49152 - 65535)。

如果我们测试这些标准服务和其他 TCP/IP 服务(Telnet、 FTP、 SMTP等) 使用的端口号,会发现它们大多数是奇数。这是有历史原困的,这些端口号从 NCP 端口号派生而来(NCP 是网络控制协议,在 TCP 之前作为 ARPANET 的传输层协议)。NCP 虽然简单,但不是全双工的,困此每个应用需要两个连接,并为每个应用保留奇偶成对的端口号。当 TCP 和 UDP 成为标准的传输层协议时,每个应用只需要一个端口号,因此来自 NCP 的奇数端口号被使用。

QQ,微信

早期的时候,QQ 还是主要使用 TCP 协议,而后来就转向了采用 UDP 的方式来保持在线,TCP 的方式来上传和下载数据。现在,UDP 是 QQ 的默认工作方式,表现良好。相信这个也被沿用到了微信上。

简单的考证:登录 PC 版 QQ,关闭多余的 QQ 窗口只留下主窗口,并将其最小化。几分钟过后,查看系统网络连接,会发现 QQ 进程已不保有任何 TCP 连接,但有 UDP 网络活动。这时在发送聊天信息,或者打开其他窗口和功能,将发现 QQ 进程会启用 TCP 连接。

登陆成功之后,QQ 有一个 TCP 连接来保持在线状态。这个 TCP 连接的远程端口一般是80,采用 UDP 方式登陆的时候,端口是 8000。

QQ 客户端之间的消息传送采用了 UDP,因为国内的网络环境非常复杂,而且很多用户采用的方式是通过代理服务器共享一条线路上网的方式,在这些复杂的情况下,客户端之间能彼此建立起来 TCP 连接的概率较小,严重影响传送信息的效率。而 UDP 包能够穿透大部分的代理服务器,因此 QQ 选择了 UDP 作为客户之间的主要通信协议。

腾讯采用了上层协议来保证可靠传输:如果客户端使用 UDP 协议发出消息后,服务器收到该包,需要使用 UDP 协议发回一个应答包。如此来保证消息可以无遗漏传输。之所以会发生在客户端明明看到“消息发送失败”但对方又收到了这个消息的情况,就是因为客户端发出的消息服务器已经收到并转发成功,但客户端由于网络原因没有收到服务器的应答包引起的。

参考 :


牛客优惠码,需要自取

算法基础入门 : 立减200 点击(https://www.nowcoder.com/courses/cover/live/520?coupon=A7zf1nB))
算法基础提升 : 立减200 点击(https://www.nowcoder.com/courses/cover/live/512?coupon=Ap2vpbF)
算法进阶中级班 : 立减300 点击(https://www.nowcoder.com/courses/cover/live/501?coupon=AeTBPMD)
算法进阶高级版 立减300 点击(https://www.nowcoder.com/courses/cover/live/493?coupon=AiC45Jz)
牛客java项目立减100元 点击(https://www.nowcoder.com/courses/cover/live/246?coupon=AfvtMp4),完成课程还能返现金学费300元.

#面经##校招##Java工程师#
全部评论
老哥你这tcp的问题都问了还是自己整理的这问的有点深感觉
2 回复 分享
发布于 2020-09-29 19:03
楼主好厉害,写一个线程池可太优秀了😂
2 回复 分享
发布于 2020-09-30 08:53
牛掰啊
2 回复 分享
发布于 2021-01-02 15:39
老哥太强啦!
1 回复 分享
发布于 2020-09-29 19:13
老哥很细!
1 回复 分享
发布于 2020-09-29 20:00
老哥是北京java吗?我27面完二面官网状态一直是面试是还有三面机会吗?
1 回复 分享
发布于 2020-09-29 21:29
老哥,总结的太到位了吧,码
1 回复 分享
发布于 2020-09-30 16:13
不错
1 回复 分享
发布于 2020-12-10 21:44
昨天刚二面完,感觉没了~~~快手没池子叭
点赞 回复 分享
发布于 2020-09-29 18:54
整理的不错!
点赞 回复 分享
发布于 2020-09-29 20:45
沾沾喜气
点赞 回复 分享
发布于 2020-09-29 21:05
基础研发平台,做IM部门么😂😂
点赞 回复 分享
发布于 2020-09-29 21:47
m
点赞 回复 分享
发布于 2020-09-30 00:45
🐂🍺
点赞 回复 分享
发布于 2020-09-30 06:55
楼主强啊,这TCP问的,有点深入啊。膜!
点赞 回复 分享
发布于 2020-09-30 08:59
太多了吧,mark
点赞 回复 分享
发布于 2020-09-30 11:16
tql
点赞 回复 分享
发布于 2020-09-30 12:26
tql
点赞 回复 分享
发布于 2020-09-30 13:28
tql
点赞 回复 分享
发布于 2020-09-30 13:38
tql
点赞 回复 分享
发布于 2020-09-30 13:44

相关推荐

72 632 评论
分享
牛客网
牛客企业服务