You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在C程序中准确获取以太网帧的到达时间及相关实现疑问

以太网帧到达时间获取指南(C程序实现)

我来帮你梳理一下关于以太网帧到达时间的问题,这在抓包或性能分析场景里挺常见的:

一、标准以太网帧结构体包含到达时间字段吗?

答案是不包含

比如Linux系统中常用的以太网头部结构体struct ethhdr,完全对应链路层帧的原始内容,定义如下:

struct ethhdr {
    unsigned char h_dest[ETH_ALEN]; /* 目的MAC地址 */
    unsigned char h_source[ETH_ALEN]; /* 源MAC地址 */
    __be16 h_proto; /* 上层协议类型(比如IP是0x0800) */
};

这里面没有任何时间相关的字段——因为时间戳属于接收元数据,是内核或网卡在捕获帧时额外添加的,不属于帧本身的链路层数据。

二、如果没有内置时间戳,怎么“推导”到达时间?

如果暂时没办法获取内核/硬件提供的时间戳,你只能在调用接收函数(比如recv()recvfrom())的瞬间记录当前系统时间,但这其实是「你的程序拿到帧数据的时间」,不是帧真正到达网卡/内核的时间。

中间会有内核调度、socket缓冲区排队等延迟,误差可能从几微秒到几毫秒不等,仅适合对精度要求不高的场景。示例代码如下:

#include <sys/time.h>

// 接收帧后立即记录时间
struct timeval tv;
gettimeofday(&tv, NULL);
printf("帧接收近似时间:%ld.%06ld\n", tv.tv_sec, tv.tv_usec);

// 或者用更高精度的clock_gettime(支持纳秒)
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
printf("帧接收近似时间:%ld.%09ld\n", ts.tv_sec, ts.tv_nsec);

三、获取到达时间的最准确方法

最靠谱的方式是让内核或硬件直接给帧打时间戳,这是在帧刚到达网卡/内核时记录的,延迟最小,精度最高。下面分两种常用场景说明:

1. 使用Libpcap库(推荐,简单易用)

Libpcap是抓包领域的标准库,它会自动从内核获取帧的到达时间戳,存储在struct pcap_pkthdr结构体中。示例代码:

#include <pcap.h>
#include <stdio.h>
#include <net/ethernet.h>

void packet_handler(u_char *user_data, const struct pcap_pkthdr *pkthdr, const u_char *packet) {
    // pkthdr->ts 就是帧到达的精确时间戳
    printf("帧到达时间:%ld.%06ld\n", pkthdr->ts.tv_sec, pkthdr->ts.tv_usec);
    
    // 解析以太网帧头部
    struct ethhdr *eth_frame = (struct ethhdr*)packet;
    // 后续处理逻辑...
}

int main() {
    pcap_t *handle;
    char errbuf[PCAP_ERRBUF_SIZE];
    
    // 打开目标网卡(比如"eth0",根据你的设备修改)
    handle = pcap_open_live("eth0", BUFSIZ, 1, 1000, errbuf);
    if (!handle) {
        fprintf(stderr, "无法打开网卡:%s\n", errbuf);
        return 1;
    }
    
    // 开始捕获帧,每收到一帧就调用packet_handler处理
    pcap_loop(handle, 0, packet_handler, NULL);
    
    pcap_close(handle);
    return 0;
}

默认情况下时间戳是微秒级,部分系统和网卡支持纳秒精度,你可以通过Libpcap的配置选项开启。

2. 使用Raw Socket + 内核时间戳(底层开发场景)

如果需要直接操作raw socket而不用Libpcap,可以通过设置socket选项让内核给每个收到的帧附加时间戳,然后用recvmsg()提取。示例代码片段:

#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 创建raw socket,接收所有以太网帧
    int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sockfd < 0) {
        perror("socket创建失败");
        return 1;
    }
    
    // 启用纳秒精度时间戳
    int opt = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_TIMESTAMPNS, &opt, sizeof(opt)) < 0) {
        perror("设置SO_TIMESTAMPNS失败");
        close(sockfd);
        return 1;
    }
    
    unsigned char buffer[ETH_FRAME_LEN];
    struct msghdr msg;
    struct iovec iov;
    char ctrl_buf[CMSG_SPACE(sizeof(struct timespec))];
    
    // 初始化msghdr结构,用于接收数据和控制信息(时间戳)
    iov.iov_base = buffer;
    iov.iov_len = sizeof(buffer);
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = ctrl_buf;
    msg.msg_controllen = sizeof(ctrl_buf);
    msg.msg_flags = 0;
    
    // 接收帧
    ssize_t len = recvmsg(sockfd, &msg, 0);
    if (len < 0) {
        perror("recvmsg失败");
        close(sockfd);
        return 1;
    }
    
    // 从控制信息中提取时间戳
    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
    if (cmsg != NULL && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPNS) {
        struct timespec *ts = (struct timespec*)CMSG_DATA(cmsg);
        printf("帧到达时间:%ld.%09ld\n", ts->tv_sec, ts->tv_nsec);
    }
    
    // 解析以太网帧头部
    struct ethhdr *eth_frame = (struct ethhdr*)buffer;
    // 后续处理逻辑...
    
    close(sockfd);
    return 0;
}

如果你的网卡支持硬件时间戳,还可以进一步设置SO_TIMESTAMPING选项,让时间戳由网卡硬件直接生成,精度能达到纳秒级,几乎消除软件延迟。


内容的提问来源于stack exchange,提问作者pelican rouge

火山引擎 最新活动