【Linux】TCP协议

一. 传输层

端口号

  • 在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过netstat -n查看);
  • 认识知名端口号
    ssh服务器, 使用22端口
    ftp服务器, 使用21端口
    telnet服务器, 使用23端口
    http服务器, 使用80端口
    https服务器, 使用443
    我们在日常使用时应避免与这些端口号进行冲突,cat /etc/services指令可以查看这些端口号
  • 一个进程可以绑定多个端口号,一个端口号不能被多个进程绑定

netstat

用于查看网络状态的指令

常用选项:

n 拒绝显示别名,能显示数字的全部转化成数字

l 仅列出有在 Listen (监听) 的服務状态

p 显示建立相关链接的程序名

t (tcp)仅显示tcp相关选项

u (udp)仅显示udp相关选项

a (all)显示所有选项,默认不显示LISTEN相关

pidof

  • 场景:

当我们要批量化删除任务时,通过管道标准输入给kill命令要杀的pid时是从fd为0的文件读取,这样不行,kill命令要使用命令行参数,这样获取对应进程的pid很复杂,可以使用pidof来简化

  • 作用和用法:
    用于根据进程名快速查询对应的进程 ID
    常用选项:

-s 仅返回第一个匹配进程的 PID(默认返回所有匹配 PID)

-o 排除指定 PID 的进程(如 pidof -o 1234 nginx 排除 PID 为 1234 的 nginx 进程)

-x 包含通过脚本启动的进程(如 pidof -x test.sh 查找名为 test.sh 的脚本进程)

-c 仅显示与当前根目录相同的进程(需 root 权限,用于容器环境隔离)

使用场景:

通过 pidof 获取 PID 后,用 kill 批量终止进程(如关闭所有 python 进程):

正常终止(发送 TERM 信号)

kill $(pidof python)

强制终止(无响应进程,发送 KILL 信号)

sudo kill -9 $(pidof python)

其中pidof会返回多个同名进程id,这些同名进程多数为了满足并发处理任务需求,少数是异常场景导致的残留

二. UDP协议

  • 特点:
    UDP传输的过程类似于寄信
    无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
    不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
    面向数据报: 不能够灵活的控制读写数据的次数和数量
  • 面向数据报:
    应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;
    用UDP传输100个字节的数据:如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节;
  • UDP的缓冲区
    UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;(UDP 的 "发送缓冲区" 是 "极简临时暂存",不负责可靠传输,但能避免应用程序因硬件速度不匹配而直接阻塞;)
    UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;

UDP的socket也具有全双工特性,同时支持读写

  • UDP协议格式

    16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;如果校验和出错, 就会直接丢弃;也就是说一个UDP能传输的数据最大长度是64K(包含UDP首
    部).如果发送的数据量很大,那么得划分为一个个的64k数据块来发送

三. TCP协议

  • TCP全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制
  • 具有传输控制协议的原因就是具有发送缓冲区,应用层将数据交付下来后任务就完成了,后续工作与其无关,先将数据写入发送缓冲区中,什么时候发送由操作系统自主决定

1.TCP协议段格式

  • 4位首部长度中是包含报头选项的,基本单位是字节,表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是(二进制表示1111)15 * 4 = 60字节,最小长度为20字节(固定部分)
  • 16位窗口大小代表的是自己的接收缓冲区空间的大小,这里涉及到确认应答机制

确认应答机制

客户端和服务端基于TCP协议进行通信互发消息时,发送的是完整的TCP报文,携带了完整的tcp报头,当其中一方发送消息时,另一方收到之后需要返回一个消息来确认收到,这就是确认应答。对于发送方来讲,发送速度由对方的接收缓冲区中剩余空间大小决定(16位窗口大小),也就是流量控制!

但是不存在整个过程100%可靠的网络协议。当双方发送消息时存在一个问题,发送消息方收到了确认应答消息,那么发送确认应答消息的一方怎么知道自己发送的确认被对方收到了呢?总有一方的消息是没法应答的,没有应答的数据无法保证可靠性,所以最新的一条消息是没有应答的,无法保证发出去的消息完全可靠

