传输层协议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 程序时自定义的应用层协议

相关推荐
SPC的存折3 分钟前
1、Redis数据库基础
linux·运维·服务器·数据库·redis·缓存
爱学习的小囧1 小时前
VMware ESXi 6.7U3v 新版特性、驱动集成教程和资源包、部署教程及高频问答详情
运维·服务器·虚拟化·esxi6.7·esxi蟹卡驱动
小疙瘩1 小时前
只是记录自己发布若依分离系统到linux过程中遇到的问题
linux·运维·服务器
dldw7772 小时前
IE无法正常登录windows2000server的FTP服务器
运维·服务器·网络
运维有小邓@2 小时前
什么是重放攻击?如何避免成为受害者?
运维·网络·安全
光路科技2 小时前
工业数字化三大核心概念拆解:IIoT、工业互联网与工业4.0
网络
我是伪码农2 小时前
外卖餐具智能推荐
linux·服务器·前端
汤愈韬3 小时前
下一代防火墙通用原理
运维·服务器·网络·security
IMPYLH3 小时前
Linux 的 od 命令
linux·运维·服务器·bash
有代理ip5 小时前
网络隐私防护指南:代理服务与换 IP 工具的科学结合
网络·tcp/ip·web安全