第二讲:网络协议基础——TCP/IP协议栈、UDP协议、Socket编程基础
各位同学,咱们今天聊点实在的。网络协议这东西,说白了就是设备之间沟通的“普通话”。你想想看,如果我说中文你说英文,那还怎么玩?嵌入式系统里,两个板子要通信,没有协议就是鸡同鸭讲。
我个人习惯把网络协议分成两个层面来理解:一个是“怎么传”,也就是数据怎么打包、怎么路由;另一个是“传什么”,也就是应用层的数据格式。今天咱们重点讲前者——TCP/IP协议栈和UDP协议,顺便把Socket编程的基础打牢。
核心观点:嵌入式网络通信,90%的问题出在协议理解不透彻。你只要把TCP/IP的四层模型吃透,后面写代码就是套模板的事。
2.1 TCP/IP协议栈:四层模型,别搞混了
很多教材喜欢讲七层OSI模型,但我建议你直接忘掉它。实际工程中,我们只认TCP/IP的四层模型:应用层、传输层、网络层、网络接口层。
为什么?因为嵌入式系统资源有限,你不可能像PC那样跑完整的协议栈。我在做工业网关项目时,就遇到过裁剪协议栈导致兼容性出问题的情况——嗯,这个坑后面再细说。
先看一张图,把四层模型和对应的协议理清楚:
这张图我建议你存下来。每次写网络代码前看一眼,心里就有底了。数据从应用层往下走,每层加一个头部,这叫封装;接收方从下往上走,每层剥一个头部,这叫解封装。
实战小技巧:在嵌入式调试时,我经常用Wireshark抓包,然后对照四层模型看每一层的头部信息。比如TCP的SYN、ACK标志位,IP的源地址和目标地址——一眼就能看出问题出在哪一层。
2.2 TCP vs UDP:一个像快递,一个像明信片
传输层就两个主角:TCP和UDP。很多初学者纠结“到底用哪个”,其实没那么复杂。
TCP(传输控制协议)——可靠、有序、面向连接。就像你寄快递:要填单子、要确认收货、丢了要重发。代价是什么?慢,占用资源多。
UDP(用户数据报协议)——不可靠、无序、无连接。就像你寄明信片:扔进邮筒就不管了,丢了就丢了。好处是什么?快,开销小。
我在项目中遇到过这样一个场景:一个数据采集系统,传感器每秒上报1000个数据点。如果用TCP,三次握手、确认重传这些机制会把CPU吃满,而且延迟会飙升。后来改成UDP,配合应用层的简单校验,问题就解决了。
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认重传) | 不可靠(尽力而为) |
| 数据顺序 | 保证有序 | 不保证有序 |
| 传输速度 | 较慢 | 较快 |
| 头部开销 | 20~60字节 | 8字节 |
| 典型应用 | HTTP、FTP、MQTT | DNS、DHCP、视频流 |
注意:千万不要以为UDP不可靠就完全不能用。很多工业协议(如Modbus TCP、EtherCAT)底层用的就是UDP,但通过应用层做了可靠性保障。关键看你的业务场景能不能容忍丢包。
2.3 Socket编程基础:说白了就是“文件描述符”
Socket是什么?我换个说法你就懂了:它是操作系统提供的一个接口,让应用程序可以像读写文件一样读写网络数据。
在Linux/嵌入式系统中,一切皆文件。Socket也不例外——它就是一个文件描述符。你打开一个Socket,得到一个整数(比如3),然后对这个整数做read/write操作,数据就通过网络发出去了。
我刚开始学Socket时,总觉得它很神秘。后来在ARM Cortex-M上移植了LwIP协议栈,亲手调通了TCP Server,才明白:Socket编程就是三板斧——创建、绑定、收发。
2.3.1 TCP Socket编程流程
TCP是面向连接的,所以流程稍微复杂一点。我习惯把它分成服务端和客户端两条线来记:
服务端(Server):
socket()—— 创建Socket,指定协议族(AF_INET)、类型(SOCK_STREAM)bind()—— 绑定IP地址和端口号listen()—— 开始监听,等待客户端连接accept()—— 接受客户端连接,返回一个新的Socket用于通信recv()/send()—— 收发数据close()—— 关闭连接
客户端(Client):
socket()—— 创建Socketconnect()—— 连接服务端(指定IP和端口)send()/recv()—— 收发数据close()—— 关闭连接
看个最简单的TCP服务端代码(Linux环境):
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main() {
int server_fd, client_fd;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 1. 创建Socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定端口8080
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
// 3. 开始监听
listen(server_fd, 3);
printf("等待客户端连接...\n");
// 4. 接受连接
client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
// 5. 接收数据
recv(client_fd, buffer, 1024, 0);
printf("收到消息: %s\n", buffer);
// 6. 关闭
close(client_fd);
close(server_fd);
return 0;
}
避坑指南:我曾经在嵌入式板子上调试TCP服务端,发现客户端连上来后,服务端收不到数据。查了半天,原来是htons()忘了调用——端口号字节序不对。记住:网络字节序是大端,主机字节序可能是小端,一定要用htons()和htonl()转换。
2.3.2 UDP Socket编程流程
UDP就简单多了,不需要连接,直接发:
服务端:
socket()—— 创建Socket,类型用SOCK_DGRAMbind()—— 绑定端口recvfrom()—— 接收数据(同时获取发送方地址)sendto()—— 发送数据(指定目标地址)
客户端:
socket()—— 创建Socketsendto()—— 直接发数据给服务端recvfrom()—— 接收服务端回复
看代码更直观:
// UDP服务端示例
int sockfd;
struct sockaddr_in server_addr, client_addr;
char buffer[1024];
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 接收数据(不需要accept)
int len = sizeof(client_addr);
recvfrom(sockfd, buffer, 1024, 0, (struct sockaddr *)&client_addr, &len);
printf("收到: %s\n", buffer);
// 回复数据
sendto(sockfd, "OK", 2, 0, (struct sockaddr *)&client_addr, len);
close(sockfd);
关键区别:TCP的recv()/send()不需要指定对方地址,因为连接已经建立;UDP的recvfrom()/sendto()必须带地址参数,因为UDP是无连接的,每次都要告诉内核“发给谁”。
2.4 嵌入式环境下的Socket注意事项
在PC上写Socket程序,你基本不用操心资源问题。但在嵌入式系统里,每一字节内存、每一个CPU周期都要精打细算。
我总结了几条实战经验:
- 缓冲区大小要合理:不要用1024字节的固定缓冲区,要根据MTU(最大传输单元,通常1500字节)来设定。我一般用1460字节,刚好一个TCP分段。
- 非阻塞模式是必须的:嵌入式主循环里不能用阻塞的
recv(),否则整个系统会卡死。用fcntl()设置O_NONBLOCK,或者用select()/poll()做超时处理。 - 注意内存对齐:有些MCU(比如STM32)对非对齐访问会触发硬件异常。网络协议头部的结构体定义,记得加
__attribute__((packed))。 - 心跳机制:TCP长连接在嵌入式环境里容易断(比如Wi-Fi信号不好),我习惯在应用层加心跳包,30秒发一次,连续3次没回复就重连。
曾经踩过的坑:有一次做4G DTU项目,设备连上服务器后,过几个小时就自动断开。查了三天,发现是运营商NAT超时时间只有5分钟,而我们的心跳包间隔是10分钟。改成3分钟发一次心跳,问题解决。记住:嵌入式网络通信,不仅要懂协议,还要懂网络环境。
好了,这一讲的内容就到这里。TCP/IP协议栈是基础中的基础,Socket编程是嵌入式网络开发的必修课。你只要把四层模型刻在脑子里,把TCP和UDP的适用场景分清楚,再把Socket的创建-绑定-收发流程练熟,后面写网络应用就是水到渠成的事。
公众号:蓝海资料掘金营,微信deep3321