但是局部上可以做到可靠,只要发送消息方能收到确认应答消息即可,接收方发送的确认应答消息不需要反馈,也没有意义。

捎带应答

就是在确认应答的基础,返回ack报文的同时一并发送数据,减少了通信次数,提高了效率

32位序号与32位确认序号

双方发送消息时,经过网络后,接收到的数据据顺序可能与发送数据顺序不一致,而造成乱序本身就是不可靠的一种,所以TCP协议会给发送的信息带上序号。

32位意味着 TCP 能对连续的 4GB 字节流进行编号。TCP 是面向字节流的协议,序列号本质是对 "字节流中每个字节" 的唯一标识,但 32 位的范围终究有限(4GB),当传输数据量超过此范围时,序列号会 "回绕"(从最大值回到 0)。

当收到发送信息的序号后,接收方还需要返回确认序号的消息。确认序号的序列是收到报文的序号+1.确认序号的意义,表示确认序号之前的数据我已经全部收到了,下一次发送,从确认序号指定的数字开始发送

  • 为什么需要序号和确认序号这两种序号?
    因为通信过程中,发送确认信号时可能也想发送数据以提高通信效率,而发送的信息又一定要包含tcp报头(可以不包含数据),所以就存在这两种序号,支持批量化发送序号也能确保批量化响应数据,这两种序号的存在还可以保证确认应答机制的信息不会乱序
  • 报文就是结构体数据,封装就是从结构体对象中把报头信息拷贝到数据前面就是封装
  • 重新理解序列号与确认序号:
    序号的生成逻辑:序号=基准值+数组下标,因为每一对连接内核都会为其维护一对tcp缓冲区,基准序号是发送缓冲区中第一个字节的TCP序号,数组下标代表缓冲区中数据的相对位置。
    确认序号-基准值=下次发送数据在缓冲区中的位置,这样一加一减的操作为了将抽象的tcp序号与发送缓冲区中的物理位置关联

6位标志位

tcp通信的时候收到的报文一定是有各种类型的,不同的类型决定了服务端要做不同的动作,接收方通过6个标记为来区分tcp报文的类型

  • ACK(Acknowledgment) :确认32位确认序号是否有效
    当 ACK=1 时:表示确认号字段有效,接收方通过确认号告知发送方已接收的据范围。
    当 ACK=0 时:表示确认号字段无效,此时报头中的确认号数值无意义通常忽略
  • SYN(Synchronize) : 请求建立连接; 我们把携带SYN标识的称为同步报文段
    作用是在连接建立阶段,告知对方我要发起连接,并同步你同步我的初始序列号(就是报头中32位序号与32位确认序号)
  • PSH(push) : 提示接收端应用程序立刻从TCP缓冲区把数据读走
    内核与应用层存在数据隔离,tcp接收缓冲区中的数据需要应用层手动读取,当应用层没有及时读取导致接收缓冲区写不下了,影响通信效率,这时通信时可以带PSH。注意: PSH 标志(推送位),仅能让内核 "立即将数据从 TCP 接收缓冲区提交到应用层的 socket 接收缓冲区"(仍在内核空间),无法跳过应用层的读取操作。最终数据还是需要应用层调用 recv 等接口,从 socket 接收缓冲区读取到用户空间。
  • RST(Reset) : 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
    作用是强制中断(重置)当前的 TCP 连接,用于处理连接中的异常情况或主动拒绝无效连接。当连接出现异常时,比如三次握手时,当客户端向服务器发送带ACK标志位的报头时,一旦发送客户端就认为建立连接成功,万一在传输过程中丢包,服务端没有收到确认应答信息,认为没有建立连接成功,当客户端向服务器再发送消息时发现是一个陌生的连接信息,这时候就要处理连接异常的情况,向客户端返回带Ret标志位的报头请求重连。tcp虽然保证可靠性,但是允许连接建立失败,可靠性是在建立连接成功后的通信过程中保证的
  • URG(Urgent) : 紧急指针是否有效
    该标志位用于判断紧急指针是否有效,紧急指针存储的是紧急数据相较于数据头的偏移量,大小为一个字节(因为不能有过多的紧急数据影响tcp通信整体的有序性),使用场景如下:
    当一台服务器和一个客户端进行通信时,可能是因为服务器正在执行一个周期长的任务或者资源不足导致卡顿,不回信息但是又没挂掉,客户端想要知道到底什么情况,如果发送普通信息就会进入服务器的接收缓冲区等待,等待历史发过的信息响应完后才到这条,那就没意义了,所以这种情况下想要立即返回就需要发送带URG紧急指针标记位的报头,服务器需要提供读取紧急数据+软件功能提供状态编号,才能支持读取紧急数据,紧急数据在应用层称为带外数据,应用层读写时send或recv接口中MSG_OOB选项代表应用层支持读取紧急数据
  • FIN(finish) : 通知对方, 本端要关闭了
    FIN 仅关闭 本端到对端的发送方向,不影响接收方向(发送 FIN 后仍可接收对方数据),相当于尽关闭写端

