AI智能
改变未来

TCP


TCP

TCP是一个面向连接的、可靠的、基于字节流的传输层协议。
TCP需要三次握手建立连接。
TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错。这是有状态
当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发。这是可控制
TCP 为了维护状态,将一个个 IP 包变成了字节流。

UDP

UDP是一个面向无连接的传输层协议。
UDP没有相应建立连接的过程。
UDP 的数据传输是基于数据报的。

半连接队列

三次握手前,服务端的状态从CLOSED变为LISTEN, 同时在内部创建了两个队列:半连接队列和全连接队列,即SYN队列和ACCEPT队列。
当客户端发送SYN到服务端,服务端收到以后回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列,也就是半连接队列。
当客户端返回ACK, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue)。

SYN Flood 攻击

用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN。

  1. 处理大量的SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常的请求。
  2. 由于是不存在的 IP,服务端长时间收不到客户端的ACK,会导致服务端不断重发数据,直到耗尽服务端的资源。
增加 SYN 连接,也就是增加半连接队列的容量。减少 SYN + ACK 重试次数,避免大量的超时重发。利用 SYN Cookie 技术,在服务端接收到SYN后不立即分配连接资源,而是根据这个SYN计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证 Cookie 合法之后才分配连接资源。

TCP 三次握手


三次握手的目的是确认双方发送和接收的能力,三次就足够了。
第三次握手的时候,可以携带数据。前两次握手不能携带数据。
如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据,那么服务器势必会消耗更多的时间和内存空间去处理这些数据,增大了服务器被攻击的风险。
第三次握手的时候,客户端已经处于ESTABLISHED状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。

TCP 四次挥手


建立连接时,被动方服务器端结束CLOSED阶段进入“握手”阶段并不需要任何准备,可以直接返回SYN和ACK报文,开始建立连接。
释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。

等待2MSL的意义

RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。
MSL指的是Maximum Segment Lifetime:一段TCP报文在传输过程中的最大生命周期。
2MSL即是服务器端发出为FIN报文和客户端发出的ACK确认报文所能保持有效的最大时长。
确认服务器端是否收到客户端发出的ACK确认报文。
如果客户端在2MSL内,再次收到了来自服务器端的FIN报文,说明服务器端由于各种原因没有接收到客户端发出的ACK确认报文。客户端再次向服务器端发出ACK确认报文,计时器重置,重新开始2MSL的计时;
否则客户端在2MSL内没有再次收到来自服务器端的FIN报文,说明服务器端正常接收了ACK确认报文,客户端可以进入CLOSED阶段,完成“四次挥手”。

TCP 报文


如何标识唯一标识一个连接?答案是 TCP 连接的四元组——源 IP、源端口、目标 IP 和目标端口。
那 TCP 报文怎么没有源 IP 和目标 IP 呢?这是因为在 IP 层就已经处理了 IP 。TCP 只需要记录两者的端口即可。
序列号是一个长为 4 个字节,也就是 32 位的无符号整数,表示范围为 0 ~ 2^32 – 1。如果到达最大值了后就循环到0。

序列号在 SYN 报文中交换彼此的初始序列号。序列号保证数据包按正确的顺序组装。

窗口大小占用两个字节,也就是 16 位,但实际上是不够用的。因此 TCP 引入了窗口缩放的选项,作为窗口缩放的比例因子,这个比例因子的范围在 0 ~ 14,比例因子可以将窗口的值扩大为原来的 2 ^ n 次方。
校验和占用两个字节,防止传输过程中数据包有损坏,如果遇到校验和有差错的报文,TCP 直接丢弃之,等待重传。

控制位

1)URG:紧急指针标志,为1时表示紧急指针有效,为0则忽略紧急指针。
2)ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。
3)PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。
4)RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。
5)SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。
6)FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。

TCP 快速打开(TFO,TCP Fast Open)

  1. 客户端发送SYN给服务端,服务端接收到
  2. 服务端通过计算得到一个SYN Cookie, 将这个Cookie放到 TCP 报文的 Fast Open选项中,给客户端返回 SYN + ACK
  3. 客户端拿到这个 Cookie 的值缓存下来,发送ACK
