第4章 以太网CAPL编程入门:CAPL语言基础、事件处理函数(on ethernetPacket)、CAPL中的以太网数据结构
好,咱们进入以太网CAPL编程的第一课。说实话,很多从CAN转过来的工程师,第一次看到以太网CAPL代码时都会有点懵——怎么多了这么多结构体?怎么还有IP地址和端口号?别急,我带你一步步捋清楚。
4.1 CAPL语言基础——你其实已经会了大半
如果你写过CAN的CAPL脚本,那以太网的CAPL对你来说就是换了个皮。核心语法完全一样:
- 变量声明:还是用
int、float、char,只是多了几个以太网专用的数据类型 - 控制结构:
if-else、for、while,一个都没变 - 函数定义:还是
void MyFunction() { ... }这种写法
我个人习惯把以太网相关的变量名加上前缀,比如 ethPkt、ipAddr,这样一眼就能看出是网络相关的。你在项目里也可以定个命名规范,后面维护起来会轻松很多。
核心要点:CAPL是事件驱动的。你写的大部分代码,都是在响应某个“事件”——比如收到一个报文、定时器到期、按键被按下。
4.2 事件处理函数——on ethernetPacket 是主角
在CAN里,我们最常用的是 on message。到了以太网,对应的就是 on ethernetPacket。它的写法长这样:
on ethernetPacket *
{
// 这里写处理逻辑
write("收到一个以太网报文,长度:%d", this.len);
}
这个 * 表示匹配所有以太网报文。你也可以指定只处理某种类型的报文,比如:
on ethernetPacket 0x0800 // 只处理IPv4报文
{
// ...
}
嗯,这里要注意:0x0800 是以太网帧的 EtherType,IPv4 就是 0x0800,ARP 是 0x0806,IPv6 是 0x86DD。我在项目中遇到过好几次,有人把 EtherType 和 IP 协议号搞混了——这两个是完全不同的东西,千万别弄反了。
小技巧:调试阶段建议先用 on ethernetPacket * 把所有报文都抓下来,打印出基本信息,确认网络环境正常后,再缩小范围到特定类型。这样可以快速定位问题。
4.3 CAPL中的以太网数据结构——你得认识这几个“箱子”
以太网报文在CAPL里是用结构体表示的。最常用的几个,我给你列出来:
| 数据结构 | 说明 | 常用字段 |
|---|---|---|
EthernetPacket |
完整的以太网帧 | len, source, dest, type |
Ipv4Packet |
IPv4报文(嵌套在EthernetPacket里) | source, dest, protocol, payload |
TcpPacket |
TCP报文段 | srcPort, dstPort, seqNumber |
UdpPacket |
UDP数据报 | srcPort, dstPort, length |
你想想看,这些结构体其实是嵌套的。一个完整的报文,从外到里是:
- 以太网帧头(MAC地址、EtherType)
- IP头(源IP、目的IP、协议类型)
- 传输层头(TCP/UDP端口号)
- 应用层数据(真正的有效载荷)
在CAPL里访问这些字段,就像剥洋葱一样:
on ethernetPacket *
{
// 先拿到以太网帧
EthernetPacket eth = this;
// 如果是IPv4,再取IP层
if (eth.type == 0x0800)
{
Ipv4Packet ip = eth.ipv4;
write("源IP: %d.%d.%d.%d", ip.source[0], ip.source[1], ip.source[2], ip.source[3]);
// 如果是UDP,再取UDP层
if (ip.protocol == 17) // UDP协议号是17
{
UdpPacket udp = ip.udp;
write("目的端口: %d", udp.dstPort);
}
}
}
避坑指南:我曾经在项目里犯过一个低级错误——直接访问 this.ipv4.udp.dstPort,结果程序崩溃了。为什么?因为不是所有报文都是IPv4+UDP。如果收到一个ARP报文,它根本没有IP层和UDP层。所以,一定要先判断类型,再逐层访问。
4.4 实战小例子——抓取并打印UDP报文
光说不练假把式。咱们写一个完整的例子,把收到的UDP报文信息打印出来:
on ethernetPacket *
{
// 只处理IPv4报文
if (this.type != 0x0800) return;
Ipv4Packet ip = this.ipv4;
// 只处理UDP报文
if (ip.protocol != 17) return;
UdpPacket udp = ip.udp;
write("=== UDP报文 ===");
write("源MAC: %02x:%02x:%02x:%02x:%02x:%02x",
this.source[0], this.source[1], this.source[2],
this.source[3], this.source[4], this.source[5]);
write("源IP: %d.%d.%d.%d:%d",
ip.source[0], ip.source[1], ip.source[2], ip.source[3], udp.srcPort);
write("目的IP: %d.%d.%d.%d:%d",
ip.dest[0], ip.dest[1], ip.dest[2], ip.dest[3], udp.dstPort);
write("UDP长度: %d 字节", udp.length);
write("有效载荷: %d 字节", udp.length - 8); // UDP头固定8字节
}
这段代码我经常用在项目调试初期。说白了,就是先确认网络报文能不能正常到达,源和目的对不对,端口号有没有配错。有一次客户说他们的设备收不到数据,我跑了一下这个脚本,发现目的端口号写反了——源和目的颠倒了。这种问题,没有抓包脚本的话,排查起来真的很头疼。
4.5 几个容易踩的坑
- 字节序问题:以太网用的是大端序(网络字节序)。你在打印IP地址或端口号时,CAPL已经帮你转换好了,但如果你要手动拼装报文,一定要用
htons()和htonl()做转换。 - 报文长度:
this.len是整个以太网帧的长度,包括头部和CRC。但实际有效数据可能比这个短,因为网卡可能做了填充。别直接用this.len去访问载荷,要用this.payload.length。 - 性能问题:如果网络流量很大(比如100Mbps),
on ethernetPacket *里别写太复杂的逻辑,否则会丢包。我一般只在里面做简单的过滤和统计,复杂的解析放到定时器里去做。
我的建议:刚开始学以太网CAPL时,先别急着写复杂的逻辑。先写一个最简单的 on ethernetPacket * { write("收到报文"); },确认环境通了,再逐步加功能。这样出了问题,你知道是环境的问题还是代码的问题。
好了,这一章的内容就这些。以太网CAPL编程其实不难,关键是理解数据结构的嵌套关系,以及养成逐层访问的习惯。下一章咱们会讲怎么用CAPL发送以太网报文,到时候你会看到,发送比接收还要简单一些。