Skip to content

小林coding的TCP篇!

先来一个超级无敌长提纲! image

TCP基本认识

TCP头格式有哪些?

image

  • 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机,每发送一次数据就累加一次,用来解决网络包乱序的问题。
  • 确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答之后,可以认为该序号之前的数据都已经被正确接收,是用来解决丢包问题。
  • 控制位:
    • ACK:为1时,确认应答字段有效。TCP规定,除了最初建立连接时的SYN包之外,该位置必须设置为1.
    • RST:为1时,说明出现异常,必须强制断开连接,就是reset的意思
    • SYN:为1时,表示希望建立连接,并在序列号字段进行序列号初始值设定,意思应该是,syn为1,那么SEQ数值就是序列号的初始值。
    • FIN:表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

为什么需要TCP协议?TCP工作在哪一层?

IP层,也就是网络层,是不可靠的,不保证网络包的交付、按序交付、不保证完整性。

如果要保障数据包的可靠性,就需要由上层TCP协议来负责。

TCP是工作在传输层的可靠的数据传输服务,确保接收端收到的是无损坏、无间隔、非冗余和按序的。

什么是TCP?

TCP是面向连接的、可靠的基于字节流的传输协议。

  • 面向连接:一定是一对一的,不能像UDP协议一样可以一个主机同时向多个主机发送消息,一对多是无法做到的。
  • 可靠的:无论网络链路如何变化,TCP保证一个报文一定能到达接收端。

字节流:用户消息通过TCP传输时,数据可能会被操作系统分组成多个TCP包。那么接收方需要知道消息的边界才能读取出有效的用户消息。并且TCP是有序的,当前一个TCP报文没有收到,即使收到了后面的,也不会给应用层处理。重复的TCP报文会自动丢弃。

什么是TCP连接?

用于保证可靠性和流量维护的信息的集合称为一个连接,如socket、序列号和窗口大小。

建立一个TCP连接,需要客户端和服务端达成上述消息的共识:

  • Socket:由IP地址和端口号组成
  • 序列号:用来解决乱序问题
  • 窗口大小:流量控制

如何唯一确定一个TCP连接?

通过一个四元组:(源地址、源端口、目标地址、目标端口)

其中,源地址和目标地址是不在TCP头部的,在IP中,32位。源端口和目标端口则在TCP头部,64位。

有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?

服务端通常固定在某个本地端口上监听,等待客户端请求。

对IPV4,客户端IP数最多是2^32次方,端口数最多是2^16次方。也就是服务端单机最大TCP连接数,约为2^48.

当然实际上不可能,有以下因素:

  • 文件描述符限制,每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。 Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
    • 系统级:当前系统可打开的最大数量,通过cat/proc/sys/fs/file-max 查看;
    • 用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看:
    • 进程级:单个进程可打开的最大数量,通过cat /proc/sys/fs/nr_open 查看
    • 内存限制: 每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会 发生 OOM。

UDP和TCP的区别?分别的应用场景是?

UDP不提供复杂控制机制,利用IP提供面向无连接的通信服务。

UDP的头部只有8个字节,格式如下:

image

包长度保留UDP首部长度跟数据长度之和。校验和是为了提供可靠的UDP首部和数据,防止收到网络传输中受损的UDP包。

TCP和UDP的区别:

  1. 连接
  • TCP面向连接,发送数据之前先建立连接。
  • UDP不需要连接,即刻传输数据。
  1. 服务对象
  • TCP是一对一的,UDP则支持一对一、一对多、多对多的交互通信。
  1. 可靠性
  • TCP是可靠交付的,数据可以无差错,不丢失、不重复、按序到达。
  • UDP属于最大努力交付,不保证可靠。但是有基于UDP的可靠传输协议,如QUIC协议,HTTP3就用到了。
  1. 拥塞控制、流量控制
  • TCP有拥塞控制和流量控制,保证数据传输安全。
  • UDP没有
  1. 首部开销
  • TCP首部长,没用“选项字段”是20个字节、如果用了就更长,而UDP只有8个字节。
  1. 传输方式
  • TCP是流式传输,没有边界,但保证顺序和可靠
  • UDP是以包为单位发送,有边界,但可能乱序和丢包
  1. 分片不同
  • TCP数据大小如果超过MSS,Max Segment Size,则会在传输层分片,目标收到后也在传输层组装分片,如果中途丢失了,只需要传输这个丢失的分片。
  • UDP的数据如果大于MTU大小,则会在IP层分片,目标主机收到后,在IP层组装完数据,接着再传给传输层。MTU是max transmission unit。是数据链路层的概念,一般以太网的MTU是1500字节,包括IP头部。

为什么分片层次不一样?

这里是我结合deepseek得到的结果。