在后面的三次握手中,客户端会将之前缓存的 Cookie、SYN 和HTTP请求(是的,你没看错)发送给服务端,服务端验证了 Cookie 的合法性,如果不合法直接丢弃;如果是合法的,那么就正常返回SYN + ACK。

可选项

timestamp是 TCP 报文首部的一个可选项,一共占 10 个字节
kind(1 字节) + length(1 字节) + info(8 个字节,timestamp和timestamp echo,各占 4 个字节)
kind = 8, length = 10

TCP 的时间戳主要解决两大问题:计算往返时延 RTT(Round-Trip Time)防止序列号的回绕问题

流量控制

对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。

  1. 双方在通信的时候,发送方的速率与接收方的速率是不一定相等,如果发送方的发送速率太快,会导致接收方处理不过来,这时候接收方只能把处理不过来的数据存在缓存区里(失序的数据包也会被存放在缓存区里)。
  2. 如果缓存区满了发送方还在疯狂着发送数据,接收方只能把收到的数据包丢掉,大量的丢包会极大着浪费网络资源,因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种动态平衡才好。
  3. 接收方每次收到数据包,可以在发送确定报文的时候,同时告诉发送方自己的缓存区还剩余多少是空闲的,我们也把缓存区的剩余大小称之为接收窗口大小,用变量win来表示接收窗口的大小。
  4. 发送方收到之后,便会调整自己的发送速率,也就是调整自己发送窗口的大小,当发送方收到接收窗口的大小为0时,发送方就会停止发送数据,防止出现大量丢包情况的发生。
  5. 当发送方收到接受窗口 win = 0 时,这时发送方停止发送报文,并且同时开启一个定时器,每隔一段时间就发个测试报文去询问接收方,打听是否可以继续发送数据了,如果可以,接收方就告诉他此时接受窗口的大小;如果接受窗口大小还是为0,则发送方再次刷新启动定时器。

TCP 滑动窗口

  1. 发送窗口:

  2. 接收窗口:

拥塞控制

对于拥塞控制来说,TCP 每条连接都需要维护两个核心状态:

  1. 拥塞窗口(Congestion Window,cwnd):目前自己还能传输的数据量大小,限制的是发送窗口的大小。发送窗口大小 = min(rwnd接收窗口大小, cwnd拥塞窗口大小)
  2. 慢启动阈值(Slow Start Threshold,ssthresh)

慢启动

刚开始进入传输数据的时候,你是不知道现在的网路到底是稳定还是拥堵的,如果做的太激进,发包太急,那么疯狂丢包,造成雪崩式的网络灾难。
一个 RTT 下来,cwnd翻倍,当 cwnd 到达慢启动阈值之后,cwnd 只能加这么一点: 1 / cwnd。

拥塞避免

当 cwnd 到达慢启动阈值之后,cwnd 只能加这么一点: 1 / cwnd。

快速重传

在 TCP 传输的过程中,如果发生了丢包,即接收端发现数据段不是按序到达的时候,接收端的处理是重复发送之前的 ACK。
比如第 5 个包丢了,即使第 6、7 个包到达的接收端,接收端也一律返回第 4 个包的 ACK。
当发送端收到 3 个重复的 ACK 时,意识到丢包了,于是马上进行重传,不用等到一个 RTO 的时间到了才重传。
即使第 5 个包丢包了,当收到第 6、7 个包之后,接收端依然会告诉发送端,这两个包到了。剩下第 5 个包没到,就重传这个包。这个过程也叫做选择性重传(SACK,Selective Acknowledgment)

快速恢复

在这个阶段,发送端如下改变:

  1. 拥塞阈值降低为 cwnd 的一半
  2. cwnd 的大小变为拥塞阈值
  3. cwnd 线性增加

超时重传

TCP 具有超时重传机制,即间隔一段时间没有等到数据包的回复时,重传这个数据包。
这个重传间隔也叫做超时重传时间(Retransmission TimeOut, 简称RTO)

标准方法,也叫Jacobson / Karels 算法:

Nagle算法

试想一个场景,发送端不停地给接收端发很小的包,一次只发 1 个字节,那么发 1 千个字节需要发 1000 次。这种频繁的发送是存在问题的,不光是传输的时延消耗,发送和确认本身也是需要耗时的,频繁的发送接收带来了巨大的时延。

而避免小包的频繁发送,这就是 Nagle 算法要做的事情。

