3、SocketCAN核心API详解:socket()、bind()、connect()、sendmsg()、recvmsg()、close()
说实话,很多刚接触SocketCAN的朋友,上来就照着网上的demo抄,能跑就行。但真到了项目里——尤其是要处理高负载CAN总线或者做网关转发的时候——你会发现,这几个API的用法细节直接决定了你的程序是稳如老狗还是动不动就丢帧。
我个人习惯,在讲具体代码之前,先理清楚SocketCAN在整个Linux网络栈里的位置。你想想看,它本质上就是一个特殊的协议族(AF_CAN),走的还是socket那一套接口。所以,理解这六个核心API,就等于拿到了操作CAN总线的钥匙。
3.1 socket() —— 创建CAN套接字
这是第一步。我见过有人在这里写错协议类型,导致bind的时候一直报错。其实很简单:
int s;
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
三个参数的含义:
- PF_CAN:协议族,别写成AF_CAN,虽然值一样,但语义上PF用于创建,AF用于绑定。
- SOCK_RAW:原始套接字,直接收发完整的CAN帧。如果你做网关或者诊断,基本都用这个。
- CAN_RAW:协议类型。注意,这里不是IPPROTO_TCP那种,而是CAN协议特有的。
3.2 bind() —— 绑定CAN接口
socket创建完,它还是个空壳。你得告诉它:你要从哪个CAN接口收数据,或者往哪个接口发。bind就是干这个的。
struct sockaddr_can addr;
struct ifreq ifr;
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr));
这里有个坑,我踩过:必须先通过ioctl拿到接口索引。你不能直接写死ifindex,因为系统重启后接口索引可能会变。尤其是多CAN卡设备,顺序一乱,程序就绑错了。
3.3 connect() —— 过滤CAN ID
很多人不理解:CAN总线是广播式的,为什么还要connect?其实,connect在SocketCAN里不是建立连接,而是设置接收过滤规则。
struct sockaddr_can addr;
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
// 关键:设置要接收的CAN ID
addr.can_addr.tp.tx_id = 0x123;
addr.can_addr.tp.rx_id = 0x123;
connect(s, (struct sockaddr *)&addr, sizeof(addr));
connect之后,这个socket就只收CAN ID为0x123的报文了。其他报文内核直接帮你过滤掉,应用层根本收不到。这在处理大量无关报文时特别有用。
但我得提醒你:connect的过滤粒度是单个ID。如果你需要过滤一组ID(比如0x100-0x1FF),那就得用setsockopt设置CAN_RAW_FILTER。这个我们下一章会细讲。
3.4 sendmsg() —— 发送CAN报文
发送报文,我推荐用sendmsg而不是write。为什么?因为sendmsg可以携带辅助数据(cmsg),比如设置发送时间戳、优先级等。write只能发裸数据。
struct msghdr msg = {0};
struct iovec iov;
struct can_frame frame;
frame.can_id = 0x123;
frame.can_dlc = 8;
memcpy(frame.data, "HelloCAN", 8);
iov.iov_base = &frame;
iov.iov_len = sizeof(frame);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 可以在这里添加cmsg,比如设置发送时间戳
// msg.msg_control = ...
sendmsg(s, &msg, 0);
这里有个细节:can_frame结构体的大小是16字节(4字节ID + 1字节DLC + 1字节填充 + 8字节数据 + 2字节填充)。别算错了,否则sendmsg会返回EINVAL。
3.5 recvmsg() —— 接收CAN报文
接收和发送是对称的。同样,我推荐用recvmsg而不是read。原因一样:recvmsg可以获取辅助数据,比如接收时间戳、帧类型(标准帧/扩展帧/远程帧)等。
struct msghdr msg = {0};
struct iovec iov;
struct can_frame frame;
char ctrl[256]; // 辅助数据缓冲区
iov.iov_base = &frame;
iov.iov_len = sizeof(frame);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = ctrl;
msg.msg_controllen = sizeof(ctrl);
recvmsg(s, &msg, 0);
// 检查接收到的帧类型
if (frame.can_id & CAN_EFF_FLAG) {
printf("扩展帧,ID = 0x%x\n", frame.can_id & CAN_EFF_MASK);
} else {
printf("标准帧,ID = 0x%x\n", frame.can_id & CAN_SFF_MASK);
}
// 从cmsg中提取时间戳
struct cmsghdr *cmsg;
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMPNS) {
struct timespec *ts = (struct timespec *)CMSG_DATA(cmsg);
printf("接收时间戳: %ld.%09ld\n", ts->tv_sec, ts->tv_nsec);
}
}
嗯,这里要注意:recvmsg默认是阻塞的。如果你不想阻塞,要么用O_NONBLOCK,要么用select/poll/epoll。我个人习惯用epoll,因为CAN总线报文密集时,select的效率会下降。
3.6 close() —— 关闭套接字
这个最简单,但也最容易忽略。
close(s);
close会做几件事:
- 清空socket的接收缓冲区(没读的报文就丢了)
- 释放绑定的过滤规则
- 如果socket有未发送的报文,也会丢弃
3.7 核心API关系图
下面这张图,把六个API的调用流程和关系画清楚了。你可以把它当作SocketCAN编程的路线图:
3.8 六个API的对比总结
| API | 核心作用 | 必须调用? | 常见错误 |
|---|---|---|---|
| socket() | 创建CAN套接字 | 是 | 协议类型写错(如写成SOCK_DGRAM) |
| bind() | 绑定到具体CAN接口 | 是 | 未获取接口索引就绑定 |
| connect() | 设置单ID接收过滤 | 否 | 误以为建立TCP连接 |
| sendmsg() | 发送CAN帧(支持cmsg) | 否(可用write替代) | can_frame大小计算错误 |
| recvmsg() | 接收CAN帧(支持cmsg) | 否(可用read替代) | 未处理阻塞/非阻塞模式 |
| close() | 关闭套接字释放资源 | 是 | 多线程中未同步导致EBADF |