TCP协议在握手的时候,也就是SYN为1的时候,会有选项字段,里面规定了MSS(一般是MTU-20-20,20是TCP首部长度,和IP首部长度。MTU一般是1500字节)。因此TCP自己会先主动分片,因为如果在IP层分片丢失,会导致整个数据包重传。

而UDP则摆烂,啥也不管,丢给下层网络层IP做处理,让IP层检查到数据超过MTU,被迫在IP层分片。

TCP和UDP应用场景

  • 由于TCP面向连接,能保证数据的可靠交付,经常用于FTP文件传输,HTTP/HTTPS。
  • 由于UDP面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,常用于:
    • 包总量较少的通信,如DNS、SNMP等;
    • 视频、音频等多媒体通信
    • 广播通信

为什么UDP头部没首部长度,而TCP有?

原因是TCP首部有可变长的选项字段,而UDP长度不变,无需多一个字段去记录UDP首部长度。

为什么UDP头部有包长度,TCP没有?

TCP如何计算负载数据长度? IP总长-IP首部长-TCP首部长。

为什么UDP不这么算?小林coding认为两种可能:

第一种说法:因为为了网络设备硬件设计和处理方便,首部长度需要是 4字节的整数倍。如果去掉 UDP 的「包长度」字段,那 UDP 首部长度就不是 4 字节的整数倍了,所以我觉得这可能是为了补全 UDP 首部长度是 4字节的整数倍,才补充了「包长度」字段。

第二种说法:如今的 UDP 协议是基于 IP 协议发展的,而当年可能并非如此,依赖的可能是别的不提供 自身报文长度或首部长度的网络层协议,因此 UDP 报文首部需要有长度字段以供计算。

TCP和UDP可以用同一个端口吗?

可以!

传输层的端口号作用是区分同一个主机上不同应用程序的数据包。

传输层的TCP和UDP在内核里是完全不同的两个模块。

当主机收到数据包之后,可以在IP包头的协议号知道是TCP/UDP协议,传给对应模块处理。 image

TCP连接建立

来个经典的三次握手图!

image

image

image

image

注意,第三次握手就已经可以携带数据了!

Linux怎么看TCP状态?

netstat -napt

为什么三次握手而不是两次、四次?

“因为三次握手才能保证双方具有接收和发送的能力。”low爆了!

前面我们知道什么是TCP连接,就是用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。

所以,重要的是为什么三次握手才可以初始化Socket、序列号和窗口大小并建立TCP连接。

原因1:避免历史连接:

三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。

我们考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还 被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了SYN(seq = 100)报文(注意!不是重传SYN,重传的 SYN 的序列号是一样的)

image

为什么两次握手不能阻止历史连接?因为如果是两次握手,服务端在收到客户端的SYN之后,就会马上进入ESTABLISHED状态。意味着这时可以给对方发送数据。假如这个连接是历史连接,它并不是知道自己是历史连接,只有收到客户端的RST报文后才会断开连接。那么服务端如果建立连接后发送了数据,这些数据就浪费了,因为是历史连接的。

image

原因2:同步双方初始序列号: 序列号作用:

  • 接收方可以去除重复的数据
  • 接收方可以根据数据包的序列号按顺序接收。
  • 可以标识发送出去的数据包中哪些是已经被对方收到的。

客户端发送携带初始序列号的SYN报文时,服务端就要返回一个ACK应答报文,标识服务端收到了客户端的SYN报文。反过来也一样。那么两次这样的操作,实际上形成了四次握手,而第二次和第三次可以合并为一次,因此是三次握手。

image

原因三:避免资源浪费:

如果只有两次握手,当客户端的SYN报文阻塞,导致多次发送,那么因为没有第三次握手,服务端不清楚客户端是否收到了自己的ACK,所以服务端每收到一个SYN就只能先主动建立一个连接。造成建立多个冗余连接,造成不必要资源浪费。

image

即如果两次握手,且可能有消息滞留情况下,服务端重复接受无用请求SYN报文,会造成重复分配资源。

小结

TCP建立连接时,通过三次握手能防止历史连接的建立,减少双方不必要的资源开销,能帮助双方同步初始序列号。

为什么每次建立TCP,初始化的序列号都要求不一样?

主要原因:

  • 为了防止历史报文被下一个相同四元组连接接收(主要)
  • 为了安全性,防止黑客伪造相同序列号的TCP报文被对方接收。

假设每次序列号从0开始,本次连接中,发送了一些数据,然后被阻塞了,此后客户端宕机重启之类的,重建了一个与上一个连接相同四元组的连接。

新连接建立完毕,上次发送的数据包此时刚好抵达了服务端,且刚好在服务端的接收窗口内,被服务端接收了,就造成了数据混乱。

如果每次建立连接,初始序列号都一样,很容易出现历史报文被下一个相同四元组连接接收的问题。

每次都不同初始序列号,可以很大程度上避免这个问题,但是不能完全避免,TCP利用时间戳机制避免历史报文。