超时重传机制

  • 如何确定超时时间?

连接管理机制

  • 三次握手也可以看作四次握手
    因为服务端向客户端发送的信息是SYN+ACK,属于捎带应答,可以分开发送就变成了四次,那么为什么可以捎带应答呢?因为服务器的设计理念就是一直为客户端提供服务,有连接请求一定会同一连接。
    那么同理,四次挥手也可以看成三次挥手?只要将服务端发送的ACK和FIN合并即可,那么为什么不,因为断开连接是一方没有数据给对方发送了,关闭自己的写端但是还可以接收到对方数据,所以必须断开两次
  • 建立连接时不同握手次数的缺点
    服务器是一对多的,存在大量连接就需要管理,先描述再组织,通过数据结构来管理,维护连接也是有成本的,所以在建立连接时需要考虑到维护成本、以及谁来承担的问题。
  • 一次握手
    只要客户端给服务器发送连接请求,在无法确认双向都能正常通信并且序列号同步的情况下,服务端就要创建连接并维护起来,会严重浪费服务器资源,称为SYN洪水。还有一种情况,就是同时使用多态客户端给服务器发送连接请求,服务器都要接收建立连接,一旦资源不足就会挂掉,这一台台客户端称为肉机。
  • 两次握手
    当客户端给服务端发送连接请求时,服务器一旦收到就会建立连接并维护起来,同时给客户端发送确认应答信息,但是客户端要收到确认应答才会认为建立连接成功,所以万一服务端发送的ACK丢失,客户端就认为没有连接就不会发送消息,但服务端已经将连接建立好了,又没有信息,浪费服务资源,所以当连接失败的时候,维护连接的成本交由服务器承担了,由于服务器是一对多客户端,可能因为小部分客户端问题而影响了整个服务,这样不行
  • 三次握手
    双方都能有一次完整可靠的通信,以最少的握手次数验证了全双工特性。

    同时奇数次握手,可以确保一般情况下握手失败的连接成本是嫁接在client端的

2.状态验证(含结论)

accept的作用?

连接的建立成功和上层accept没有关系,三次握手是由双方操作系统自动完成的


使用的是Linux中lesson51的代码,只更改了TcpServer.hpp中部分,简化代码,只初始化套接字、绑定和打开监听状态,start中不使用accept来获取新连接

client和server连接建立不一致问题

tcp服务端在内核中会用特定的数据结构来维护已完成三次握手的连接,称为全连接队列,等待用户层通过accept系统调用来取出,其中listen的第二个参数backlog+1表示底层已经建立号的连接队列的最大长度。

而对未完成三次握手的连接(状态为SYN_RCVD),也会用半连接队列来维护起来,直到收到客户端的ACK确认,但是服务端不会长时间维护半连接队列中的节点

  • listen第二个参数为什么不能太长、为什么不能没有?
    当服务器资源紧张时,应用层通过系统调用接口在处理部分连接,剩下的连接在全连接队列中闲置了,还要花费额外资源来维护队列,如果过长的全连接队列会影响效率,但又不能没有,因为要随时做好服务器压力减轻、上一个连接处理完的准备,这时直接从半连接队列中拿去连接到应用层处理,这样能保存服务器高效允许,不会浪费服务器资源让其闲置,所以要合理设置全连接队列的长度

