传输层协议TCP

TCP协议段格式

1.TCP报头至少20字节,每一行四字节,则至少5行,还可以选择十个选项,每个选项是一行,即四字节,那就是说tcp报头最大60字节

  1. 4位首部长度表示的不是tcp报头的字节数,而是tcp报头的行数,所以叫做4位首部行数更合适,这个值的范围应该是5~15。两个字节共十六位,去掉4位首部行数,保留6位什么都不干,然后还有6位是6个标志位

3.URG标志位(urgent)和16位紧急指针需要共同使用,URG表示有紧急数据,紧急指针是紧急数据的字节数,从有效载荷的第一个字节开始数

  • 例如:紧急指针值为 10,则紧急数据为有效载荷的前 10 个字节。

ACK(acknowlege)标志位表示应答,纯ACK报文其有效载荷是没有数据的,也不会消耗序列号

PSH(push)表示提交数据,其有效载荷就是要提交的数据,PSH和ACK可以一块用,即捎带应答

RST(reset)表示重新建立连接,当收到报文后,硬件中断进入协议栈,解包到tcp层后,根据五元组找到套接字,然后将套接字完全释放,重新建立连接需要应用层的帮助,重新connect才行,RST是做不到依靠协议栈来重新建立连接的

SYN(​​Synchronize)表示要建立连接(同步),其有效载荷并没有数据,但会消耗一个序列号,逻辑上视为 1 字节虚拟数据

FIN(final)表示要断开连接,其有效载荷并没有数据,但会消耗一个序列号,逻辑上视为 1 字节虚拟数据

4.序号和确认序号

在tcp三次握手时会商议初始序号(ISN Initial Serialize Number),后续的整个数据通信和四次挥手都是基于ISN一直递增。

序号用来给发送缓冲区中的数据编号,一个字节数据占一个序号,SYN和FIN也会消耗一个序号

确认序号是期望收到的下一个序号

1. 三次握手(ISN协商)​

  1. ​**​Client → Server: SYN, Seq=1000(Seq是序号,Ack是确认序号)**​

    • 客户端发送 SYN,初始序列号 ISN_C=1000
  2. ​Server → Client: SYN-ACK, Seq=5000, Ack=1001​

    • 服务器确认客户端的 ISN_C,并发送自己的 ISN_S=5000

    • Ack=1001表示期望客户端下次发送 Seq=1001

  3. ​Client → Server: ACK, Seq=1001, Ack=5001​

    • 客户端确认服务器的 ISN_S,握手完成。

​2. 数据传输(基于ISN递增)​

  1. ​Client → Server: PSH-ACK, Seq=1001, Ack=5001, data="Hello"​

    • 客户端发送数据(假设长度 5 字节),下次 Seq=1006
  2. ​Server → Client: ACK, Seq=5001, Ack=1006​

    • 服务器确认收到数据,期望下次客户端发送 Seq=1006
  3. ​Server → Client: PSH-ACK, Seq=5001, Ack=1006​,data="nihao"

  4. ​Client → Server: ACK, Seq=1006, Ack=5006

    .......


​3. 四次挥手(正常关闭)​

  1. ​Client → Server: FIN-ACK, Seq=2000, Ack=5001​

    • 客户端发起关闭,序列号从上次结束位置递增。
  2. ​Server → Client: ACK, Seq=5001, Ack=2001​

    • 服务器确认客户端的 FIN。
  3. ​Server → Client: FIN-ACK, Seq=5001, Ack=2001​

    • 服务器发起自己的 FIN(数据已发完)。
  4. ​Client → Server: ACK, Seq=2001, Ack=5002​

    • 客户端确认服务器的 FIN,连接完全关闭。

确认序号(期望收到的下一个序号)= 接收到的报文的序号+有效载荷的字节长度

序号 = 接收到的报文的确认序号(因为你期望得到的下一个序号,就是我将要为之发出的)

  1. 16位窗口大小

