什么是TCP
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
- 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
- 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。
tcp拥塞控制的几个策略
- 慢启动:在连接刚建立时,TCP发送方会以较小的拥塞窗口(cwnd)开始发送数据,并根据每次接收到的确认(ACK)增加cwnd的大小,从而逐渐增加发送速率,达到网络带宽的最佳利用。
- 拥塞避免:当
cwnd
>= ssthresh
时,就会使用「拥塞避免算法」,TCP发送方会切换到拥塞避免模式,此时每次接收到一个ACK时,cwnd的大小只会增加一个MSS(最大报文段长度),而不是每次都加倍。这样可以避免发送过多的数据导致网络拥塞。
- 拥塞发生:当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:
- 快速重传:当TCP发送方连续收到3个重复的ACK时,TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则
ssthresh
和 cwnd
变化如下:
cwnd = cwnd/2
,也就是设置为原来的一半;
ssthresh = cwnd
;
- 进入快速恢复算法
- 超时重传:如果在发送数据时未能及时接收到ACK,
ssthresh
设为 cwnd/2
,cwnd
重置为 1
- 快速恢复:进入快速恢复之前,
cwnd
和 ssthresh
已被更新了,然后,进入快速恢复算法如下:
- 拥塞窗口
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了);
- 重传丢失的数据包;
- 如果再收到重复的 ACK,那么 cwnd 增加 1;
- 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值(这里是快速重传时候的ssthresh ),原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
TCP如何保证可靠
- 重传机制
- 超时重传
- 快速重传
- SACK-- 可以将已收到的数据的信息发送给「发送方」
- D-SACK -- 使用了 SACK 来告诉「发送方」有哪些数据被重复接收了
- 滑动窗口
- 流量控制 -- TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
- 拥塞控制
TCP和UDP区别
1. 连接
- TCP 是面向连接的传输层协议,传输数据前先要建立连接。
- UDP 是不需要连接,即刻传输数据。
2. 服务对象
- TCP 是一对一的两点服务,即一条连接只有两个端点。
- UDP 支持一对一、一对多、多对多的交互通信
3. 可靠性
- TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
- UDP 是尽最大努力交付,不保证可靠交付数据。
4. 拥塞控制、流量控制
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
- UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
5. 头部长度不同
- TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是
20
个字节,如果使用了「选项」字段则会变长的。
- UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
6. 传输方式
- TCP 是流式传输,没有边界,但保证顺序和可靠。
- UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
7. 分片不同
- TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
- UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
两个队列
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
- 半连接队列,也称 SYN 队列;服务端收到第一次握手后,会将
sock
加入到这个队列中,队列内的sock
都处于SYN_RECV
状态。
- 全连接队列,也称 accept 队列;在服务端收到第三次握手后,会将半连接队列的
sock
取出,放到全连接队列中。队列里的sock
都处于 ESTABLISHED
状态。这里面的连接,就等着服务端执行accept()后被取出了。
socket函数常见方法
服务端方法:
socket
,新建一个socket对象
- 如果没有调用bind方法会,客户端对服务端发起了连接建立,服务端会回 RST 报文
bind
,将 socket 绑定在指定的 IP 地址和端口;
- 服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,然后客户端对服务端发起了连接建立,服务端会回 RST 报文。
listen
,进行监听,服务器端处于listen状态
- 每一个
socket
执行listen
时,内核都会自动创建一个半连接队列和全连接队列。
accept
,等待客户端连接;accept 成功返回是在三次握手成功之后
accept方法
只是为了从全连接队列中拿出一条连接,本身跟三次握手几乎毫无关系。
客户端
connect
,向服务端的地址和端口发起连接请求;connect 成功返回是在第二次握手
TCP三次握手
这个问题可以理解为为什么TCP不是两次握手,因为主要原因是两次握手只能确定一方的状态
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
需要注意的是第三次握手是可以携带数据的,前两次握手是不可以携带数据的
第一次握手丢失了,会发生什么?
- 客户端会重传 SYN 报文,也就是第一次握手就会触发「超时重传」机制,重传 SYN 报文
第二次握手丢失了,会发生什么?
- 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由
tcp_syn_retries
内核参数决定;
- 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由
tcp_synack_retries
内核参数决定。
第三次握手丢失了,会发生什么?
- 服务端会重传 SYN-ACK 报文,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传,经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。
TCP的ACK报文会重传吗
TCP协议中的ACK(Acknowledgment)报文通常不会重传。
ACK报文是TCP用于确认接收到数据的报文,用于确认对方发送的数据已经成功接收。在TCP的可靠传输机制中,发送方发送数据后,会等待接收方发送ACK报文进行确认。如果发送方在一定的超时时间内没有接收到ACK报文,则会认为数据没有成功送达,并进行重传(这里是对应报文不是ACK报文)。
在TCP协议中,数据的重传是由发送方负责的,而ACK报文的重传通常不会由接收方触发。一般情况下,接收方只需要发送ACK报文来确认已接收到数据,而不会因为没有接收到ACK报文而进行重传。接收方只有在需要通知发送方数据丢失或者需要进行流量控制等特殊情况下,才会发送特定的控制报文,如SACK(Selective Acknowledgment)报文或者窗口更新报文。
什么是 SYN 攻击?如何避免 SYN 攻击?
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
避免 SYN 攻击方式,可以有以下四种方法:
- 调大 内核处理速度
- 增大 TCP 半连接队列;
- 开启 tcp_syncookies;可以将全连接队列利用起来
- 减少 SYN+ACK 重传次数
TCP四次挥手
- 客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位被置为 1
的报文,也即 FIN
报文,之后客户端进入 FIN_WAIT_1
状态。
- 服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入 CLOSE_WAIT
状态。
- 客户端收到服务端的
ACK
应答报文后,之后进入 FIN_WAIT_2
状态。
- 等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入 LAST_ACK
状态。
- 客户端收到服务端的
FIN
报文后,回一个 ACK
应答报文,之后进入 TIME_WAIT
状态
- 服务端收到了
ACK
应答报文后,就进入了 CLOSE
状态,至此服务端已经完成连接的关闭。
- 客户端在经过
2MSL
一段时间后,自动进入 CLOSE
状态,至此客户端也完成连接的关闭。
你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
第一次挥手丢失了,会发生什么?
- 客户端:如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由
tcp_orphan_retries
参数控制。
- 服务端:后续会通过keepalive机制结束连接
第二次挥手丢失了,会发生什么?
- 客户端:ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制
- 服务端:服务端会主动调用close函数
第三次挥手丢失了,会发生什么?
- 服务端:如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文
第四次挥手丢失了,会发生什么?
- 服务端:如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文
为什么 TIME_WAIT 等待的时间是 2MSL?
2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
为什么需要 TIME_WAIT 状态?
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
- 保证「被动关闭连接」的一方,能被正确的关闭;
TIME_WAIT 过多有什么危害?
- 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
- 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为
32768~61000
,也可以通过 net.ipv4.ip_local_port_range
参数指定范围。
服务器出现大量 TIME_WAIT 状态的原因有哪些?
- 第一个场景:HTTP 没有使用长连接
- 第二个场景:HTTP 长连接超时
- 第三个场景:HTTP 长连接的请求数量达到上限
四次挥手可以变成三次吗?
答案是可以
什么是 TCP 延迟确认机制?
当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。 TCP 延迟确认的策略:
- 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
- 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
- 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
当被动关闭方在 TCP 挥手过程中,如果「没有数据要发送」,同时「没有开启 TCP_QUICKACK(默认情况就是没有开启,没有开启 TCP_QUICKACK,等于就是在使用 TCP 延迟确认机制)」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
close和shutdown的区别
- close 函数,同时 socket 关闭发送方向和读取方向,也就是 socket 不再有发送和接收数据的能力。如果有多进程/多线程共享同一个 socket,如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为 0,才会发出 FIN 报文。
- shutdown 函数,可以指定 socket 只关闭发送方向而不关闭读取方向,也就是 socket 不再有发送数据的能力,但是还是具有接收数据的能力。如果有多进程/多线程共享同一个 socket,shutdown 则不管引用计数,直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的进程企图使用该 socket,将会受到影响。
TCP 的 Keepalive
TCP 的 Keepalive 这东西其实就是 TCP 的保活机制
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件(默认是两个小时),那么内核里的 TCP 协议栈就会发送探测报文。
- 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
如何解决TCP的粘包问题?
什么是粘包问题?
粘包的问题出现是因为不知道一个用户消息的边界在哪,如果知道了边界在哪,接收方就可以通过边界来划分出有效的用户消息。
一般有三种方式分包的方式:
- 固定长度的消息:这种是最简单方法,即每个用户消息都是固定长度的,比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。
- 特殊字符作为边界:我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。
- 自定义消息结构:我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。