SYN_RCVD状态不会触发RST是因为服务器接收到了ack但是自己把它丢弃掉了,而不是ack自己丢失需要重新连接

TIME_WAIT状态

  • 主动断开连接的一方,在四次挥手完成后,要进入time_wait状态,等待若干时长之后,自动释放。
  • TIME_WAIT等待时间:
    TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.MSL是文件在网络中的最大存在时间,作用是限制TCP报文在网络中的存活时间,MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;可以用echo 时间 > /proc...来更改MSL时间
  • 为什么等待时间是2MSL
    2MSL刚好对应通信来回的过程,TIME_WAIT状态要等待一会才closed,是因为要等历史通信的数据在网络中被对方接收或者丢弃(若断开连接后立马重连,用的端口都一样,那么历史残留信息可能会影响当前通信),因为存在超时补发机制所以一般这个等待都是为了丢弃历史通信数据,在网络中传输时,报头中的序号起始位置是随机的就是为了避免历史数据残留的问题,避免使用相同的序号来传数据
  • 使用场景:
    1.连接关闭的2MSL等待,主动关闭连接的一方发送最后一个 ACK 后,会进入 TIME-WAIT 状态并等待 2 倍 MSL。等待 2MSL 的本质:1 倍 MSL 确保对端未收到 ACK 时,能在超时前重发 FIN 报文;另 1 倍 MSL 确保本端之前发送的所有 "滞留报文" 都已过期失效,避免新连接复用端口时收到这些旧报文。让通信双方的历史数据得以消散,并且让四次挥手具有较好的容错性
    2.贯穿通信全程的 "报文过期丢弃",这个规则在 TCP 连接建立、数据传输阶段同样生效:无论连接处于 ESTABLISHED(数据传输)还是 SYN_SENT(三次握手)状态,只要 TCP 报文在网络中传输的时间超过 MSL,就会被路由器等网络设备根据 "生存时间" 规则丢弃。
  • TIME_WAIT状态下引起bind失败验证

    观察得,关闭服务器重启后,若使用原端口号会显示已经使用,那是因为服务器的tcp连接还没完全断开,此时还在等待2MSL时间清除历史数据和保证四次挥手的可靠性,所以此时不允许用原端口号重新监听,为了避免端口号和TIME_WAIT占用的链接重复,可以使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符

sockfd:套接字描述符(由 socket() 创建)。

level:选项所属的协议层级,常见值:

SOL_SOCKET:通用套接字层(如缓冲区大小、端口重用)。

IPPROTO_IP:IP 层(如 TTL、多播)。

IPPROTO_TCP:TCP 层(如 Nagle 算法、超时时间)。

optname:具体选项名(如 SO_REUSEADDR、TCP_NODELAY)。

optval:指向选项值的指针。

optlen:optval 的长度。

在创建套接字后使用就可以解决重启服务器时端口复用的问题

  • 注意:
    1.Fin_WAIT或CLOSE_WAIT想要看到现象,必须从底层全连接队列中将连接获取到应用层中,否则会被直接释放·
    2.客户端重启时不会受占用端口号影响,是因为客户端的端口号是由系统随机绑定的,而服务器每次都必须一样的

流量控制

  • 接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应. 从通信开始就要进行流量控制。因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
  • 第一次通信的时候,怎么保证数据量是合理的?
    三次握手阶段,不仅仅是握手建立连接和验证全双工特性,双发还交换了报文,已经协商了双方的接收能力
  • 第三次的时候可以携带数据发送了
    三次握手中前两次,双方交换了报头中窗口大小的信息,明确对方剩余缓冲区的大小,所以第三次握手时可以携带信息发送了(捎带应答)
  • 流量控制,体现了可靠性,也保证了效率
    因为有流量控制,所以最大程度避免了一直传输导致丢包不断重发的问题,这样控制的好处就是尽量确保每一次经过网络传输的数据都被有效接收使用,不浪费网络资源
  • 窗口大小字段越大, 说明网络的吞吐量越高;接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;发送端接受到这个窗口之后, 就会减慢自己的发送速度;如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
  • 16位窗口字段存放了窗口大小信息,16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
    实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;左移 M 位等价于乘以 2 ^M次方