具体来说,Nagle 算法的规则如下:

  1. 当第一次发送数据时不用等待,就算是 1byte 的小包也立即发送
  2. 后面发送满足下面条件之一就可以发了:
    数据包大小达到最大段大小(Max Segment Size, 即 MSS)
    之前所有包的 ACK 都已接收到

延迟确认

试想这样一个场景,当我收到了发送端的一个包,然后在极短的时间内又接收到了第二个包,那我是一个个地回复,还是稍微等一下,把两个包的 ACK 合并后一起回复呢?

延迟确认(delayed ack)所做的事情,就是后者,稍稍延迟,然后合并 ACK,最后才回复给发送端。TCP 要求这个延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms。

不过需要主要的是,有一些场景是不能延迟确认的,收到了就要马上回复:

接收到了大于一个 frame 的报文,且需要调整窗口大小TCP 处于 quickack 模式(通过tcp_in_quickack_mode设置)发现了乱序包

TCP 的 keepalive

试想一个场景,当有一方因为网络故障或者宕机导致连接失效,由于 TCP 并不是一个轮询的协议,在下一个数据包到达之前,对端对连接失效的情况是一无所知的。
这个时候就出现了 keep-alive, 它的作用就是探测对端的连接有没有失效。
当超过一段时间之后,TCP自动发送一个数据为空的报文(侦测包)给对方,如果对方回应了这个报文,说明对方还在线,连接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为链接丢失,没有必要保持连接。

net.ipv4.tcp_keepalive_intvl = 15net.ipv4.tcp_keepalive_probes = 5net.ipv4.tcp_keepalive_time = 1800

keepalive是TCP保鲜定时器,当网络两端建立了TCP连接之后,闲置(双方没有任何数据流发送往来)了tcp_keepalive_time后,服务器就会尝试向客户端发送侦测包,来判断TCP连接状况(有可能客户端崩溃、强制关闭了应用、主机不可达等等)。
如果没有收到对方的回答(ack包),则会在 tcp_keepalive_intvl后再次尝试发送侦测包,直到收到对方的ack。
如果一直没有收到对方的ack,一共会尝试 tcp_keepalive_probes次,每次的间隔时间在这里分别是15s, 30s, 45s, 60s, 75s。
如果尝试tcp_keepalive_probes,依然没有收到对方的ack包,则会丢弃该TCP连接。
TCP连接默认闲置时间是2小时,一般设置为30分钟足够了。

http 的keep-alive

  1. 所谓短连接,就是每次请求一个资源就建立连接,请求完成后连接立马关闭。每次请求都经过“创建tcp连接->请求资源->响应资源->释放连接”这样的过程。
  2. 所谓长连接(persistent connection),就是只建立一次连接,多次资源请求都复用该连接,完成后关闭。
  3. 所谓并行连接(multiple connections),其实就是并发的短连接。
  4. 在HTTP/1.0里,client发出的HTTP请求头需要增加Connection:keep-alive字段
  5. 在HTTP/1.1里,就默认是开启了keep-alive,要关闭keep-alive需要在HTTP请求头里显示指定Connection:close
  6. 当浏览器请求的是一个静态资源时,即服务器能明确知道返回内容的长度时,可以设置Content-Length来控制请求的结束。浏览器通过这个字段来判断当前请求的数据是否已经全部接收。
  7. 当服务端无法知道实体内容的长度时,就可以通过指定Transfer-Encoding: chunked来告知浏览器当前的编码是将数据分成一块一块传递的。当然, 还可以指定Transfer-Encoding: gzip, chunked表明实体内容不仅是gzip压缩的,还是分块传递的。最后,当浏览器接收到一个长度为0的chunked时, 知道当前请求内容已全部接收。
  8. nginx的keepalive_timeout默认是60,即默认开启了http keepalive模式,而且http的长连接会保持60秒,60秒内没有数据来往即关闭连接。
    keepalive_requests : 100,设置每个长连接最多能处理的请求次数,超过了以后连接就会被close,定期关闭对于清理每个连接的占用的内存是非常必要的,否则连接占用的内存会越来越大,这是不推荐的。

TCP 协议面试灵魂10问
浅谈Http长连接和Keep-Alive以及Tcp的Keepalive

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » TCP