网络原理 - TCP/IP(一)

目录

[1. 应用层:用户与网络的 "交互窗口"](#1. 应用层:用户与网络的 “交互窗口”)

[1.1 应用层协议:规范交互的 "通用语言"](#1.1 应用层协议:规范交互的 “通用语言”)

[1.2 自定义协议:适配特殊需求的 "专属规则"](#1.2 自定义协议:适配特殊需求的 “专属规则”)

[1.3 应用层数据格式:让数据 "说得明白"](#1.3 应用层数据格式:让数据 “说得明白”)

[1.3.1 XML:结构化但繁琐的 "老大哥"](#1.3.1 XML:结构化但繁琐的 “老大哥”)

[1.3.2 JSON:轻量高效的 "新主流"](#1.3.2 JSON:轻量高效的 “新主流”)

[1.3.3 Protobuffer:极致高效的 "二进制选手"](#1.3.3 Protobuffer:极致高效的 “二进制选手”)

[2. 传输层:数据传输的 "交通指挥官"](#2. 传输层:数据传输的 “交通指挥官”)

[2.1 再谈端口号:应用程序的 "门牌号"](#2.1 再谈端口号:应用程序的 “门牌号”)

[2.1.1 端口号的核心作用](#2.1.1 端口号的核心作用)

[2.1.2 唯一标识一次通信的 "五元组"](#2.1.2 唯一标识一次通信的 “五元组”)

[2.1.3 端口号的范围划分](#2.1.3 端口号的范围划分)

[2.1.4 关于端口号的两个常见问题](#2.1.4 关于端口号的两个常见问题)

[2.2 UDP 协议:简单直接的 "快递服务"](#2.2 UDP 协议:简单直接的 “快递服务”)

[2.2.1 UDP 协议的格式](#2.2.1 UDP 协议的格式)

[2.22 UDP 的特点](#2.22 UDP 的特点)

[2.2.3 UDP 的使用注意事项](#2.2.3 UDP 的使用注意事项)

[2.2.4 基于 UDP 的应用层协议](#2.2.4 基于 UDP 的应用层协议)

[2.3 TCP 协议:可靠传输的 "幕后管家"](#2.3 TCP 协议:可靠传输的 “幕后管家”)

[2.3.1 TCP 协议段格式:"快递面单" 的复杂设计](#2.3.1 TCP 协议段格式:“快递面单” 的复杂设计)

[2.3.2 确认应答(ACK):"收到请回复" 的保障机制](#2.3.2 确认应答(ACK):“收到请回复” 的保障机制)

[2.3.3 超时重传:"没收到回复就重试" 的保险](#2.3.3 超时重传:“没收到回复就重试” 的保险)

[2.3.4 连接管理:"三次握手建连接,四次挥手断连接"](#2.3.4 连接管理:“三次握手建连接,四次挥手断连接”)

[2.3.5 滑动窗口:"批量发货,提升效率" 的优化](#2.3.5 滑动窗口:“批量发货,提升效率” 的优化)

[2.3.6 流量控制:"别发太猛,我处理不过来"](#2.3.6 流量控制:“别发太猛,我处理不过来”)

[2.3.7 拥塞控制:"别把网络搞堵了" 的全局优化](#2.3.7 拥塞控制:“别把网络搞堵了” 的全局优化)

[2.3.8 延迟应答:"攒一波回复,减少交互"](#2.3.8 延迟应答:“攒一波回复,减少交互”)

[2.3.9 捎带应答:"顺手回复,一石二鸟"](#2.3.9 捎带应答:“顺手回复,一石二鸟”)

[2.3.10 面向字节流:"数据无边界,但有序"](#2.3.10 面向字节流:“数据无边界,但有序”)

[2.3.11 粘包问题:"数据粘在一起,怎么拆分?"](#2.3.11 粘包问题:“数据粘在一起,怎么拆分?”)

[2.3.12 异常情况:"遇到问题,如何处理?"](#2.3.12 异常情况:“遇到问题,如何处理?”)

[2.3.13基于 TCP 的应用层协议](#2.3.13基于 TCP 的应用层协议)

[2.4 TCP/UDP对比](#2.4 TCP/UDP对比)


在我们每天刷手机、逛网页、发消息的背后,隐藏着一套复杂而精妙的网络规则 ------TCP/IP 协议栈。从今天开始,我们就一步步揭开它的面纱,先从最贴近我们的应用层说起,再深入传输层的核心知识:端口号和 UDP / TCP 协议。

1. 应用层:用户与网络的 "交互窗口"

应用层是网络体系里最贴近我们的 "用户接口",像日常用的浏览器、聊天软件、办公工具,它们实现核心功能的逻辑都扎根于此。这一层不用操心数据在网络里咋传输,专注处理用户业务需求 ------ 你想翻译文字,它负责完成翻译逻辑;你要在线协作,它就保障文档编辑、同步,是用户和网络交互的直接 "桥梁"。

1.1 应用层协议:规范交互的 "通用语言"

应用层靠 协议 让不同应用有序通信,这些协议是应用间交流的 "共同规则"。简单说,协议就是一套 "约定":既可以是像 HTTP(网页通信)、DNS(域名解析) 这样的通用协议(相当于行业通用 "规则手册" ),也能是程序员根据场景自定义的 "专属规则"。后续我们会专门讲解应用层的具体协议(比如 HTTP、DNS 等),这里先记住:所有你能直接感知到的网络功能,都离不开应用层的支撑。

你可以把协议理解成 "对话脚本"------ 浏览器和服务器用 HTTP 交流,就知道啥时候说 "要资源"、啥时候回 "给你内容";要是通用协议满足不了需求(比如企业内部系统要特殊数据交互 ),就该 自定义协议 登场了。

1.2 自定义协议:适配特殊需求的 "专属规则"

碰到通用协议覆盖不到的场景(比如企业内部生产管理、定制化系统 ),就得自己设计 自定义协议。核心要约定两件事:

  1. 交互什么信息:客户端(像 APP、网页 )和服务器之间,要传哪些数据?比如工厂监控系统,可能要传 "设备编号、运行参数、故障代码";
  2. 数据格式咋组织 :这些信息用啥格式传?是用简单的 "文本分隔符"(比如用 \n 分割不同字段 ),还是用更规范的 XML、JSON?

**举个例子:**做外卖 APP 时,打开 "附近商家" 页面,APP(客户端 )得告诉服务器 "用户位置、想找啥类型商家",服务器再返回 "商家列表、评分、地址"------ 这些 "传什么、怎么传",要么是产品逻辑定的,要么是程序员结合场景设计的,这就是自定义协议的过程。

1.3 应用层数据格式:让数据 "说得明白"

不管是通用协议还是自定义协议,传递数据时都得有统一格式

常见的有这几种:

1.3.1 XML:结构化但繁琐的 "老大哥"

XML(可扩展标记语言 )用 标签嵌套 组织数据,结构清晰、可读性强,适合描述复杂信息。

比如一份系统配置:

XML 复制代码
<system-config>
  <server>
    <ip>192.168.1.100</ip>
    <port>8080</port>
  </server>
  <database>
    <name>myDB</name>
    <username>admin</username>
  </database>
</system-config>

标签层层包裹,数据关系一目了然,但缺点也明显 ------ 语法繁琐、冗余大(标签占空间 ),传输大文件时效率低,适合对可读性要求高的场景(比如配置文件 )。

1.3.2 JSON:轻量高效的 "新主流"

JSON(JavaScript 对象表示法 )靠 键值对 存数据,语法简洁、解析方便,成了 Web 开发的 "首选格式"。

比如前端给后端传用户登录信息:

javascript 复制代码
{
  "username": "csdn_user",
  "password": "123456",
  "remember_me": true
}

不管是网页和服务器交互,还是手机 App 传数据,JSON 因为 "轻量好解析 ",几乎成了标配。和 XML 比,它少了冗余标签,传输效率更高

1.3.3 Protobuffer:极致高效的 "二进制选手"

Protobuffer 是 Google 搞的 二进制数据格式 ,特点是 体积小、解析快 ,但需要先定义 "数据模板"(写 .proto 文件 )。

比如定义用户信息:

javascript 复制代码
syntax = "proto3";
message User {
  string username = 1;  // 字段1:用户名
  int32 age = 2;       // 字段2:年龄
}

它传输的是二进制数据,比 JSON、XML 更省带宽,适合对性能要求极高的场景(比如游戏实时同步、分布式系统通信 )。不过它有学习成本(得先定义模板 ),所以常用在 "追求极致效率" 的场景里。

  • 通用场景(网页、App 常规交互 ) :优先用 JSON,轻量、易解析,开发成本低;
  • 需要强结构化、可读性(配置文件、旧系统兼容 ) :选 XML,标签嵌套清晰,但别在大文件传输里用;
  • 极致性能场景(游戏、高频通信 ) :上 Protobuffer,二进制传输省带宽,但要接受 "先定义模板" 的额外工作;
  • 特殊业务需求(企业内部系统、定制化逻辑 ) :搞 自定义协议,灵活适配,但要做好 "规则设计",别让格式太混乱。

2. 传输层:数据传输的 "交通指挥官"

如果说应用层是直接服务用户的 "窗口",那传输层就是负责把数据从一个应用准确送到另一个应用的 "交通枢纽"。它的核心任务是确保数据能够可靠或高效地在发送端和接收端的应用程序之间传输。而要完成这个任务,就离不开两个重要的 "工具":端口号和传输协议。

2.1 再谈端口号:应用程序的 "门牌号"

想象一下,你要给朋友寄一个包裹,只知道他家的地址(就像网络中的 IP 地址)还不够,因为一栋楼里可能有很多住户,你需要知道具体的门牌号才能确保包裹准确送达。在网络中,端口号就相当于应用程序的 "门牌号"。

2.1.1 端口号的核心作用

一台电脑上可能同时运行着多个网络应用,比如浏览器、聊天软件、下载工具等。当数据通过网络到达这台电脑时,操作系统需要知道该把这些数据交给哪个应用程序处理,这时候端口号就派上了用场。

例如,你的电脑(IP 地址:172.23.12.14)同时打开了浏览器和 FTP 客户端:

  • 浏览器要和网页服务器的 80 端口通信(因为 HTTP 协议默认使用 80 端口);
  • FTP 客户端要和 FTP 服务器的 21 端口通信(FTP 协议默认使用 21 端口);
  • 操作系统通过不同的端口号,就能准确地把来自服务器 80 端口的数据交给浏览器,把来自 21 端口的数据交给 FTP 客户端。

2.1.2 唯一标识一次通信的 "五元组"

在 TCP/IP 协议中,一次完整的通信(比如你用浏览器访问某个网站)是通过 "五元组" 来唯一确定的,它就像通信的 "身份证",包括:

  • 源 IP 地址:发送数据的主机 IP(比如你的电脑 IP:192.168.1.100);
  • 源端口号:发送数据的应用程序端口(比如浏览器临时使用的 56789 端口);
  • 目的 IP 地址:接收数据的主机 IP(比如网站服务器 IP:123.123.123.123);
  • 目的端口号:接收数据的应用程序端口(比如网站服务器的 80 端口);
  • 协议号:使用的传输协议(比如 TCP 协议的协议号是 6)。

通过这五个信息,网络中的设备就能精准地识别每一次通信,不会出现数据错乱的情况。在电脑上,我们可以通过netstat -n命令查看当前的通信五元组信息。

2.1.3 端口号的范围划分

端口号是一个 16 位的数字,范围从 0 到 65535,根据用途可以分为两类:

  • 知名端口号(0-1023):这些端口号是 "标准化" 的,被一些广泛使用的应用层协议固定占用。比如:

    • SSH(远程登录)协议使用 22 端口;
    • FTP(文件传输)协议使用 21 端口;
    • Telnet(远程终端)协议使用 23 端口;
    • HTTP(超文本传输)协议使用 80 端口;
    • HTTPS(加密的超文本传输)协议使用 443 端口。

我们自己开发程序时,要避开这些端口号,否则会和系统中的标准服务发生冲突。

  • 动态分配端口号(1024-65535):这些端口号主要供客户端应用程序临时使用。当你打开一个客户端程序(比如浏览器),操作系统会从这个范围中随机分配一个端口号作为 "源端口",当程序关闭后,这个端口号会被释放,以便下次分配给其他程序。

2.1.4 关于端口号的两个常见问题

  1. 一个进程可以绑定多个端口号吗?
    可以。比如一台服务器上的监控程序,可能需要同时监听多个端口,以便监控不同的服务,它可以通过绑定多个端口来实现这一功能。

  2. 一个端口号可以被多个进程绑定吗?
    通常情况下不可以。端口号就像门牌号,一个门牌号不能同时属于多个住户。如果两个进程试图绑定同一个端口号,操作系统会提示 "端口已被占用" 的错误。不过,在一些特殊的网络架构(如负载均衡)中,通过特殊的配置可以实现多个进程 "共享" 一个端口号,但这并不是默认情况。

2.2 UDP 协议:简单直接的 "快递服务"

传输层有两大核心协议:UDP 和 TCP。UDP(用户数据报协议)是其中相对简单的一个,它就像一种 "普通快递服务",简单直接,但不保证数据一定能送达。

2.2.1 UDP 协议的格式

UDP 的协议首部非常简洁,只有 8 个字节,包含四个部分:

  • 16 位源端口号:发送数据的应用程序端口号;
  • 16 位目的端口号:接收数据的应用程序端口号;
  • 16 位 UDP 长度:整个 UDP 数据报(包括首部和数据)的总长度,最大为 65535 字节(近似值:64KB)一旦数据报的长度超出64KB,此时可能导致数据出现截断;
  • 16 位 UDP 检验和:用于检查数据在传输过程中是否损坏,如果检验失败,数据会被直接丢弃。

|---------|----------|----------|-----------|-------|
| 16位源端口号 | 16位目的端口号 | 16位UDP长度 | 16位UDP检验和 | 数据 |
| UDP报头 |||| UDP载荷 |

这种简洁的格式让 UDP 的处理效率很高,开销也很小。

2.22 UDP 的特点

  1. 无连接:使用 UDP 发送数据时,不需要像打电话那样先 "拨号建立连接",只要知道对方的 IP 地址和端口号,就可以直接发送数据。就像寄普通快递,你不需要提前通知收件人,填好地址直接投递即可。

  2. 不可靠:UDP 没有确认机制和重传机制。如果数据在传输过程中因为网络拥堵、故障等原因丢失了,UDP 不会通知发送方,也不会重新发送数据。这就像普通快递如果丢失了,快递公司可能不会主动告诉你,除非你自己查询。

  3. 面向数据报:应用层交给 UDP 多长的报文,UDP 就原样发送,既不会拆分,也不会合并。比如,应用层一次给 UDP 100 字节的数据,UDP 就封装成一个 100 字节的数据包发送;接收端必须一次接收完整的 100 字节,不能分 10 次每次接收 10 字节。这就像你寄一个 10 斤的包裹,快递不会把它拆成 10 个 1 斤的小包裹,收件人也必须一次把整个包裹取走。

2.2.3 UDP 的使用注意事项

由于 UDP 协议首部中 "UDP 长度" 字段是 16 位,这意味着整个 UDP 数据报(包括首部和数据)的最大长度是 65535 字节,去掉 8 字节的首部,UDP 能传输的最大数据量约为 64KB。

在当今的网络环境中,64KB 的数据量往往满足不了需求(比如传输一个大文件)。这时候,就需要在应用层手动处理:把大文件拆分成多个不超过 64KB 的小数据包,分别发送,接收端收到后再重新拼接成完整的文件。

2.2.4 基于 UDP 的应用层协议

虽然 UDP 不保证可靠性,但它凭借高效、低延迟的特点,被很多重要的应用层协议采用:

  • DNS(域名系统):负责把域名(如www.baidu.com)转换为 IP 地址,每次查询的数据量小,使用 UDP 能快速完成查询。
  • DHCP(动态主机配置协议):新设备接入网络时,通过 DHCP 自动获取 IP 地址等网络配置信息,需要快速响应,UDP 是合适的选择。
  • TFTP(简单文件传输协议):在局域网内传输小文件,追求简单高效,UDP 能满足需求。
  • NFS(网络文件系统):用于在局域网内共享文件,对传输速度有一定要求,UDP 能提供较好的性能。

当然,我们自己开发 UDP 程序时,也可以根据需求自定义应用层协议。

2.3 TCP 协议:可靠传输的 "幕后管家"

TCP全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制;

2.3.1 TCP 协议段格式:"快递面单" 的复杂设计

TCP 协议段(也叫 "TCP 报文段")的格式比 UDP 复杂得多,这是因为它要实现 "可靠传输",需要更多控制信息。我们把 TCP 报文段想象成 "带复杂条款的快递面单",每个字段都有特殊作用:

**16 位源端口号:**标识发送数据的应用进程,告知接收方 "数据从哪个进程来",用于区分同一主机上不同应用的网络数据。

**16 位目的端口号:**标识接收数据的应用进程,告知接收方 "数据要到哪个进程去",让接收端准确将数据交付对应应用。

**32 位序号:**TCP 是面向字节流传输,序号对发送的字节流编号,确保数据按序重组,解决网络传输乱序问题,比如发送数据段 "1 - 1000 字节",序号字段填起始编号,接收方依序号拼接。

**32 位确认序号:**期望收到对方下一个报文段的第一个数据字节的序号,用于确认已收到此前数据,实现可靠传输的 "确认机制",若收到序号 1-1000 数据,确认序号填 1001 ,告知对方期待从该序号继续发。

**4 位首部长度:**表示 TCP 首部占多少个 32 位(4 字节)块,用于接收方解析报文时,区分首部和数据边界,最大为 15,即首部最大 15×4 = 60 字节(含选项时变长)。

保留(6 位): 为未来扩展预留,当前无实际功能,默认置 0,避免新功能与现有实现冲突。

6 位标志位(URG、ACK、PSH、RST、SYN、FIN)

  • URG :紧急指针是否有效,为 1 时,紧急指针字段标识紧急数据在报文段中的位置,优先处理。
  • ACK:确认序号是否有效,连接建立后一般恒为 1,用于确认收到数据。
  • PSH:提示接收端应用程序 "立刻从 TCP 缓冲区读走数据",无需等缓冲区满,加快交互。
  • RST :要求重新建立连接,即 "复位报文段 ",网络异常(如连接超时、端口未监听)时,发送带 RST 的报文重置连接。
  • SYN :请求建立连接,"同步报文段" 标志,三次握手时,发起方发 SYN = 1 的报文,协商初始序号。
  • FIN:通知对方 "本端要关闭连接","结束报文段" 标志,四次挥手时,主动关闭方发 FIN=1 报文,告知对方不再发数据。

**16 位窗口大小:**用于 TCP 流量控制,接收方通过此字段告知发送方 "自己的接收缓冲区剩余容量",发送方据此调整发送速率,避免接收方缓冲区溢出。

**16 位检验和:**发送端用 CRC 校验算法,对 TCP 首部、数据部分计算校验值;接收端重新计算校验,若不一致则认为数据传输出错,丢弃或重传,保障数据完整性。

**16 位紧急指针:**仅当 URG=1 时有效,标识紧急数据在报文段中的偏移量,配合 URG 优先处理紧急内容(如远程登录的中断指令)。

**选项(如果有):**可选字段,用于实现额外功能(如协商最大报文段长度 MSS、时间戳等),非必选,无选项时首部长度由 4位首部长度 体现(不超 60 字节)。

**数据(如果有):**承载应用层数据(如 HTTP 报文、文件内容等),TCP 作为传输层协议,最终目的是可靠交付这部分数据到应用进程 。

2.3.2 确认应答(ACK):"收到请回复" 的保障机制

TCP 的可靠传输基础就是 "确认应答"------ 发送方发数据后,必须收到接收方的 "确认回复(ACK)",才认为数据成功送达。如果没收到,就重新发送。

流程拆解:

  1. 发送方:发数据(带 Seq=X)→ 启动 "定时器"→ 等待 ACK
  2. 接收方:收到数据→ 回复 ACK(Ack=X+1)
  3. 发送方:收到 ACK→ 关闭定时器→ 继续发下一段数据

如果发送方定时器超时(比如网络拥堵,ACK 丢了),就重新发送数据。这就像你给朋友发消息:"帮我取个快递"(Seq=100),然后盯着手机等回复。如果朋友回复 "收到"(Ack=101),你就放心了;如果半天没回复(超时),你会再发一遍 "帮我取个快递"。

关键点:

  • ACK 是 "累积确认":接收方不需要对每个字节确认,而是确认 "到某个序列号为止的所有数据都收到了"。比如收到 Seq=100、200、300 的数据,直接回复 Ack=301,代表 "100-300 都收到,下次发 301"。
  • 超时时间动态调整:网络情况复杂,超时时间不能固定(比如 Wi-Fi 很快,4G 可能慢)。TCP 会根据网络延迟动态计算超时时间(类似 "堵车时多等一会儿,不堵车少等")。

2.3.3 超时重传:"没收到回复就重试" 的保险

虽然有确认应答,但网络是不可靠的 ------ 数据可能丢包,ACK 也可能丢包。这时候,超时重传就是 TCP 的 "保险措施":

两种丢包场景及处理:

  1. 数据丢包:发送方发 seq=1000 的数据→ 超时没收到 ACK→ 重传 Seq=1000
  2. ACK 丢包:发送方发 Seq=1000→ 接收方收到并回复 Ack=1001→ 但 Ack 丢包→ 发送方超时→ 重传 Seq=1000→ 接收方发现 "数据已收到",但还是会回复 Ack=1001→ 发送方收到后,就知道之前的 ACK 丢了,但数据其实已送达。

问题 :重传可能导致 "重复数据",但 TCP 靠序列号解决 ------ 接收方会自动丢弃重复的 Seq 数据(比如收到两个 Seq=1000,只保留一个,回复 Ack=1001)。

那么, 如果超时的时间如何确定?

  • 最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回"。
  • 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
  • 如果超时时间设的太长, 会影响整体的重传效率;
  • 如果超时时间设的太短, 有可能会频繁发送重复的包;

TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间:

  • Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的。
  • 超时时间都是500ms的整数倍。
  • 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传。
  • 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增。
  • 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。

总结:

重传的超时时间的阈值不是固定不变的,随着重传次数的增加而增大(重传频率越来越低)。如果多次重传之后还丢包,说明当前丢包的概率太大了,这个时候意味着网络已经出现非常严重的故障了,再传也意义不大,就只能放弃重传。

2.3.4 连接管理:"三次握手建连接,四次挥手断连接"

TCP 通信前必须先建立连接 (类似 "打电话前先拨号接通"),通信后要断开连接 (类似 "挂电话")。这就是著名的三次握手(建立连接)四次挥手(断开连接)

三次握手(建立连接:SYN、SYN+ACK、ACK)

  1. 客户端→服务器 :发SYN 包(控制位 SYN=1,Seq = 随机数 X)→ 请求建立连接。
  2. 服务器→客户端 :回复SYN+ACK 包(SYN=1,ACK=1,Seq = 随机数 Y,Ack=X+1)→ 同意建立连接。
  3. 客户端→服务器 :发ACK 包(ACK=1,Seq=X+1,Ack=Y+1)→ 确认连接建立。

为什么是三次? 两次不行吗?

  • 两次握手可能导致 "失效的连接请求"。比如客户端发的 SYN 包因网络延迟,20 秒后才到服务器,服务器以为是新请求,回复 SYN+ACK。但客户端早已超时放弃,不会回复 ACK。服务器会一直等 ACK,浪费资源。
  • 三次握手能确保双方都能收发数据:客户端确认 "能发能收"(收到服务器的 SYN+ACK,说明服务器能收且能发);服务器确认 "能收能发"(收到客户端的 ACK,说明客户端能收)。

**为什么要进行握手?**意义何在?

  1. 三次握手,可以先针对通信路径,进行投石问路,初步的确认一下通信链路是否通畅(可靠性的前提条件)。
  2. 三次握手,是在验证通信双方,发送能力和接收能力是否正常。
  3. 三次握手的过程中也会协商一些必要的参数。通信是客户端、服务器双方的事情,要配合,其中有些内容要保持一致。参数往往是在选项中体现。

建立连接 本质上就是让通信双方保存对方的信息在相应的数据结构中,断开连接的本质目的就是为了把对端的信息,从数据结构中给删除掉、释放掉。

四次挥手(断开连接:FIN、ACK、FIN+ACK、ACK)

  1. 客户端→服务器 :发FIN 包(FIN=1,Seq=U)→ 告诉服务器 "我要断开,没数据发了"。
  2. 服务器→客户端 :回复ACK 包(ACK=1,Ack=U+1,Seq=V)→ 告诉客户端 "收到断开请求,我处理一下剩余数据"。
  3. 服务器→客户端 :发FIN+ACK 包(FIN=1,ACK=1,Seq=W,Ack=U+1)→ 告诉客户端 "我也没数据了,同意断开"。
  4. 客户端→服务器 :回复ACK 包(ACK=1,Seq=U+1,Ack=W+1)→ 确认断开。

为什么是四次? 因为 "断开请求" 和 "剩余数据处理" 要分开:

  • 客户端发 FIN 后,服务器可能还有数据没发完(比如缓存的消息),所以先回复 ACK "收到断开请求",等数据发完再发 FIN "同意断开"。
  • 客户端最后发 ACK,是为了让服务器确认 "断开请求已收到",避免服务器重发 FIN。

LISTEN(监听状态):

  • 角色:仅服务端会进入这个状态。
  • 作用:服务端调用 listen() 后,socket 进入 LISTEN 状态,开始被动等待客户端的连接请求。可以理解为 "开门营业,等客户上门",此时服务端会维护一个「半连接队列」(处理 SYN 包)和「全连接队列」(处理已完成三次握手的连接)。

ESTABLISHED(已建立连接状态):

  • 角色 :客户端和服务端成功完成三次握手后,双发都会进入这个状态。
  • 作用:表示 TCP 连接已 "就绪",双方可以正常收发数据(应用层的 read/write 都会基于这个状态的连接)。是 TCP 数据传输的 "黄金阶段",大部分业务逻辑(如 HTTP 请求 / 响应)都在这个状态完成。

CLOSE_WAIT(关闭等待状态):

  • 角色被动关闭方(一般是服务端,也可能是客户端)会进入这个状态。
  • 作用:当主动关闭方(比如客户端)发 FIN 包请求断开连接时,被动关闭方收到 FIN 后,会先回 ACK,然后进入 CLOSE_WAIT。这个状态的含义是:"我(被动方)已经知道对方要断开,但我可能还有未发完的数据,需要先处理完自己的『收尾工作』(比如应用层继续读数据、发数据)"。
  • 注意 :如果代码里没及时关闭 socket,CLOSE_WAIT 会持续存在,可能导致资源泄漏(比如服务端大量连接 stuck 在 CLOSE_WAIT,最终无法接受新连接 )。

TIME_WAIT(时间等待状态):

  • 角色主动关闭方(发完最后一个 ACK 的一端,一般是客户端)会进入这个状态。
  • 核心作用
    • 防止迟到的数据包干扰新连接:TCP 是基于 IP 的,网络里可能有延迟 / 重传的包。TIME_WAIT 会等待 2MSL(MSL 是报文最大生存时间,一般是 1 - 2 分钟 ),确保旧连接的残留报文都消失,新连接不会被旧包 "搞混"。
    • 可靠关闭连接:如果最后一个 ACK 丢了,被动关闭方会重发 FIN,TIME_WAIT 状态让主动关闭方有机会重传 ACK,保证双方都能 "干净断开"。
  • 小坑:高并发场景下,大量 TIME_WAIT 可能占用端口(默认端口范围有限),导致新连接失败。可以通过调整内核参数(如 tcp_tw_reuse)优化,但需谨慎。

简单总结:

  • LISTEN 是服务端 "等连接" 的状态;
  • ESTABLISHED 是 "正常传数据" 的状态;
  • CLOSE_WAIT 是被动关闭方 "处理收尾" 的状态;
  • TIME_WAIT 是主动关闭方 "兜底防残留" 的状态。

这些状态共同保障了 TCP 连接 "建立 - 传输 - 断开" 的可靠性,但也会带来一些运维 / 开发需要关注的细节(比如 CLOSE_WAIT 泄漏、TIME_WAIT 端口占用 )。

2.3.5 滑动窗口:"批量发货,提升效率" 的优化

刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答。收到ACK后再发送下一个数据段。 如果每次发数据都要等 ACK,效率太低(尤其网络快时,等待时间占比大)。

TCP 用滑动窗口(Sliding Window) 优化:允许发送方一次性发多个数据段,不用等一个 ACK 再发下一个。

核心逻辑:

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节(四个段)。
  • 发送前四个段的时候, 不需要等待任何ACK, 直接发送;
  • 收到第一个ACK后, 滑动窗口向后移动,继续发送第五个段的数据; 依次类推;
  • 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据, 才能从缓冲区删掉;
  • 窗口越大,则网络的吞吐率就越高;

举个例子

窗口大小 = 3,发送方发 Seq=1000、2000、3000→ 收到 Ack=1001(确认第一个)→ 窗口滑动,可发 Seq=4000。相当于 "流水线作业",不用等全部 ACK,提升了效率。

**那么如果出现了丢包,如何进行重传?**这里分两种情况讨论:

**情况一:**数据包已经抵达,ACK被丢了。

这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认;

情况二: 数据包就直接丢了。必然需要重传数据了~~

丢失报文的 "提醒":

接收端未收到 1001 - 000 字节,会持续返回 ACK = 1001 表示 "期望接收从1001开始的数据" )。这相当于接收端在向发送端 "喊话":"我要的是 1001 开头的数据,之前丢了,快补发!"

触发快速重传条件:

发送端连续3 次收到相同的 ACK = 1001 时,判断 "对应数据(1001 - 2000字节)极有可能丢失",直接启动快速重传 ,主动补发该段数据。

接收端的 "缓冲兜底":

其实接收端可能早已收到 2001 - 7000 字节(网络乱序等原因),但因 1001 - 2000 缺失,这些数据被暂存于接收端内核的接收缓冲区

当发送端补发 1001 - 2000 后,接收端确认完整收到 1 - 7000 字节,会返回 ACK = 7001(表示 "已接收 1 - 7000 字节,接下来期望 7001 开头的数据" ),让传输恢复正常。

2.3.6 流量控制:"别发太猛,我处理不过来"

接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。

因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度 。这个机制就叫做流量控制(FlowControl);

流量控制是接收方限制发送方速度的机制,靠 "窗口大小" 实现:

  1. 接收方:根据自己的处理能力(比如缓存大小、CPU 负载),在 ACK 里设置 "窗口大小"(比如窗口 = 1000 字节)。
  2. 发送方:根据接收方的窗口大小,调整发送速度(最多发 1000 字节的数据,等收到 ACK 再调整)。

如果接收方处理不过来(比如缓存满了),可以把窗口设为 0→ 发送方停止发数据,直到接收方窗口 > 0。

类比

你给朋友传文件,朋友说 "我硬盘只剩 1G 空间了(窗口 = 1G)",你就只传 1G 的数据;等朋友清理出空间,告诉你 "现在有 2G 空间(窗口 = 2G)",你再传更多。

2.3.7 拥塞控制:"别把网络搞堵了" 的全局优化

TCP 依靠滑动窗口 实现了高效可靠的大数据量传输,但这一机制并非 "万能解药"------ 若连接建立初期就盲目发送大量数据,仍会引发网络问题。
网络环境中存在多台计算机,可能本身已处于拥塞状态 (如链路带宽不足、路由转发压力大)。此时若不了解网络实际状况,直接发送大量数据,会进一步加剧拥塞,导致数据延迟、丢包甚至传输中断,形成 "雪上加霜" 的恶性循环。
为解决上述问题,TCP 引入慢启动 (Slow Start) 机制:连接初期不直接发送大量数据,而是先以小量数据试探 ,通过观察数据传输反馈(如 ACK 确认、丢包情况),动态感知网络拥塞程度,再据此逐步调整后续数据发送速率,实现 "既避免加剧拥塞,又能高效利用网络资源" 的平衡。
简单说,慢启动是 TCP 应对复杂网络的 "谨慎探路者"------ 用 "先小步试探,再按需加速" 的策略,让数据传输适配网络状态,保障整体传输的稳定性与效率 。

核心算法(简化版):

  1. 慢启动(Slow Start):刚建立连接时,窗口从小到大指数增长(比如初始窗口 = 1→ 发 1 个数据段→ 收到 ACK→ 窗口 = 2→ 发 2 个→ 收到 ACK→ 窗口 = 4......),快速探测网络状态。
  2. 拥塞避免(Congestion Avoidance):窗口增长到 "慢启动阈值" 后,改为线性增长(窗口每次 + 1),避免增长过快搞堵网络。
  3. 拥塞发生(Congestion Detection):如果发生超时重传或快速重传,说明网络拥堵→ 降低慢启动阈值,重新慢启动。
  • 此处引入一个概念程为拥塞窗口;
  • 发送开始的时候, 定义拥塞窗口大小为1;
  • 每次收到一个ACK应答, 拥塞窗口加1;
  • 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;

像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快。

  • 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍;
  • 此处引入一个叫做慢启动的阈值;
  • 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长;
  • 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
  • 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;

少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;

拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

2.3.8 延迟应答:"攒一波回复,减少交互"

接收方收到数据后,不是立刻回复 ACK,而是延迟一会儿(比如几十毫秒),等应用层取走数据,或者 "攒几个 ACK 一起回复"。这样做的好处是:

  • 优化网络交互:减少 ACK 的数量(比如攒 3 个 ACK 一起发),降低网络开销。
  • 辅助流量控制:延迟期间,接收方的窗口可能变大(应用层取走数据,缓存空了),可以告诉发送方 "窗口更大了,多发包"。

注意

延迟不能太久(否则发送方超时重传),一般由操作系统调优(比如 Linux 默认延迟 40ms 左右)。

2.3.9 捎带应答:"顺手回复,一石二鸟"

捎带应答是基于延时应答 引入的机制,TCP 允许在回复数据时,顺手把 ACK 带过去,不用单独发 ACK 包。

例子

客户端给服务器发请求(Seq=100),服务器处理完要给客户端发响应数据(Seq=200)。这时候,服务器可以在响应数据的 TCP 头里,把 Ack=101(确认客户端的请求)捎带过去。相当于 "回消息时顺便说'收到你的消息了'",减少一次单独的 ACK 交互。

2.3.10 面向字节流:"数据无边界,但有序"

  • 创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
  • 调用write时,数据会先写入发送缓冲区中;
  • 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
  • 如果发送的字节数太短,就会先在缓冲区里等待, 等到缓冲区长度差不多了,或者其他合适的时机发送出去;
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
  • 然后应用程序可以调用read从接收缓冲区拿数据;
  • 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。这个概念叫做 全双工;

由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:

  • 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
  • 读100个字节数据时,也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节,也可以一次read一个字节, 重复100次;

2.3.11 粘包问题:"数据粘在一起,怎么拆分?"

因为 TCP 是面向字节流的,发送方连续发多个数据段,接收方可能 "一次性收到多个段的数据",导致粘包(比如本该是两条消息 "你好""吃饭了吗",接收方收到 "你好吃饭了吗")。

解决方法(应用层实现):

  1. 定长消息:每个消息固定长度(比如每条消息 100 字节),接收方按长度拆分。
  2. 分隔符:用特殊字符分隔消息(比如换行符 "\n"),接收方按分隔符拆分。
  3. 长度字段:每个消息前加 "长度字段"(比如前 4 字节表示消息长度),接收方先读长度字段,再读对应长度的数据。

2.3.12 异常情况:"遇到问题,如何处理?"

如果发送丢包更严重的情况,甚至说网络直接出现故障的情况,如何处理??

进程崩溃场景:

不管进程是正常结束,还是因故障异常崩溃,系统都会自动回收文件资源、关闭相关文件 。这一过程会触发 TCP 连接的四次挥手流程 ,目的是让通信双方有序关闭连接,确认彼此删除保存的连接信息 。
TCP 连接的生命周期和进程生命周期不完全绑定,即便进程已退出,只要 TCP 连接还在,就能依靠系统中留存的连接信息,完成后续挥手操作,和正常流程的四次挥手,在核心逻辑(确认双方删除连接信息)上是一致的 。

正常关机场景:

当主机执行正常关机操作,系统会强制终止所有进程 ,进而触发TCP连接的四次挥手流程 。不过实际中,若关机动作很快,四次挥手可能还没完整执行,系统就关闭了 。

  • 理想情况:本端和对端能顺利配合,正确删除各自保存的连接信息,平稳关闭连接 。
  • 非理想情况 :至少本端能发送第一个 FIN 报文,告知对端 "我方要结束连接" 。对端收到 FIN 后,会进入连接释放流程,回复 ACK 并发送自己的 FIN(此 FIN 无需 ACK 回应 );若对端发的 FIN 没收到 ACK,会启动超时重传机制 。多次重传仍无回应时,对端会单方面释放连接信息,类似协商离婚不成,通过 "法律手段" 强制终止关系 。

突发断电(直接拔电源)场景:

直接拔电源会让机器瞬间关机,大概率来不及发送 FIN 报文。

  • 接收方断电:

发送方发现长时间没收到 ACK 确认,会触发报文重传 。重传多次仍失败后,TCP 会执行 "复位(RST)" 操作,清除原连接的临时数据,重新初始化连接 。这个过程靠 "复位报文段(RST)" 实现,RST 报文不带 ACK 确认 ;若重传、复位都没用,发送方会单方面放弃连接

  • 发送方断电

接收方本就在等待发送方的消息,结果迟迟没收到,需判断发送方是 "彻底断开" 还是 "暂时没发" 。TCP 协议里,接收方若一段时间没收到数据,会主动发 "心跳包" 探测对端状态 。若对端没回应心跳,接收方会尝试复位连接,单方面释放连接资源 。
这里的心跳包是 TCP 层不带应用层数据的特殊报文 ,按固定周期发送(类似 "定期打电话问平安" );若收不到心跳回应,就判定对端 "故障失联" 。

网线断开场景:

本质是 "接收方断电 + 发送方断电 " 两种情况的组合 。发送方会因收不到 ACK 重传报文,接收方会因收不到数据发心跳探测,双方多次尝试无果后,触发连接复位、单方面释放等操作,处理逻辑融合了前两种场景的机制 。

2.3.13基于 TCP 的应用层协议

TCP 的可靠性让它成为很多应用层协议的基础:

  • HTTP/HTTPS:网页传输,必须确保数据完整(否则页面显示错乱)。
  • FTP:文件传输,大文件不能丢包。
  • SMTP/POP3/IMAP:邮件收发,邮件内容不能丢失。

2.4 TCP/UDP对比

**TCP:**有连接、可靠传输、面向字节流、全双工。

**UDP:**无连接、不可靠传输、面向数据报、全双工。

我们说了TCP是可靠连接,那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单,绝对的进行比较。

  • TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
  • UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播;

归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用, 还是要根据具体的需求场景去判定。

相关推荐
一只小bit1 小时前
Linux网络:阿里云轻量级应用服务器配置防火墙模板开放端口
linux·网络·阿里云
帽儿山的枪手2 小时前
HVV期间,如何使用SSH隧道绕过内外网隔离限制?
linux·网络协议·安全
charlie1145141913 小时前
设计自己的小传输协议 导论与概念
c++·笔记·qt·网络协议·设计·通信协议
BachelorSC3 小时前
【网络工程师软考版】网络安全
网络·安全·web安全
(Charon)4 小时前
【C语言网络编程】HTTP 客户端请求(基于 Socket 的完整实现)
网络·网络协议·http
Bryce李小白5 小时前
Kotlin实现Retrofit风格的网络请求封装
网络·kotlin·retrofit
Lovyk6 小时前
Linux网络管理
服务器·网络·php
MC皮蛋侠客7 小时前
AsyncIOScheduler 使用指南:高效异步任务调度解决方案
网络·python·fastapi
DAWN_T178 小时前
关于网络模型的使用和修改/保存和读取
网络·人工智能·pytorch·python·深度学习·神经网络·机器学习
fake_ss1989 小时前
计算机网络基础(一) --- (网络通信三要素)
java·网络·tcp/ip·udp·信息与通信