滑动窗口

  • 确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.所以采用一次发送多条数据的方法可大大提高性能,就是将多个信息段的等待时间重叠在一起了

  • 因为有滑动窗口的区域,才可以一次向对方发送大量的tcp报文

  • 1.滑动窗口在哪里?是发送缓冲区中的一部分

  • 2.滑动窗口的范围大小,是对方的接受窗口大小(目前)

  • 3.如何理解区域划分?通过指针/下标来进行区域划分,通过双指针来维护,窗口滑动本质上就是指针右移

  • TCP 发送缓冲区分为三个区域,通过双指针(Snd_una、Snd_nxt)的动态移动实现 "循环复用",新的已发送已确认数据不会占用额外空间,而是通过指针右移和内存回收实现区域的 "动态流转"

  • 已发送已确认区:

    扩大:仅当收到 ACK 时,Snd_una 右移 → 区域扩大。

    缩小(回收):内核定期将已确认区标记为空闲,供新数据写入 → 区域 "逻辑上消失",内存被复用。

  • 已发送未确认区:

    作用:存储已经发送的数据但未收到应答的区域,用于超时重传

    扩大:发送数据时,Snd_nxt 右移 → 区域扩大(从待发送区 "夺取" 空间)。

    缩小:收到 ACK 时,Snd_una 右移 → 区域缩小(部分数据进入已确认区)

  • 待发送区:

    扩大:应用层写入新数据 → 区域扩大(复用已确认区回收的空闲空间,或占用缓冲区剩余空间)。

    缩小:发送数据时,Snd_nxt 右移 → 区域缩小(数据进入未确认区)。

  • 总结:

    整个发送缓冲区是一块固定大小的连续内存(创建套接字时指定或内核默认),三个区域通过指针移动 "动态流转",而非物理上的分割。已确认数据的内存会被循环覆盖(新数据直接写入旧数据的位置),因此无需额外空间存储新的已确认数据,实现高效的内存复用。滑动窗口不会在发送缓冲区中越界,tcp采用了类似环状的算法

  • 数据包抵达,ACK被丢弃

    确认序号的定义是保证确认序号之前的报文都收到了,这样可以保证滑动窗口线性的连续向后更新,不会出现跳跃的情况,所以允许少量的ack丢失

  • 数据包丢失如何重传:

    当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001" 一样;如果发送端主机连续三次收到了同样一个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已

    经收到了, 被放到了接收端操作系统内核的接收缓冲区中;这种机制被称为 "高速重发控制"(也叫 "快重传")

  • 流量控制是通过滑动窗口实现的,假设滑动窗口的两个指针为int start和int end,

    start根据确认序号设置,end=确认序号+min(窗口大小,有效数据,拥塞窗口),拥塞窗口下文会讲,end指针除了受对方缓冲区剩余空间大小影响,还有受本身数据有效范围影响,否则多出来的滑动窗口大小也没有意义

  • 为什么有了快重传还要超时重传?快重传适用于多数据同时发送的场景,能通过接收方重复发送ack来立马判断是否丢包,用来提高效率的,而超时重传是用来兜底的,能覆盖所有丢包的场景,但是效率不如快重传,二者相辅相成。

延迟应答

  • 已知发送方一次发送更多的数据发送的效率就越高,代表接收方一次给发送方通告一个更大的窗口大小,发送方的滑动窗口就越大一次能发送更多数据。如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小,因为窗口越大, 网络吞吐量就越大, 传输效率就越高.我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。所以收到发送方的数据后不立马返回ack应答,给上层充分的实践来取走数据,这时候再返回给发送方一个更大的窗口,这就是延迟应答,收到报文不着急应答其实是一种博概率的做法,因为网络协议栈是严格分层的,你也不能确定在延迟的这段时间内上层一定会取走数据,接受方窗口一定会变大,但经过实验这样延迟应答的效率确实比立马返回的效率高。所以在我们自己写服务器时,比较推荐的做法是每次通过read和recv尽快的把数据全部从内核中拿上来,就是为了延迟应答时接收方能返回一个更大的窗口提高通信效率
  • 延迟应答的策略:
    数量限制: 每隔N个包就应答一次;
    时间限制: 超过最大延迟时间就应答一次;
    具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;

