第2章:以太网基础——TCP/IP协议栈概述、MAC地址与IP地址、UDP与TCP协议对比、Socket编程基础
好,咱们正式开始啃以太网这块硬骨头。很多做CAN的朋友,一听到以太网就头大,觉得那是搞IT的人玩的。其实没那么玄乎。你想想看,CAN和以太网本质上都是在做同一件事——把数据从A点搬到B点,只是规矩不同罢了。
我个人习惯,在讲任何协议之前,先搞清楚它长什么样,为什么要这么长。今天这一章,就是给咱们的CAN转以太网网关打地基。地基不牢,后面写代码全是坑。
2.1 TCP/IP协议栈:到底是个什么玩意儿?
说白了,TCP/IP协议栈就是一套通信的规矩。它把整个通信过程分成了四层,每一层只管自己的事。我刚开始学的时候,总觉得分层是脱裤子放屁,直到我在一个项目里,因为物理层没处理好,导致整个网关疯狂丢包……嗯,从那以后我再也不敢小看任何一层了。
这四层分别是:
- 应用层:你写的程序就在这层。比如HTTP、MQTT,还有咱们后面要用的自定义协议。
- 传输层:负责端到端的通信。主要两个小弟——TCP和UDP,后面细聊。
- 网络层:核心是IP协议,负责寻址和路由。说白了就是告诉数据包:“你该往哪走”。
- 网络接口层:最底层,管的是网线、网卡、MAC地址这些物理玩意儿。
记住一个原则:上层只管把数据扔给下层,下层只管把数据传出去。每一层都只跟自己的邻居打交道。这就是分层的好处——你改应用层代码,不用去动网卡驱动。
我在做第一个网关原型时,犯过一个低级错误:我在应用层封装了一个很大的数据包,结果发现网络层死活发不出去。后来查了半天,才发现是MTU(最大传输单元)的限制。这就是不懂分层的代价。
2.2 MAC地址与IP地址:两个地址,各管一摊
很多新手会问:为什么要有两个地址?一个不够吗?
我打个比方你就明白了。IP地址就像你的家庭住址,是逻辑上的位置。MAC地址就像你的身份证号,是物理上的唯一标识。你搬家了,家庭住址会变,但身份证号不会变。对吧?
MAC地址:
- 48位,通常写成 00:1A:2B:3C:4D:5E 这种格式
- 出厂时烧录在网卡里,理论上全球唯一
- 工作在数据链路层,负责局域网内的设备识别
IP地址:
- IPv4是32位,写成 192.168.1.100 这种点分十进制
- 可以手动配置,也可以DHCP自动获取
- 工作在网络层,负责跨网络的路由寻址
避坑指南:我曾经在一个项目里,因为两个设备的MAC地址冲突,导致整个局域网广播风暴。查了整整两天才找到原因。所以,如果你用开发板做网关,记得先检查一下MAC地址是不是唯一的。很多廉价开发板出厂时MAC地址是重复的!
在咱们的CAN转以太网网关里,IP地址是用来让其他设备找到你的网关的。MAC地址则是交换机用来转发数据帧的依据。两个地址,缺一不可。
2.3 UDP与TCP协议对比:一个快,一个稳
这两个协议,是传输层的两大主力。我经常被问到:“做网关到底用TCP还是UDP?”
我的回答是:看场景。
先看个对比表:
| 特性 | UDP | TCP |
|---|---|---|
| 连接方式 | 无连接,直接发 | 面向连接,先握手 |
| 可靠性 | 不可靠,丢了不管 | 可靠,丢包重传 |
| 速度 | 快,开销小 | 慢,有确认机制 |
| 数据边界 | 保留消息边界 | 流式传输,无边界 |
| 典型应用 | 视频流、DNS、游戏 | 网页、文件传输、邮件 |
UDP的特点:
- 像寄明信片,写了地址就扔邮筒,不保证对方能收到
- 没有握手和挥手,延迟极低
- 适合实时性要求高、可以容忍少量丢包的场景
TCP的特点:
- 像寄挂号信,有回执,丢了会重发
- 有三次握手建立连接,四次挥手断开连接
- 适合要求数据完整、不能出错的场景
注意:在CAN转以太网网关中,我个人更倾向于UDP。为什么?因为CAN本身就是一个实时性很强的总线,数据量小,频率高。如果用TCP,握手重传的开销反而会拖慢整个系统。当然,如果你的应用场景要求绝对不能丢数据(比如固件升级),那就老老实实用TCP。
我记得有一次,客户要求把CAN上的电机控制数据通过网关转发到上位机。我一开始用了TCP,结果发现电机一加速,网关就卡死。后来换成UDP,问题迎刃而解。原因就是TCP的拥塞控制机制在高速数据流下反而成了瓶颈。
2.4 Socket编程基础:让代码开口说话
Socket,中文叫套接字,说白了就是应用程序和网络协议栈之间的一个接口。你往Socket里写数据,协议栈就帮你发出去。你从Socket里读数据,协议栈就把收到的数据递给你。
在嵌入式Linux环境下,Socket编程其实很简单。我给大家看一个最基础的UDP收发示例:
// UDP发送端示例
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main() {
int sockfd;
struct sockaddr_in dest_addr;
char *msg = "Hello from CAN Gateway!";
// 1. 创建Socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 设置目标地址
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(8888);
dest_addr.sin_addr.s_addr = inet_addr("192.168.1.100");
// 3. 发送数据
sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr *)&dest_addr, sizeof(dest_addr));
// 4. 关闭Socket
close(sockfd);
return 0;
}
这段代码干了四件事:创建Socket、设置地址、发送数据、关闭Socket。就这么简单。
对于TCP,流程稍微复杂一点,多了个连接的过程:
// TCP客户端示例
int sockfd;
struct sockaddr_in server_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");
// 先连接
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 再发送
send(sockfd, msg, strlen(msg), 0);
// 最后关闭
close(sockfd);
小技巧:在写网关程序时,我建议把Socket的创建和配置封装成一个函数。比如叫 udp_socket_init() 或者 tcp_client_connect()。这样主程序看起来清爽,也方便后期维护。我曾经在一个项目里,因为Socket创建代码散落在各个地方,改个端口号要改五个文件……那滋味,你懂的。
好了,这一章的内容就到这里。以太网基础打牢了,后面咱们才能放心地让CAN和以太网“联姻”。下一章,我会带大家看看硬件层面怎么选型,以及如何设计一个靠谱的网关电路。