既然IP层会分片,为什么TCP还需要MSS?

image

因为如果交给IP进行分片,隐患是:IP本身没有超时重传机制,而是由TCP控制的。当某个IP分片丢失之后,接收方的IP层无法组成完整TCP报文,也就无法把数据报文送到TCP层,所以接收方自然不会返回响应ACK。接着就会引起发送方的超时重传。

但是因为没有分片,因此会导致重发整个大的TCP报文。因此,如果让IP层进行分片传输,效率是很低的。

所以为了达到最佳的传输效能,TCP协议在建立的时候就要协商双方的MSS值。经过TCP分片后,如果一个TCP分片丢失,进行重发时也是以MSS为单位,而不是重传所有的分片,大大增加重传的效率。

第一次握手丢失会咋样?

客户端发出去的SYN假如阻塞了,触发超时重传,再发一次,注意SYN报文号是一样的。重发次数是写死在内核里面的。每次重传的等待时间是之前的两倍。

第二次握手丢失?

第二次握手丢失了,客户端和服务端都会重传,很好理解。

客户端重传SYN,服务端重传SYN-ACK报文。

image

第三次握手丢失?

image

什么是SYN攻击?

攻击者短时间伪造不同IP地址的SYN报文,而服务端每接收到一个SYN报文,就进入SYN_RCVD状态,但是服务端发出去的ACK+SYN是肯定不会得到应答的,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。

TCP连接断开

image

  • 客户端打算关闭连接,此时会发送一个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状态,至此客户端也完成连接的关闭。

有一点要注意,主动关闭连接的一方才有TIME_WAIT状态。

为什么挥手要四次?

最关键的是无法像握手那样合并第二次和第三次,因为服务端回复第二次ACK,表示收到了客户端准备关闭连接的请求。而可能服务端还有消息未发送完,必须发送完毕后才发送第三次握手,表示同意现在关闭连接。

为什么TIME_WAIT等待时间是2MSL

MSL:是max segment lifetime,最大报文生存时间,它是任何报文在网络上存在的最长时间,超过则被丢弃。

MSL和TTL的区别:MSL的单位是时间,TTL是经过路由跳数。所以MSL》=TTL消耗为0的时间。

TTL值一般是64,Linux的MSL设置为30秒,意味着Linux认为数据报文经过64个路由器的时间不会超过30秒。

TIME WAIT 等待2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方 的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待2 倍的时间。

为什么需要TIME_WAIT状态?

主动发起关闭的一方才会有TIME_WAIT状态。有它的原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误接受。
  • 保证被动关闭连接的一方能被正确关闭。

原因一:防止历史连接中的数据,被后面相同四元组的连接错误接受。

前置条件:序列号只有32位,用完了就回到0开始。初始序列号是随机的,基于时钟生成的一个随机数。

因此,无法根据序列号来判断新老数据。

假设TIME_WAIT没有等待时间或者很短,被延迟的数据包抵达后会发生什么?

image

image

如上图,服务端关闭连接之前发送的报文被延迟了,接着服务端以相同四元组重新打开了连接,之前被延迟的SEQ抵达了客户端,而该数据报文序列号刚好在客户端的接收窗口内,客户端就收到了,产生了数据错乱。

2MSL的时长,足以让两个方向上的数据包都被丢弃,使得新建立连接后,收到的数据包不会有历史残留的。

原因2:被动关闭连接的一方可以被正确关闭。

TIME_WAIT的作用还可以是等待足够长的时间,确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭。

image

其实事实上是不是也终止了连接?我觉得是,但是服务端收到了RST,解释为错误,并不优雅。

服务器出现大量TIME_WAIT状态的原因?

首先,只有主动关闭连接的一方才会有TIME_WAIT状态。 什么场景服务端主动断开?

  • HTTP没用长连接
  • HTTP长连接超时
  • HTTP长连接请求数量达到上限

如果已经建立了连接,但是客户端故障了怎么办?

如果服务端一直不发数据给客户端,那么永远无法感知到客户端宕机了。因此,TCP有保活机制

定义一个时间段,在时间段内没有任何连接活动,那么保活机制会开始生效。每隔一段时间发送一个探测报文,如果连续几个报文无响应,就可以认为当前TCP死亡了。

在Linux中,对应参数如下:

net.ipv4.tcp keepalive time=7200
net.ipv4.tcp keepalive intvl=75
net.ipv4.tcp keepalive probes=9

也就是说,在Linux中,最短需要两个多小时才可以发现一个死亡链接。

TCP保活机制检测时间有点长,web服务软件一般都会提供keepalive_timeout参数,指定HTTP长连接的超时时间,实现保活。

如果连接建立,服务端进程崩溃会怎么样?

TCP的连接信息是内核维护的,进程崩溃后,内核会处理TCP的释放过程。所以不用担心。

wow!