拥塞控制

  • 背景引入
    如果发送数据出现问题,不仅仅是对方主机出现了问题,也可能是网络出现了问题,通信时出现了少量的丢包tcp会判定为常规情况,如果出现大量的丢包会判定网络出现了问题,就好比一个班考试如果一个人挂科那么会认为是自己没学好,如果只有一个人及格,那么肯定不是学生的问题。网络出现问题一般是硬件设备出问题,或者数据量太大引起阻塞。tcp可通过自身的可靠性机制(重传、确认)和拥塞控制机制,主动适应并尽可能在逻辑通信层修复网络问题,当然,TCP 的能力是有限的(比如无法直接修复物理链路故障),网络的物理维护(如光纤、路由器故障)需要专业人员
  • 当通信双方都出现了大量的数据丢包问题,tcp会判断网络出问题了(拥塞了),发送方就不能对报文进行超时重发,这样会加重网络的拥塞状态,因为网络资源是共享的,有很多主机在同时连接使用,如果每台主机都这么想那么可能导致网络直接瘫痪。TCP 协议通过 "以丢包为拥塞信号,统一执行降速行为" 的机制,让多台主机在网络拥塞时形成了 "共同应对策略"(共识),从而避免网络因无序竞争而崩溃,保障了互联网的可扩展性和稳定性。
  • 拥塞窗口:
    拥塞窗口是发送端为了避免网络拥塞,自主计算并动态调整的发送速率限制,存储在发送端内核的 TCP 控制块(TCB)中。它的调整完全基于发送端对网络拥塞的感知(如丢包、延迟),通过拥塞控制算法(如 Reno、Cubic)实现,不会通过 TCP 报文传递给接收端。
  • 发送端滑动窗口大小=min(接收方窗口大小,拥塞窗口)
    前者考虑的是对方主机的接受能力,后者考虑的是动态的、网络的接收能力
    若网络无拥塞,实际发送端窗口由接收窗口决定(流量控制主导);
    若网络拥塞,实际发送端窗口由拥塞窗口决定(拥塞控制主导)。
  • tcp的慢启动机制
    先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;. "慢启动" 只是指初使时慢, 但是增长速度非常快.发送开始的时候, 定义拥塞窗口大小为1,每次收到一个ACK应答, 拥塞窗口加1,由于一次首发信息可能存在多个ack应答,所以拥塞窗口呈现指数级增长。
    慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长

    当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
    在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
  • 总结:
    每台主机根据自身丢包情况和网络状态来进行拥塞控制,不是说同一时间所有主机一起进行,少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞; 当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降; 拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.

面向字节流

  • 创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区,每一个连接都会维护一对缓冲区,由于缓冲区的存在, TCP程序的读和写不需要一一匹配,例如:
    写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
    UDP就完全不同,面向数据报,发送一次就必须对应接收一次,
  • tcp协议在内核中实现,应用层用户调用write接口将数据写入tcp的发送缓冲区,剩下的发送控制全权由tcp负责,用户可能write了多个请求,但是tcp只认识多个字节数据,将多个请求都看成一串字节数据,tcp不关心上层协议,不关心上层报文格式,只有字节的概念
  • 应用层调用read将数据从tcp接收缓冲区读取上来时,看到的是一串字节流数据,所以要进行反序列化将字节流变成一个个的请求