十六位窗口大小指的是接收窗口的大小,接收窗口是套接字接收缓冲区还能接收数据的大小

  1. 16位校验和

  2. ​伪头部(Pseudo-Header)​​:

    包含IP层的关键信息(源IP、目的IP、协议号、TCP长度),用于确保这些字段未被篡改。

    源IP(32位) | 目的IP(32位) | 保留(8位) | 协议号(8位) | TCP总长度(16位) 共12字节

  3. ​TCP头部​​:

    包括所有字段(源端口、目的端口、序列号、确认号等),​​校验和字段本身置为0​​。

  4. ​有效载荷​​:

    应用层数据(若长度为奇数,补0对齐)。


​2. 校验和的计算方法​

校验和是通过​​16位反码求和(One's Complement Sum)​​计算的,具体步骤如下:

  1. ​初始化​​:将校验和字段置为0。

  2. ​分段求和​​:将伪头部、TCP头部和有效载荷按16位(2字节)分段,累加所有分段。

  3. ​反码处理​​:将累加结果取反码(按位取反),得到最终校验和。

  4. ​验证​ ​:接收方对所有字段(包括校验和)再次求和,若结果为0xFFFF,则校验通过。

​公式表示​​:

校验和 = ~(伪头部之和 + TCP头部之和 + 数据之和)

TCP保证可靠传输的机制

1.确认应答机制

对于除了ACK/RST以外的tcp请求报文,必须有ACK报文进行确认应答

ack报文的生成规则

确认序号 = 收到的报文的序号 + 有效载荷长度

2.超时重传机制

对于除了ACK/RST这种不需要应答的报文以外,其余请求报文必须有ACK进行确认应答,倘若等待一段时间没收到确认应答,那就会超时重传请求报文。OS发送这种需要应答的报文会创建对应定时器,并通过时钟中断推动定时器,实现超时重传的功能,收到ack后释放对应定时器

超时时间是动态计算的:

第一次500ms

第二次2*500ms

第三次4*500ms

.....

若超时了五次,发送FIN也没什么意义,因为FIN也收不到确认应答,所以会直接释放自个通信套接字!

3.三次握手四次挥手连接管理机制

我们首先要明确一件事,那就是进程调用系统调用和TCP层的行为是既有同步也有异步的,即应用层和传输层的同步和异步,应用层是主要靠进程来推动,传输层主要是靠OS硬件中断、时钟中断来推动的

三次握手过程:

服务器进程调用socket先创建一个套接字,然后调bind将套接字绑定地址,接着调用listen将套接字设置为监听套接字,调用accept,这时因为监听套接字的全连接队列是空,所以服务器进程会在全连接队列的等待队列上阻塞等待。客户端进程用socket创建一个套接字,然后用connect,套接字会先自动绑定一个地址,修改套接字状态为SYN_SENT,开始组装SYN报文,然后交给IP层,封装IP报头,根据目的ip查路由表找到下一跳IP和发送网卡接口,然后将这些都交给数据链路层,网卡驱动会查arp缓存,得到目的mac地址,如果缓存中没有那就广播arp请求,然后目的主机会单播返回应答,最终也是得到了目的mac地址,然后接下来就是封装mac头和CRC校验,封装好之后,将mac帧写进发送网卡的发送缓冲区中,接着写发送网卡的TDT寄存器,通知网卡数据就绪,网卡会利用DMA将数据读出,然后HVY将数字信号转成光电信号,用接口发出,客户端进程会在套接字通用等待队列上阻塞。服务器网卡接收到光电信号后用HVY转成数字信号,然后DMA写进网卡接收缓冲区,接着网卡触发硬件中断,中断控制器生成中断号,给目标cpu设置高电平中断引脚,cpu执行完执行周期后进入中断周期,cpu检查if寄存器和中断引脚,然后将pc保存在用户栈栈顶,cpu陷入内核,将用户态寄存器保存在内核栈中,然后从中断控制器获取中断号,查中断向量表执行中断方法,实际上就是走协议栈,数据链路层网卡驱动从接收缓冲区中读出数据,然后检查mac头和crc校验位,无误之后交给ip层,IP层接收到全部分片后组装出tcp报文,交给tcp层,tcp层一看是SYN报文,那就去三元组里查套接字,查到监听套接字后,在半连接队列里创建struct request_sock,通信套接字雏形就出来了,其状态为SYN_RCVD,然后该次硬件中断还没完,会组建捎带应答SYN+ACK报文,然后发出。客户端网卡收到后同理会触发硬件中断,在传输层根据五元组找到通信套接字,然后唤醒套接字通用等待队列上的进程,套接字状态改成ESTABLISHED,接着还要再组建ACK应答发过去。客户端进程被唤醒后connect返回,接着执行剩下逻辑。服务器端网卡收到ACK后,触发硬件中断,然后层层解包到传输层,先去五元组里查找,发现找不到对应套接字,那就去三元组查找,找到监听套接字,然后将半连接队列中的struct request_sock给升级成struct sock加进全连接队列里,然后通信套接字状态改成ESTABLISHED,唤醒监听套接字全连接队列的等待队列上的服务器进程,服务器进程执行accept,将全连接队列中的struct sock创建对应的struct socket 和 struct file,返回对应的文件描述符

数据传输过程:

客户端主机收到SYN+ACK后,客户端进程被从套接字通用等待队列上唤醒,然后会调用write,将应用层数据写进通信套接字发送缓冲区中,等待数据达到MSS或没有发出但还未收到应答的数据那就开始组装报文,如果这两个条件都不满足还有保底的定时器机制,总之发送缓冲区数据会被加到tcp报文的有效载荷里面,然后走协议栈层层封装报头,最后发出去,服务器网卡收到后触发硬件中断,走协议栈层层解包,到tcp层会通过五元组找通信套接字,找到后将有效载荷弄进套接字接收缓冲区中,然后唤醒通信套接字接收缓冲区等待队列上的服务器进程,硬件中断接着组装ACK确认应答,然后发出去,毕竟发送数据是PSH报文,这是需要确认应答的。然后服务器进程被唤醒后会从接收缓冲区中读数据,然后处理。我们假设一下假如客户端发的数据包丢了,但是客户端进程的write很明显是异步的,进程并不会等待确认应答的到来,也就是说,write将数据写进发送缓冲区后,write就结束了,然后客户端进程会执行read,因为没数据而在套接字接收缓冲区阻塞等待,如果长时间没有收到确认应答,那么时钟中断使定时器到期后,会自动超时重传,假如ack响应客户端收到了,那就把定时器给销毁,就这样,发送非ACK/RST报文,OS都创建并管理对应定时器,除非收到对应的ACK,不然就依靠时钟中断来进行超时重传。

四次挥手过程:

假如客户端主动close(fd)关闭套接字,那么会释放掉struct file 和 struct socket,struct sock由OS来管理,并发送FIN包,套接字状态为FIN_WAIT_1,服务器网卡收到后触发硬件中断,然后走协议栈,到tcp层靠五元组找到套接字然后修改状态为CLOSE_WAIT,并组装ACK包发回去,剩下的事情已经和客户端进程没什么关系了,客户端网卡收到后触发硬件中断,走协议栈解包到tcp层然后修改通信套接字状态,并释放超时定时器。服务器进程也close(fd),同样的释放了struct file 和struct socket,然后struct sock给操作系统管理,修改通信套接字状态为LAST_ACK,发送FIN包给客户端,然后客户端网卡接收,触发硬件中断,解包到tcp层后根据五元组找到套接字,然后修改套接字状态为TIME_WAIT,并发送ACK包,服务器收到后这次解包到tcp层,然后把struct sock给释放,服务器端套接字彻底释放,客户端如果2*MSL没有收到那就会释放struct sock,至此,该连接优雅结束

总结:

1.对于需要应答的报文,OS会维护定时器并通过时钟中断来实现超时重传,当然如果收到确认应答ack那就释放定时器

2.套接字状态保存在struct sock中,甚至struct request_sock中也有,这就是为什么整个过程中套接字状态可以一直被保存(SYN_SENT、TIME_WAIT这些可不是连接状态,是套接字状态,如果是连接状态两边情况又怎么可能不同呢?我们也可以这样理解)

TIME_WAIT状态的理解

1、TIME_WAIT是通信套接字的一个状态,在四次挥手中主动关闭连接的一方,将会在第二阶段拥有这个状态。

2、MSL是max segment lifetime最大段生命周期,传输层报文我们一般称之为段,MSL就是tcp报文的最大生命周期,当然,MSL本质其实是一个估值,这个估值几乎可以保证所有tcp报文都会在这段时间内到达接收端,从而结束生命周期。

3.我们知道,一个报文被网卡接收,然后触发硬件中断,然后会走协议栈进行解包,而数据链路层是以目标mac地址来标识是不是发给主机,无法分辨新旧报文,IP层看ip地址,也分不出来,传输层看五元组,假如新连接和旧连接的五元组相同,也是分不出来的(但这一层也分不出来的概率挺低的,因为客户端套接字地址是随机的),所以如果新连接是在旧连接刚关闭后就建立好了,是完全有可能出现新连接误处理旧报文的情况,所以会有一个TIME_WAIT的2*MSL时间,来使旧报文全部到达接收端,这样网络中就不会有旧报文的残留,在此期间,该地址是被TIME_WAIT状态的套接字绑定的,其他套接字是无法bind的,所以2*MSL的时间保证了旧报文都已被接收,又保证了新连接是无法建立的,从而避免旧报文被新连接处理的情况

TIME_WAIT状态作用一:如果对方没有收到ACK,对方就会超时重传FIN包,处于TIME_WAIT状态的套接字如果在2*MSL内收到了对方超时重传的FIN包,这时TIME_WAIT一方就会重发ACK,这也是TIME_WAIT状态的意义之一,使对方的套接字能顺利释放

TIME_WAIT状态作用二:TIME_WAIT状态有2*MSL的时间,保证了本次连接的旧报文都已被接收,又保证了相同五元组新连接是无法建立的,从而避免旧报文被新连接处理的情况(这会引起一个问题,就是如果是服务器主动关闭连接,短时间内无法重启服务器,因为地址正被TIME_WAIT的套接字绑定,因此该地址没法bind其他套接字)

解决TIME_WAIT状态引起的bind失败

1.显式 bind vs 隐式绑定​

​类型​ ​定义​ ​示例​
​显式 bind 通过 bind() 系统调用明确绑定地址和端口 bind(sockfd, &addr, sizeof(addr))
​隐式绑定​ 由内核自动关联地址 accept() 返回的通信套接字复用监听套接字的地址

**2. SO_REUSEADDR**​​

  • ​只许一个​ 套接字显式bind处于 TIME_WAIT 状态的地址​
  • 主要解决服务器重启问题

边界条件验证​

cpp 复制代码
// 场景1:前一个套接字处于 TIME_WAIT
setsockopt(sock1, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(sock1, &addr, sizeof(addr));  // 成功(即使端口在 TIME_WAIT)

// 场景2:前一个套接字仍活跃(未关闭)
setsockopt(sock2, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(sock2, &addr, sizeof(addr));  // 失败(errno=EADDRINUSE)

**3. SO_REUSEPORT**​​

  • ​允许多个套接字显式bind绑定同一地址,无论该地址是否处于TIME_WAIT状态​
  • ​隐含 SO_REUSEADDR 的功能​

理解CLOSE_WAIT状态

被动断开连接一方的套接字,在第一阶段将会处于CLOSE_WAIT状态

CLOSE_WAIT状态一般是和read等函数配合使用,read从接收缓冲区中读,如果有数据那就读,如果没数据那就看套接字状态,倘若是ESTABLISHED,那就阻塞等待,如果是CLOSE_WAIT,那read就返回0,然后被动断开连接一方的进程可以继续执行close(fd),从而进入四次挥手的第二阶段,这里就不得不提,倘若应用层代码有BUG,被动方迟迟不close,那四次挥手就无法继续执行,双方套接字将会卡在FIN_WAIT_2和CLOSE_WAIT

TCP保证高效传输的机制

1.滑动窗口

滑动窗口里的数据可以用来连续发送报文段,无需等待单个ACK,一发一答的方式使整个通信中传输时间过长,效率很低,一次连续发送多个报文,使传输时间重叠在一起,效率就高了。

TCP的报文发送算法--nagle算法

1.当tcp发送缓冲区中没有未确认的数据或数据达到MSS,那就直接发送,否则不发送

2.nagle算法来实现数据的实际发送,我们也可以看出,tcp报文的发送其实是有条件的,其主要作用是尽可能合并小数据,减少报文发送次数
注意:
滑动窗口决定一次可以发送多少数据,那滑动窗口必须由网络状况和接收端接受能力来动态调整,所以 滑动窗口的大小 = min(接收窗口,拥塞窗口)

流量控制

根据接收端的处理能力(接收窗口), 来决定发送端的发送速度(滑动窗口). 这个机制就叫做流量控制(Flow Control)
接收端将自己接收缓冲区的剩余空间大小放入 TCP 报头中的 "窗口大小" 字段,倘若接收缓冲区满了,那就将报头中的窗口大小设为0,这时发送方不再发送数据, 但是发送方会定期发送一个窗口探测数据段,本质就是一个纯ack包,这样不会影响序列号什么的,接收端接收到ack后把窗口大小告诉发送端,同样用ack包回应
窗口大小是16 位数字最大表示 65535, 但TCP 接收缓冲区最大就是 65535 字节么?
实际上, TCP 报头 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口大小字段的值左移 M 位

拥塞控制

发送方会维护一个值叫做拥塞窗口,该值是用来衡量网络状况的,根据丢包情况和报文应答来动态调整拥塞窗口大小

拥塞窗口的单位不再是字节,而是MSS,最大段大小,这是由ip层的MTU最大传输单元来得到的,MTU是ip报文的最大值,MSS是tcp报文有效载荷的最大值

拥塞窗口是用来衡量网络状况的,那肯定是动态的


拥塞窗口一开始初始值为1,慢启动阈值一开始为16,然后每发送一个请求并收到对应ack应答,即完成一轮传输,那拥塞窗口值就乘2,达到慢启动阈值后,每完成一轮传输,拥塞窗口值加1,倘若遇到超时重传的丢包,也就是发送了请求但没收到ack导致超时,我们认为网络情况很不好,那就慢启动阈值变为拥塞窗口的一半,然后拥塞窗口变为1,倘若出现快重传的丢包,也就是一次发送多个请求报文,然后前面有几个请求丢了,但后面有请求正常到达,我们认为网络状况些许不好,那就慢启动阈值变为拥塞窗口的一半,然后拥塞窗口也变为拥塞窗口的一半,这叫做快恢复,相当于就是直接跳过了慢启动的过程,直接进入拥塞避免

概念介绍

慢启动(慢开始):拥塞窗口为1,然后每轮传输乘2,拥塞窗口指数级增长

拥塞避免:拥塞窗口达到慢启动阈值后,每轮传输拥塞窗口加1,线性增长

快恢复:倘如发生快重传的丢包情况,那就慢启动阈值变为拥塞窗口的一半,然后拥塞窗口也变为拥塞窗口的一半

2.快重传和累计确认

依靠滑动窗口的机制,发送方一次可以发送多个tcp报文,那这几个tcp报文的传输过程是有可能出现丢包的,可能是ack包丢了,也可能直接就是请求包丢了,对于不同丢包情况,tcp有不同策略来应对

累计确认

当前面的ack包丢了后,发送端可以通过后续的ack包来得知自己前面的数据包有没有到达,如上,确认序号为1001的ack包丢了,但当收到确认序号2001的ack包,那就可以得知1~1000数据包肯定已到达,因此1~1000的定时器直接释放就是了

快重传

滑动窗口内的数据可以用来连续发送报文,而不用等待ack,假设一个数据包丢了,比如上面的1001~2000数据包,丢了之后,接收到2001~3000的数据包,然后解包到tcp层,根据序号空出前面1001~2000的位置,然后将2001~3000的数据写进接收缓冲区对应位置,并组装ack包,其确认序号为1001,后面的多个数据包到来均是如此处理,于是发送方会收到多个相同确认序号的ack包,当收到三次后,不在等超时重传定时器到期了,直接重发1001~2000数据包,这就是快重传

3.延迟应答

如果接收端收到数据后就立即返回ack应答,这时可能套接字接收缓冲区的数据还没有处理多少,那么响应的ack报文的窗口大小字段就比较小,我们可以每两个报文给一个ack,也可以收到报文后等待个200ms再响应ACK包,主要是给接收端一定的时间,使接收缓冲区中的数据尽可能被读出处理,这样能返回ack时能设置一个更大的接收窗口

4.捎带应答

类似三次握手的第二次的SYN+ACK报文就是捎带应答,主要可以合并报文,这样就不用单独发送ack了

捎带应答的本质

  • ​定义​ ​:将ACK确认信息​​搭载在数据报文​​中发送,避免单独发送纯ACK包

  • ​设计目标​​:减少报文数量

面向字节流

每一个tcp套接字都拥有一个发送缓冲区和接收缓冲区

1、使用write会将数据写进发送缓冲区,也就是说发送缓冲区中其实都是应用层数据,但报文的发送依赖的其实是滑动窗口和nagle算法这些传输层机制,并不是write这种应用层接口

2.报文被网卡接收,然后写进网卡接收缓冲区,触发硬件中断,层层解包将应用层数据写进套接字接收缓冲区中,然后唤醒接收缓冲区等待队列上的进程,继续调用read从接收缓冲区中读出数据

粘包问题

从tcp接收缓冲区中读出来的数据会存在粘包问题,因为没法区分应用层数据的边界,udp没有这个问题,因为udp接收缓冲区中是udp报文,使用recvfrom会从udp接收缓冲区中读出一个完整报文,然后去掉udp报头将有效载荷写进recvfrom参数的缓冲区里面,而tcp是用write去接收缓冲区中读应用层数据,所以会有粘包问题

解决粘包问题

1、定长的包:就像udp一样,如果应用层数据包是定长的,那就每次读一个定长应用层数据包,没有粘包问题

2、添加应用层长度报头:在变长应用层数据包前面添加长度字段,作为应用层报头,这样从tcp接收缓冲区中读数据就可以根据长度字段读出完整应用层数据报文

3、添加明确分隔符:在应用层数据包后加分隔符对应用层数据包进行划分,这样也可以

TCP连接断开情况详解

1、四次挥手:

通过close释放struct file和struct socket,将struct sock交给操作系统管理,并发送FIN包给对方,对方响应ack,后面自身同样调close,最终双方通信套接字全都释放,非常优雅

2、进程终止:

进程终止的第一阶段会释放pcb的资源,其中就有文件描述符表,同样的调close,然后后面同样进行四次挥手

3、机器重启:

机器重启会退出进程,所以最终还是四次挥手断开连接

4.机器掉电:

假设接收方机器掉电,那发送方发送的报文就不会有响应,五次超时重传后无应答后,直接释放发送方套接字结构,包括struct sock

基于TCP应用层协议

HTTP
HTTPS
SSH
Telnet
FTP
SMTP
当然, 也包括你自己写 TCP 程序时自定义的应用层协议

相关推荐
大模型服务器厂商1 小时前
京东AI投资版图扩张:具身智能与GPU服务器重构科研新范式
服务器·人工智能·重构
开开心心就好2 小时前
Excel数据合并工具:零门槛快速整理
运维·服务器·前端·智能手机·pdf·bash·excel
武汉格发Gofartlic6 小时前
Fluent许可与网络安全策略
大数据·开发语言·网络·人工智能·安全·web安全·数据分析
悲伤小伞7 小时前
Linux_Ext系列文件系统基本认识(一)
linux·运维·服务器·c语言·编辑器
喜欢你,还有大家7 小时前
Linux笔记2——常用命令-1
linux·服务器·笔记
网硕互联的小客服7 小时前
服务器无法访问公网的原因及解决方案
运维·服务器
kyle~10 小时前
数据交换---JSON格式
服务器·microsoft·json
一只脑洞君11 小时前
tcp的三次握手与四次挥手
java·网络·tcp/ip
小周学学学11 小时前
Tomcat及Nginx部署使用
服务器·nginx·tomcat
想睡hhh11 小时前
Linux文件——文件系统Ext2(1)_理解硬件
linux·服务器·磁盘