名词解释
什么是无源连接?
在网络通信中,无源连接(passive connection)是指连接建立后,两个终端之间的状态保持不活跃,并且没有持续的资源(比如带宽)占用。这通常意味着,在这种连接中,如果不传输数据,网络资源不会被占用或浪费。
什么是有源连接?
有源连接(active connection)指的是连接一旦建立,带宽资源就已经被分配并保持占用,不管是否有数据传输。例如,传统电话系统在虚拟信号时代就是有源连接。即使没有实际的通话数据传输,连接也会占用带宽资源。
为什么进行心跳检查?
心跳检查是用于确保两个通信节点之间的连接仍然有效的机制。它通过定期发送特殊的小数据包(心跳信号)来确认连接状态。心跳检查的重要性如下:
- 确保连接状态:即便是无源连接,如果一方的状态机(用于管理连接状态的逻辑)出现问题或连接丢失,对方可能对这个问题无从知晓。心跳信号能够确保双方都能实时获得连接状态。
- 检测和应对故障:如果没有心跳检查,一端可能已经丢失了连接,但对方还认为连接存在,这可能导致严重的通信问题。心跳信号的缺失表明连接不再有效,允许双方采取适当的恢复措施。
- 保持连接活跃:在某些网络环境中(特别是防火墙和NAT网络),长时间不活动的连接可能会被中断。定期的心跳信号有助于保持连接活跃,避免因长时间不活动被断开。
为什么要有心跳包
为什么要心跳检查,因为目前讨论的数据连接场景,都是无源连接,排除NAT的情况,连 接就是存在于src和dest两端OS中的状态机,为什么会要用无源连接呢,有源是连接建立带 宽就分配好了,不传有效数据这个带宽也被占用着,这不就浪费了,虚拟信号时代的电话 就是有源的。 心跳检查是两端都要做的,不做的那一端一样存在状态不对而不自知的情况。
一、TCP协议
TCP(Transmission Control Protocol)通信过程中,Sequence Number、Next Sequence Number和Acknowledgment Number这三个参数起着至关重要的作用,它们帮助维持数据包的有序传输和错误恢复。
-
Sequence Number(序列号):用于标识从TCP源端到目的端的字节流,它表示在这个报文段中的第一个字节数据在整个发送序列中的序号,用于保证TCP报文段能按照发送顺序接收。
-
Next Sequence Number(下一个序列号):它表示本报文段最后一个字节的下一个字节的序号,用于接收方期望接收下一个字节的顺序号。
-
Acknowledgment Number(确认号):它的值等于TCP收到的最后一个字节的数据序号加1,也就是期望从对端下一个接收的字节的数据的序号。
以下是一个基本示例,假设在没有数据丢失的情况下:
示例:
-
主机A向主机B发送一个携带数据的TCP段,假设Sequence Number为200,数据长度为100字节,所以Next Sequence Number为200+100=300。
-
当主机B成功接收这个TCP段时,向主机A发送一个确认报文段,其Acknowledgment Number应设为主机A的Next Sequence Number(300),表明主机B已经成功接收由主机A发送的到序号299为止的所有数据,并期待接收序号为300的数据。
-
如果出现数据丢失,例如主机A发送的数据段没有被成功接收,邑可以利用 Acknowledgment Number和Sequence Number进行乱序检测和重传控制。
二、TCP心跳包
sequence number + TCP Segment len = next sequence number
你看到的相等意味着这个tcp包没有有效载荷(包体没有数据)
握手 心跳 重传(Dup Ack) 拒绝RST等这种控制类的 基本上 TCP Seglen = 0
但是 TCP 本身提供的 Keep-alive 报文特征就非常不同了。首先,它的序列号就很奇特, 是上一个报文的序列号减 1,载荷为 0。回复的报文也同样特别,确认号为收到的序列号 加 1。而且,无论是探测包还是回复包,其载荷长度都为 0
文字描述不是很容易理解,我给你看一个实际的例子。这是某一次我用 Chrome 浏览器访 问网站时做的抓包:
上图的红色底色的多个报文,就是 Wireshark 识别出来的 TCP 心跳包。我也把需要关注 的信息用红色方框标注出来了。
25 号报文是离心跳包最近的一个常规报文,Wireshark 告诉我们:它的下一个序列号(图 中的 NextSeq)是 1578。也就是说,如果下一个是常规报文,那么这个常规报文的序列 号就是 1578。然后看同是这个客户端发出的报文 27,这就是一个心跳包,它的序列号却 是 1577(也就是 1578-1),载荷为 0(Len=0)。对端对这个心跳包做了回应(包号 28),确认号为 1578(1577+1),载荷也为 0
要是你了解 TCP 握手和挥手阶段的确认号的话,你对这个 +1 机制是不是感觉很熟悉?可 见,TCP 认为心跳包也是十分重要的,它跟握手和挥手一样,都属于控制报文,它的确认 号机制也体现了这一特点。
有趣的是,RFC1122 里并没有规定心跳探测包的载荷一定是 0,它也可以是 1。只是从我 有限的抓包经验来看,心跳包都是载荷为 0 的,看来这是比较常见的实现方式。
三、Keep-alive
1.1 http层级的keep-alive
HTTP/1.0 默认是短连接,HTTP/1.1 和 2 默认是长连接。
Connection: Keep-alive 在 HTTP/1.0 里,能起到维持长连接的作用,而在 HTTP/1.1 里面没有这个作用(因为默认就是长连接)。
Connection: Close 在 HTTP/1.1 里,可以起到优雅关闭连接的作用。这个头部在流量 调度场景下也很有用,能明显加快基于 DNS/GSLB 的流量调整的收敛速度
1.2 tcp层级的keep-alive
其实,如果不做显式的配置,默认创建出来的 TCP Socket 是不启用 Keep-alive 的,也就 是都不会发送心跳包。不过,大部分应用程序已经在代码里启用了 Keep-alive,所以你平 时不太会遇到连接失效的问题。比如我稍后要演示的一个含心跳包的抓包文件,抓取的就 是 Chrome 浏览器的流量,里面就有很多心跳包,因为 Chrome 浏览器启用了 TCP 心跳 保活机制
要打开这个 TCP Keep-alive 特性,你需要使用 setsockopt() 系统调用,对已经创建的 Socket 进行配置,启用 Keep-alive。具体的调用方法,你可以参考 man setsockopt。
在 Linux 操作系统层级,也有三个跟 Keep-alive 有关的全局配置项
间隔时间:net.ipv4.tcp_keepalive_time,其值默认为 7200(秒),也就是2个小时最大探测次数:net.ipv4.tcp_keepalive_probes,在探测无响应的情况下,可以发送的 最多连续探测次数,其默认值为 9(次)最长间隔:net.ipv4.tcp_keepalive_intvl,在探测无响应的情况下,连续探测之间的最 长间隔,其值默认为 75(秒)
补充:你可以在 Linux 系统里面,执行 man tcp,查看内核对 TCP 协议栈的详细文档。
如果我们连接启用了 Keep-alive,但没有设定自定义的数值,那么就会使用上面这些默认 值,即:当连接闲置(没有数据交互)达到 7200 秒(2 小时)时发送心跳包,每次心跳包 超时时间为 75 秒,最多重试 9 次。 这样的话,对于一个已经失效的 TCP 连接,最大需要 7200+75*9=7875 秒 约等于 2 小 时 11 分钟)才能探测到
毫无疑问,这个时间是相当长的。不过结合时代背景,这个其实也可以理解:TCP Keep alive 被设计的时候是八十年代,当时因特网还很初级,所以设计者们并不想让心跳包占据 太多的网络资源。从而,就有了这么一个感知时间很长的心跳机制。关于 TCP Keep-alive 的一些更多信息在RFC1122里,你有兴趣的话可以去研究一下。
默认 TCP 连接并不启用 Keep-alive,若要打开的话要显式地调用 setsockopt(),来设 置保活包的发送间隔、等待时间、重试个数等配置。在全局层面,Linux 还默认有 3 个跟 Keep-alive 相关的内核配置项可以调整:tcp_Keepalive_time,tcp_Keepalive_probes,还有 tcp_Keepalive_intvl。
四、wireshark的flow graph功能
wireshark可以在statistics菜单里面下拉找到flow graph
五、MTU与MSS的区别、TSO
5.1 MTU(第三层 IP网络层 越靠近应用层、层级越大包大小越小)
- MTU,中文叫最大传输单元,也就是第三层的报文大小的上限。MTU 是一个静态设置,在同样的路径上,一旦某个尺寸的报文一次没通过,后续的这个尺寸的报文全都不能通过。
5.1.1MTU引发问题的一般对策
ip addr #就可以查看到各个接口的 MTU
sudo ip link set enp0s3 mtu 1400 # 调整mtu为1400字节
5.2 MSS(第四层 传输层 越靠近应用层、层级越大包大小越小)
-
MTU 本身是三层的概念,而在第四层的 TCP 层面,有个对应的概念叫 MSS,Maximum Segment Size(最大分段尺寸),也就是单纯的 TCP 载荷的最大尺寸
-
MTU 是三层报文的大小,在 MTU 的基础上刨去 IP 头部 20 字节和 TCP 头部 20 字节,就得到了最常见的 MSS 1460 字节。
5.2.1 MSS在TCP里面是怎么体现的
MSS 其实也是在握手阶段完成“通知”的。在 SYN 报文里,客户端向服务端通报了自己的 MSS。而在 SYN+ACK 里,服务端也做了类似的事情。这样,两端就知道了对端的 MSS,在这条连接里发送报文的时候,双方发送的 TCP 载荷都不会超过对方声明的 MSS。
当然,如果发送端本地网口的 MTU 值,比对方的 MSS + IP header + TCP header 更低,那么会以本地 MTU 为准,这一点也不难理解。这里借用一下 RFC879 里的公式:
SndMaxSegSiz = MIN((MTU - sizeof(TCPHDR) - sizeof(IPHDR)), MSS)
MTU 是两端的静态配置,除非我们登录机器,否则改不了它们的 MTU。但是,它们的TCP 报文却是在网络上传送的,而我们做“暗箱操作”的机会在于:TCP 本身不加密,这就使得它可以被改变!也就是我们可以在中间环节修改 TCP 报文,让其中的 MSS 变为我们想要的值,比如把它调小。
我们可以用iptables,在中间环节(比如某个软件路由或者软件网关)上,在 iptabes 的 nat 表和 FORWARD 链这个位置,我们可以添加规则,修改报文的 MSS 值。比如在这个案例里,我们通过下面这条命令,把经过这个网络环节的 TCP 握手报文里的 MSS,改为 1400 字节:
iptables -A FORWARD -p tcp --tcp-flags SYN SYN -j TCPMSS --set-mss 1400
如果我们在某个中间节点上执行这个命令就如下图所示
5.3 TSO
前面说的都是操作系统会做 TCP 分段的情况。但是,这个工作其实还是有一些 CPU 的开销的,毕竟需要把应用层消息切分为多个分段,然后给它们组装 TCP 头部等。而为了提高性能,网卡厂商们提供了一个特性,就是让这个分段的工作从内核下沉到网卡上来完成,这个特性就是 TCP Segmentation Offload。
这里的 offload,如果仅仅翻译成“卸载”,可能还是有点晦涩。其实,它是 off + load,那什么是 load 呢?就是 CPU 的开销。如果网卡硬件芯片完成了这部分计算任务,那么CPU 就减轻负担了,这就是 offload 一词的真正含义。
TSO 启用后,发送出去的报文可能会超过 MSS。同样的,在接收报文的方向,我们也可以启用 GRO(Generic Receive Offload)。比如下图中,TCP 载荷就有 2800 字节,这并不是说这些报文真的是以 2800 字节这个尺寸从网络上传输过来的,而是由于接收端启用了 GRO,由接收端的网卡负责把几个小报文“拼接”成了 2800 字节。
所以,如果以后你在 Wireshark 里看到这种超过 1460 字节的 TCP 段长度,不要觉得奇怪了,这只是因为你启用了 TSO(发送方向),或者是 GRO(接收方向),而不是 TCP 报文真的就有这么大!
想要确认你的网卡是否启用了这些特性,可以用 ethtool 命令,比如下面这样:
对了,要想启用或者关闭 TSO/GRO,也是用 ethtool 命令,比如这样:
六、IP分片
IP 层也有跟 TCP 分段类似的机制。很多人搞不清 IP 分片和 TCP 分段的区别,甚至经常混为一谈。事实上,它们是两个在不同层面的分包机制,互不影响。
在 TCP 这一层,分段的对象是应用层发给 TCP 的消息体(message)。比如应用给 TCP协议栈发送了 3000 字节的消息,那么 TCP 发现这个消息超过了 MSS(常见值为1460),就必须要进行分段,比如可能分成 1460,1460,80 这三个 TCP 段。
在 IP 这一层,分片的对象是 IP 包的载荷,它可以是 TCP 报文,也可以是 UDP 报文,还可以是 IP 层自己的报文比如 ICMP。
为了帮助你理解 segmentation 和 fragmentation 的区别,我现在假设一个“奇葩”的场景,也就是 MSS 为 1460 字节,而 MTU 却只有 1000 字节,那么 segmentation 和fragmentation 将按照如下示意图来工作:
补充:为了方便讨论,我们假设 TCP 头部就是没有 Option 扩展的 20 字节。但实际场 景里,很可能 MSS 小于 1460 字节,而 TCP 头部也超过 20 字节。
当然,实际的操作系统不太会做这种自我矛盾的傻事,这是因为它自身会解决好 MSS 跟MTU 的关系,比如一般来说,MSS 会自动调整为 MTU 减去 40 字节。但是我们如果把视野扩大到局域网,也就是主机再加上网络设备,那么就有可能发生这样的情况:1460 字节的 TCP 分段由这台主机完成,1000 字节的 IP 分片由路径中某台 MTU 为 1000 的网络设备完成。
这里其实也有个隐含的条件,就是主机发出的 1500 字节的报文,不能设置 DF(Don’tFragment)位,否则它既超过了 1000 这个路径最小 MTU,又不允许分片,那么网络设备只能把它丢弃。
Wireshark 里,我们可以清楚地看到 IP 报文的这几个标志位:
现在我们假设主机发出的报文是不带 DF 位的,那么在这种情况下,这台网络设备会把它切分为一个 1000(也就是 960+20+20)字节的报文和一个 520(也就是 500+20)字节的报文。1000 字节的 IP 报文的 MF 位(More Fragment) 会设置为 1,表示后续还有更多分片,而 520 字节的 IP 报文的 MF 字段为 0。
这样的话,接收端收到第一个 IP 报文时发现 MF 是 1,就会等第二个 IP 报文到达,又因为第二个报文的 MF 是 0,那么结合第二个报文的 fragment offset 信息(这个报文在分片流中的位置),就把这两个报文重组为一个新的完整的 IP 报文,然后进入正常处理流程,也就是上报给 TCP。
不过在现实场景里,IP 分片是需要尽量避免的,原因有很多,主要是因为互联网是一个松散的架构,这就导致路径中的各个环节未必会完全遵照所有的约定。比如你发出了大于PMTU 的报文,寄希望于 MTU 较小的那个网络环节为你做分片,但事实上它可能不做分片,而是直接丢弃,比如下面两种情况:
- 它考虑到开销等问题,未必做分片,所以直接丢弃。
- 如果你的报文有 DF 标志位,那么也是直接丢弃。
即使它帮你做了分片,但因为开销比较大,增加的时延对性能也是一个不利因素。另外一个原因是,分片后,TCP 报文头部只在第一个 IP 分片中,后续分片不带 TCP 头部,那么防火墙就不知道后面这几个报文用的传输层协议是什么,可能判断为有害报文而丢弃。
七、重传的理解
一般来说发送方向接收方发了多个包,如果有几个包中间缺了,那么接收方会返回DupAck的次数是缺的第一个包后面的包。同时DupAck永远都是要缺的那个包。