版权信息
warning
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
对于我,如果不是想要深入计算机网络,只是做应用的话,上两层需要理解得更深一点,下三层只需了解即可
1. 传输层
如果说网络层(IP层)解决的是“如何把数据从主机 A 送到主机 B”,那么传输层解决的就是“如何把数据从主机 A 的某个程序,送达主机 B 的另一个特定程序”。这被称为端到端(进程到进程) 的通信。
1.1. 传输层的核心作用
-
端口寻址(Port Addressing): 一台服务器上可能同时运行着 Web 服务器、数据库、SSH 后台服务。传输层通过**端口号(Port Number,0-65535)**来区分这些不同的应用程序。比如,默认情况下 HTTP 使用 80 端口,HTTPS 使用 443 端口,MQTT 通常使用 1883 端口。
-
分段与重组: 应用层丢下来的数据往往很大(比如一个固件更新包),传输层会将其切割成网络能够承受的小块(报文段/数据报),并在接收端将它们按顺序拼装回原样。
-
可靠性控制: 它可以决定是否要对丢失的数据进行重传,是否要控制发送的速度以免接收方处理不过来。
1.2. 传输层协议TCP和UDP
传输层最主要的两个协议。它们具有两种不同的设计哲学。
1.2.1. TCP(传输控制协议,Transmission Control Protocol)
-
特点: 面向连接、极其可靠、保证顺序、有流量控制和拥塞控制。
-
机制: 在发送数据前,双方必须先进行著名的“三次握手”建立连接。如果在传输过程中有数据包丢失,TCP 会自动要求对方重传。
-
适用场景: 绝不能容忍数据丢失或错乱的场景。比如:网页浏览、文件传输(FTP)、设备的 OTA 固件升级、以及物联网设备上报关键遥测数据的 MQTT 协议,底层无一例外都是基于 TCP 的。
1.2.2. UDP(用户数据报协议,User Datagram Protocol)**
-
特点: 无连接、不可靠、尽力而为、轻量且速度极快。
-
机制: 没有握手,没有确认,也不管接收方能不能处理得过来,直接把数据“扔”过去。如果丢了,那就丢了。
-
适用场景: 对实时性要求极高、能容忍少量丢包的场景。比如:视频流媒体、在线游戏。在嵌入式开发中,局域网内的快速设备发现(如 UDP 广播)、轻量级的 CoAP 协议,或者 NTP 网络时间同步,通常会选择 UDP 来节省系统开销。
1.3. 补充1:TCP的“三次握手”“四次挥手”
TCP为了建立可靠的数据连接,会在连接时进行“三次握手”,在断开连接时“四次挥手”。
1.3.1. “三次握手”
当你调用 Socket API 的 connect() 函数时,底层网络栈就开始执行三次握手。其核心目的是:双方互换初始序列号(Sequence Number,简称 seq),并确认双方的收发能力都正常。
为了方便理解,我们把主动发起连接的设备称为客户端(Client),等待连接的称为服务端(Server)。
-
第一次握手:客户端“抛出橄榄枝”
-
动作: 客户端向服务端发送一个带有 SYN(Synchronize,同步) 标志位的报文。
-
内容: “我想和你建立连接。我的初始数据序列号是
x。” -
状态变化: 客户端进入
SYN_SENT状态。 -
目的: 测试服务端的接收能力。
-
-
第二次握手:服务端“确认并反向抛出橄榄枝”
-
动作: 服务端收到 SYN 报文后,同意连接,并向客户端发送一个同时带有 SYN 和 ACK(Acknowledgment,确认) 标志位的报文。
-
内容: “你的请求我收到了(ACK,确认号为
x+1)。我也想和你建立连接,我的初始序列号是y(SYN)。” -
状态变化: 服务端进入
SYN_RCVD状态。 -
目的: 确认客户端的发送能力和自己的接收能力正常,同时测试客户端的接收能力。
-
-
第三次握手:客户端“最终确认”
-
动作: 客户端收到服务端的 SYN+ACK 报文后,向服务端发送一个带有 ACK 标志位的报文。
-
内容: “你的请求我也收到了(ACK,确认号为
y+1),我们开始传输数据吧!” -
状态变化: 客户端进入
ESTABLISHED(已建立连接)状态。服务端收到此报文后,也进入ESTABLISHED状态。 -
目的: 确认自己的接收能力和服务端的发送能力正常。
-
1.3.2. “四次挥手”
当你调用 close() 函数准备关闭 Socket 时,底层执行的是四次挥手。TCP 是全双工通信(数据可以同时双向流动),这就意味着关闭连接也必须是双向独立的。一方不想发数据了,不代表另一方也没数据要发了。
任何一方都可以主动发起挥手,我们假设还是客户端先发起。
-
第一次挥手:客户端“我发完了”
-
动作: 客户端发送一个带有 FIN(Finish,结束) 标志位的报文。
-
内容: “我的数据都发完了,我不会再给你发应用层数据了(但我还能接收)。”
-
状态变化: 客户端进入
FIN_WAIT_1状态。
-
-
第二次挥手:服务端“我知道你发完了”
-
动作: 服务端收到 FIN 报文后,立刻回复一个 ACK 报文。
-
内容: “收到了你的关闭请求。但是稍等,我可能还有一些数据没处理完/没发完。”
-
状态变化: 服务端进入
CLOSE_WAIT状态;客户端收到后进入FIN_WAIT_2状态。 -
注意:此时 TCP 连接处于“半关闭状态”,客户端到服务端的通道关闭了,但服务端到客户端的通道还在开启。
-
-
第三次挥手:服务端“我也发完了”
-
动作: 等服务端把手头剩下的数据都发送完毕后,它向客户端发送带有 FIN 标志位的报文。
-
内容: “我的数据也全发完了,可以彻底断开了。”
-
状态变化: 服务端进入
LAST_ACK状态。
-
-
第四次挥手:客户端“彻底再见”
-
动作: 客户端收到 FIN 后,回复一个 ACK 报文。
-
内容: “收到,彻底再见。”
-
状态变化: 服务端收到 ACK 后立即进入
CLOSED状态。而客户端则会进入一个特殊的TIME_WAIT状态,等待一段固定的时间(通常是 2 个 MSL,最长报文段寿命)后,才会最终进入CLOSED状态,释放 Socket 资源。
-
1.3.3. 为什么建立是三次,断开却是四次?
-
建立连接时(三次): 服务端收到客户端的
SYN连接请求时,它什么包袱都没有,可以直接把**同意连接(ACK)和自己也要连接(SYN)**这两个动作合并在一个数据包里(SYN+ACK)发过去,因此只需要三次。 -
断开连接时(四次): 客户端发来
FIN表示它要关了,但此时服务端很可能还在忙着处理上层的业务逻辑,或者发送缓冲区里还有没发完的数据。服务端只能先回一个ACK(表示“我收到了你的断开请求”),然后继续把没发完的数据发完。等彻底发完后,服务端才能发送自己的FIN(表示“我也准备好断开了”)。因为ACK和FIN之间存在时间差(要等应用层代码把数据处理完),所以无法合并,必须分两次发送。
1.4. 补充2:UDP的通信
UDP是无连接的协议。
-
发送方: 不需要调用
connect()。创建好 Socket 后,直接调用sendto()函数。在这个函数里,你必须每一次都显式地附带上目标 IP 和目标端口,然后把数据发出去。 -
接收方: 不需要调用
listen()和accept()去等待连接。只需创建一个 Socket,绑定本地端口,然后死循环调用recvfrom()阻塞在那儿。任何从网络上飞过来的、只要目标端口匹配的 UDP 包,都会被内核收下来丢给这个程序。
UDP把极致的速度和灵活性交给了开发者。代价就是开发者必须自己承担处理网络不确定性的风险。如果你使用 UDP,但又想保证数据不丢,你必须在自己的应用层代码里实现一套简单的确认和重传机制。
2. 应用层
应用层(Application Layer)位于最顶层。它是整个网络体系结构中唯一直接与应用程序和终端用户交互的一层。
如果说底下的四层解决了“如何将数据从A点可靠/不可靠地搬运到B点”的底层硬件通信和路由问题,那么应用层解决的就是“这些数据代表什么,以及应用程序应该如何去理解和处理这些数据”。可以说,如果没有应用层,通信双方收到的只是一连串毫无意义的 0 和 1 二进制流。
2.1. 应用层的核心作用
-
定义数据交互规则: 规定了通信双方的应用程序如何交互信息。例如,定义了请求报文和响应报文的格式、字段的含义,以及何时发送和如何处理这些报文。
-
提供网络接口: 提供标准化的接口(如 Socket API),让开发者编写的程序能够调用底层网络功能,而无需关心复杂的网卡驱动或 TCP/IP 状态机。
-
数据序列化与呈现: 决定了数据在网络传输前的组织形式(如 JSON、XML、Protocol Buffers 或是纯文本)。
2.2. 常见的应用层协议
应用层包含了数量最庞大、种类最繁多的协议,以满足各种不同的业务需求:
-
Web 与文件传输:
-
HTTP/HTTPS: 网页浏览的基石,采用请求-响应模型。
-
FTP: 用于在服务器和客户端之间传输文件。
-
-
网络基础服务:
-
DNS (Domain Name System): 互联网的“电话簿”,负责将人类可读的域名(如
google.com)解析为机器通信所需的 IP 地址。 -
DHCP: 动态主机配置协议,自动为网络中的设备分配 IP 地址。
-
-
物联网 (IoT) 与嵌入式协议:
-
MQTT (Message Queuing Telemetry Transport): 一种轻量级的发布/订阅消息传输协议。
-
CoAP: 专为资源受限的设备(如低功耗单片机)设计的类 HTTP 协议,通常基于 UDP 运行,开销极小。
-
2.3. 应用层在嵌入式开发者中的视角
从底层系统或 RTOS 开发的视角来看,应用层的界限非常清晰:
当你在开发一个带网络功能的嵌入式系统时,底层通常由芯片厂商提供的网络协处理器或软件协议栈(如 LwIP)来包揽物理层到传输层(MAC/PHY 控制、IP 路由、TCP/UDP 封装)。而应用层的工作就是你手写的代码逻辑。
-
打包(发送方向): 将裸数据(如传感器采集的 ADC 原始值或计算后的浮点数)按照指定的格式(比如 JSON 字符串)拼接好,加上特定协议(如 MQTT)的固定报头(Header),然后调用类似
send()或write()的 Socket 函数,把这坨内存数据丢给传输层。 -
解析(接收方向): 从 Socket 缓冲区里
recv()读出一条字节流。底层只保证这是无误的字节,而你的应用层代码需要去解析这段字节——提取命令、校验应用层业务逻辑,最后驱动硬件执行动作(比如控制继电器或调节 PID 参数)。
2.4. 应用层和传输层之关系
-
服务提供者与消费者的关系 (Socket 接口)
传输层是服务提供者,应用层是服务消费者。 它们之间的物理/代码界限通常就是 Socket API(套接字接口)。当你在应用层写代码时(无论是 Python 脚本还是 C 语言的嵌入式程序),你不需要手写 TCP 三次握手的逻辑。你只需要调用
socket()创建一个套接字,指定它是 TCP 还是 UDP,然后调用connect()、send()和recv()。操作系统内核(或者 RTOS 中的 LwIP 协议栈)会在底层自动帮你处理复杂的传输层状态机。 -
数据封装的关系
当应用层产生数据(Payload)并准备发送时,数据会向下传递给传输层。传输层不会关心应用层的数据是什么内容(无论它是 JSON 还是加密的二进制码),它只把这些数据当成一个整体。传输层会在这些数据前面加上自己的报头(Header),其中最关键的就是源端口号和目标端口号。
-
应用层数据 + TCP 报头 = TCP 报文段(Segment)。
-
接收端则是一个逆向的“拆快递”过程:传输层剥离 TCP 报头,检查端口号,然后将里面的“纯应用层数据”顺着 Socket 丢给对应的应用程序去解析。
-
3. Socket 套接字概述
前面我们聊了应用层和传输层,也提到了它们之间的桥梁就是 Socket(套接字)。现在我们就简单了解一下套接字。
3.1. 什么是套接字?
“Socket” 这个词的本义是“插座”。想象一下你家墙上的电源插座:插座背后隐藏着极其复杂的国家电网(发电厂、变电站、高压线),但作为用户,你完全不需要懂电力工程,你只需要把电器的插头(Plug)插进插座,就能通电。
在网络世界里也是一样:
-
国家电网 = 复杂的 TCP/IP 协议栈(三次握手、拥塞控制、IP 路由)。
-
插座 (Socket) = 操作系统内核提供给程序员的一组标准化 API。
通过这组 API,你的应用程序就可以把数据“插”进网络中,或者从网络中“拔”出数据,而无需手写底层的协议控制代码。
3.2. 一切皆文件
你一定知道 Unix/Linux 的核心哲学:一切皆文件(Everything is a file)。
Socket 完美地贯彻了这一哲学。在内核层面,Socket 其实就是一个特殊的文件。
当你调用 socket() 函数创建一个网络连接时:
-
操作系统内核会为你分配一小块内存(通常包含发送缓冲区和接收缓冲区),并维护这个连接的各种状态(比如 TCP 的
ESTABLISHED状态)。 -
内核会向你的应用程序返回一个 文件描述符(File Descriptor,简称 fd,本质上就是一个整数索引)。
从这一刻起,在代码层面,操作网络就变得和操作普通的本地文件,或者是操作一个 UART 串口、一个 I2C 设备节点没有任何区别了。
-
你想发数据?调用
write(fd, buffer, length)或者send(),把字节流写进这个“文件”。内核会自动把这些字节拿走,切片、加 TCP 头、加 IP 头,最后扔给网卡硬件(MAC/PHY)发出去。 -
你想收数据?调用
read(fd, buffer, length)或者recv(),从这个“文件”里读取。如果网卡没收到数据,你的当前任务/线程就会被内核挂起(阻塞等待),直到网卡触发中断,内核把数据剥离头部后放进 Socket 缓冲区,再唤醒你的程序去读。
3.3. Socket 必要的参数
如何确定一个独一无二的 Socket 通道?内核通过一个经典的 “五元组” 来精确定位一个 Socket 通信:
-
协议(Protocol): TCP 还是 UDP?
-
本地 IP 地址: 你的设备可能有多张网卡(比如同时连着 Wi-Fi 和以太网),走哪个 IP?
-
本地端口(Local Port): 你的应用在哪个端口?
-
目标 IP 地址: 数据要发给哪台机器?
-
目标端口(Remote Port): 对方机器上的哪个程序来接这个数据?
只要这五个参数确定了,一条端到端的逻辑数据线就确立了。
3.4. Socket 的生命周期
以Berkeley Sockets(BSD Sockets)标准接口为例。我们以一个 TCP 服务端为例,看看它的全生命周期:
-
socket():买个新插座向内核申请创建一个指定协议(TCP/UDP)的 Socket 文件描述符。
-
bind():把插座钉在墙上把这个 Socket 和某个特定的本地 IP 及端口号绑定起来。意思是:“我就守在这个端口听消息了。”
-
listen():通电并开始接客(仅限 TCP)告诉内核,这个 Socket 不是用来主动发起连接的,而是用来被动等待别人连接的。内核会为此准备一个等待队列。
-
accept():诞生一个“分身”插座这是服务端最核心的一步。当客户端的三次握手完成时,
accept()会被唤醒。注意:它不会用原来的那个 Socket 和客户端通信,而是由内核动态“克隆”出一个全新的 Socket 文件描述符(专用于跟这个客户端聊天),而最初的那个 Socket 继续回去监听新客户。 -
send()/recv():读写数据在应用层缓冲区和内核 Socket 缓冲区之间搬运字节流。
-
close():拔掉插头,拆毁插座触发四次挥手,释放内核中的控制块(TCB)内存,回收文件描述符。
3.5. MCU/SoC上的Socket
在资源受限的嵌入式系统中(比如跑在微控制器 MCU 上的 RTOS),由于内存极其有限,通常跑不动完整的 Linux 庞大网络栈。
这个时候,通常会使用轻量级的网络协议栈(比如最著名的 LwIP)。LwIP 在底层精简了大量的内存拷贝操作,但为了方便开发者,它依然在最上层封装了一套与标准 BSD Sockets 几乎一模一样的 API。
3.6. 总结
无论你是写一个庞大的后端服务器,还是仅仅想让一个小小的芯片通过传感器采集数据并推送到云端,Socket 就是那个屏蔽了所有底层硬件交互、网络路由和协议状态机的“超级隔离带”。你只要会读写 Socket 这个特殊的“文件”,就能玩转整个网络通信。