TCP/IP闲话

最近一段时间找实习找的好心酸,每次笔试的时候,都发现有知识了解的不够深入;每次面试的时候,都发现自己原本以为做的还不错的东西,在面试官看来一无是处。真的有些后悔本科到研究生一直自己瞎搞,导致没有一项技能是十分突出的。“一专多能”如果没有“专”,那“多能”在找工作的时候就没有太大的竞争力。

最近面的两个岗,竟然都问到的 TCP/IP 的内容,想当年计网也是轻松拿下的,却也在这些问题上支支吾吾半猜半懵的。赶紧回来重新温习一下基础吧。之后打算温习 SQL 的时候再总结一篇,然后剩下的就在准备过程中随便写写好了。——以下内容主要摘自TCP/IP 的那些事 的两篇博文,进行精炼和自己的理解。

主要包括:建立释放过程,及相关问题;重传方法;网络拥塞时的处理。跳过了滑动窗口和具体的选择重传等。

在被文档 TCP/IP 三次握手四次挥手的时候,基本会要求画出下面的这幅图,然后再做更深一步的询问。找到的这幅图感觉非常好,一来指出了建立和释放的流程,二来给出了各个状态名,三来给出了在调用哪些函数的时候,进行的状态变化(这个被问到过)。

连接建立及释放过程

TCP/IP连接建立及释放过程

对于建链接的3次握手:目的主要是要初始化Sequence Number 的初始值。双方同步 SN 的过程,即SYN,全称Synchronize Sequence Numbers。TCP 的传输需要依靠这个序号保证报文的有序,对拆开的报文进行拼接。

对于4次挥手:其实你仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。

连接建立相关

关于建连接时SYN超时:即服务端响应 SYN-ACK 后没有收到回复。Linux 下默认会重发5次,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP 连接才会断开

关于SYN Flood攻击:因为上述的这个问题,如果发送大量的 SYN 然后不做回复,会导致服务器的 SYN 连接队列耗尽,正常请求不能处理。

解决方案:Linux下给了一个叫tcp_syncookies的参数来应对这个事——当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,synccookies是妥协版的TCP协议,并不严谨。对于正常的请求,你应该调整三个TCP参数可供你选择,第一个是:tcp_synack_retries 可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大SYN连接数;第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。

关于ISN的初始化:ISN是不能hard code的,如果断线重连,无法确定收到的包是断线前发送的,还是重连后发送的。ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL,最大报文生存时间),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。

关于 TIME_WAIT

为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?主要有两个原因:

  • 1)TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL,
  • 2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。你可以看看这篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems

关于TIME_WAIT数量太多:在大量并发短连接时会出现此问题。通常设置两个参数,一个叫tcp_tw_reuse,另一个叫tcp_tw_recycle的参数,后者recyle比前者resue更为激进。另外,如果使用tcp_tw_reuse,必需设置tcp_timestamps=1,否则无效。如果不等待超时重用连接的话,新的连接可能会建不上。正如官方文档上说的一样“It should not be changed without advice/request of technical experts”。

  • 关于tcp_tw_reuse。官方文档上说tcp_tw_reuse 加上tcp_timestamps(又叫PAWS, for Protection Against Wrapped Sequence Numbers)可以保证协议的角度上的安全,但是你需要tcp_timestamps在两边都被打开(你可以读一下tcp_twsk_unique的源码 )。我个人估计还是有一些场景会有问题。
  • 关于tcp_tw_recycle。如果是tcp_tw_recycle被打开了话,会假设对端开启了tcp_timestamps,然后会去比较时间戳,如果时间戳变大了,就可以重用。但是,如果对端是一个NAT网络的话(如:一个公司只用一个IP出公网)或是对端的IP被另一台重用了,这个事就复杂了。建链接的SYN可能就被直接丢掉了(你可能会看到connection time out的错误)(如果你想观摩一下Linux的内核代码,请参看源码 tcp_timewait_state_process)。
  • 关于tcp_max_tw_buckets。这个是控制并发的TIME_WAIT的数量,默认值是180000,如果超限,那么,系统会把多的给destory掉,然后在日志里打一个警告(如:time wait bucket table overflow),官网文档说这个参数是用来对抗DDoS攻击的。也说的默认值180000并不小。这个还是需要根据实际情况考虑。

TIME_WAIT 只会出现在主动断开连接的一端,因此服务器端多设置 HTTP 的 KeepAlive,来避免需要断开 TCP 连接。

重连

超时重传和快速重传机制:

  • 超时重传:因为没有收到 ACK,超时之后发送方重传。但这时会导致后续的数据包收到后,也无法收到 ACK,导致带宽浪费(也可以只重传一个序号)。
  • 快速重传:TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。

TCP的RTT算法

从前面的TCP重传机制我们知道Timeout的设置对于重传非常重要。

  • 设长了,重发就慢,丢了老半天才重发,没有效率,性能差
  • 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。

为了动态地设置,TCP引入了RTT——Round Trip Time,也就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间,从而可以方便地设置Timeout——RTO(Retransmission TimeOut),以让我们的重传机制更高效。

经典算法:

  • 1)首先,先采样RTT,记下最近好几次的RTT值。
  • 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之间。

其他还有:Karn / Partridge 算法Jacobson / Karels 算法 , 似乎 Linux 上是用的后者。

TCP的拥塞处理 – Congestion Handling

TCP通过Sliding Window来做流控(Flow Control)(被我略过了),因为Sliding Window需要依赖于连接的发送端和接收端,其并不知道网络中间发生了什么。

具体一点,我们知道TCP通过一个timer采样了RTT并计算RTO,当延时突然增加,同时需要不断Time out,不断的重传数据,导致网络更加拥塞。对此TCP的设计理念是:TCP不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。关于拥塞控制的论文请参看《Congestion Avoidance and Control》(PDF)

拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。这四个算法不是一天都搞出来的,这个四算法的发展经历了很多时间,到今天都还在优化中。 备注:
• 1988年,TCP-Tahoe 提出了1)慢启动,2)拥塞避免,3)拥塞发生时的快速重传
• 1990年,TCP Reno 在Tahoe的基础上增加了4)快速恢复

简单来说

  • 慢启动:双方维持一个拥塞窗口(cwnd全称Congestion Window),对于一次正常的 ACK,cwnd++;每过一个 RTT, cwnd*2 指数上升。还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”
  • 拥塞避免:当达到 ssthresh 上限时,进入拥塞避免,每过一个 RTT cwnd+1;
  • 拥塞发生时:
    • 当发生超时时:cwnd 重置为1,上限减半,重新进入慢启动;
    • 当三次 ACK 时,cwnd 减半,上限等于 cwnd,进入快速恢复算法
  • 快速恢复算法:(好吧,我没看懂)下图是看到的一个比较好解释拥塞算法的图。快重传快恢复

参考文献:

  • https://segmentfault.com/a/1190000004954842
  • http://coolshell.cn/articles/11564.html
  • http://coolshell.cn/articles/11609.html

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注