粘包问题

  • 粘包问题中的 "包" , 是指的应用层的数据包
  • 在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段. 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.站在应用层的角度, 看到的只是一串连续的字节数据.那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.
  • 用户层可以通过定协议来解决粘包问题,这也是之前写过的encode和decode给数据添加报头和解析报头的作用
  • 解决粘包问题的方法:在应用层通过协议,明确报文和报文之间的边界
    1.定长报文:每次从缓冲区按固定大小读取即可
    2.使用特殊字符:在数据包之间约定使用,传输层发送数据时在包尾添加终止字符,应用层读取数据时解析分隔符,注意分隔符不要和正文里面的字符冲突了(转义)
    3.使用自描述字段+定长报头:数据正文前存在固定长度报头,每次先按固定长度读取报头,再从报头中获取正文大小然后读取
    4.使用自描述字段+特殊字符:依旧通过特殊字符来判断报尾,还增加自描述字段添加类型长度来避免特殊字符的误判
  • 对于UDP协议来说不存在粘包问题,如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.
    站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况.

TCP异常情况

  • 套接字是和文件直接相关的(本质就是文件描述符),文件的生命周期是随进程(每个进程都有独立的文件描述符表)
  • 异常情况:
    1.进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
    2.机器重启: 和进程终止的情况相同.
    3.机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset.
    当长时间没有写入操作时还会定期询问对方是否还在,如果对方不在 , TCP自己也内置了一个保活定时器 , 也会把连接释放.
    另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ 断线之后,也会定期尝试重新连接.

四. 宏观理解文件和socket的关系

网络文件的文件结构体中的指针会指向套接字

  • 重点解析:
  • 在 Linux 内核中,Socket 和普通文件(如磁盘文件)都通过 struct file 进行统一管理,这是 "万物皆文件" 设计哲学的直接体现(都是用read、write接口),Socket 本质是一种特殊文件,通过内核结构体的嵌套与指针关联,实现了文件系统接口与网络协议栈的打通
  • struct socket中关键字段:
    f_ops:Socket 的f_ops指向网络专属的操作函数如(sock_read_iter/sock_write_iter),而非普通文件的磁盘操作函数;
    private_data:Socket 的private_data指向 struct socket结构体 ,从而将 "文件操作" 与 "网络协议" 关联起来。
  • 为了支持网络协议的复杂性,Socket 在struct file的基础上,通过两层结构体实现网络功能的扩展
    struct socket:网络抽象层

它是 Socket 的 "管理层",包含 Socket 的类型(SOCK_STREAM/SOCK_DGRAM)、状态(如连接 / 监听),以及指向传输层协议核心结构struct sock的指针。图中圈出的socket_state、type、ops等字段,就是用来标识 Socket 的网络属性(如 TCP 还是 UDP、是否已连接)

struct sock:传输层协议核心

它是 Socket 的 "实干层",承载了 TCP/UDP 的协议细节(如 TCP 的滑动窗口、拥塞控制、连接状态机;UDP 的无连接缓冲区管理)。图中提到的sk_receive_queue(接收队列)、sk_write_queue(发送队列),就是用来存储网络数据包的缓冲区,对应 TCP 的收发流程。

五.TCP总结

  • tcp复杂的原因是要保证可靠性又要尽可能提高效率

可靠性:

校验和

序列号(按序到达)

确认应答

超时重发

连接管理

流量控制

拥塞控制

提高性能:

滑动窗口

快速重传

延迟应答

捎带应答

其他:

定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等

  • 基于TCP应用层协议
    HTTP
    HTTPS
    SSH
    Telnet
    FTP
    SMTP
    当然, 也包括自己写TCP程序时自定义的应用层协议
  • tcp和udp对比
    TCP是可靠连接, 但不是TCP一定就优于UDP, TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较
    TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
    UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等、广播;
  • 用UDP实现可靠传输(经典面试题)
    通过分场景,如果对可靠性要求很高就不用udp而用tcp了,实现的核心就是往tcp的可靠性方法上靠,例如:引入序列号, 保证数据顺序、引入确认应答, 确保对端收到了数据、引入超时重传, 如果隔一段时间没有应答, 就重发数据;
相关推荐
大树8812 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠12 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质12 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush412 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52013 小时前
Linux 11 动态监控指令top
linux
Inhand陈工13 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智14 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest14 小时前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
不会C语言的男孩14 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_14 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化