简介这是一套面向计算机、通信工程、自动化、电子信息及物联网等专业学生与教师的网络流量在线分析系统源码基于C语言实现可作为毕业设计、课程设计、作业或项目初期立项演示也适合具备一定基础的学习者进阶研究。资源包共17个文件约207KB包含C与C源码、头文件、工程配置文件、可执行程序及编译中间产物并附有TCP、UDP、HTTP等协议分析文本与HTTP内容样本便于理解流量解析逻辑同时提供README说明和C、C系统部署文档帮助快速搭建运行环境。该项目为个人高分项目已通过导师指导与答辩评审功能经测试可正常运行。已有93人学习下载读者可据此掌握网络流量在线分析的整体架构、协议解析思路与工程组织方式并在此基础上修改扩展实现更多自定义功能。1. 从抓包到看板C 语言网络流量在线分析系统到底在解决什么问题很多人第一次听到「基于 C 语言的网络流量在线分析系统」脑子里浮现的是 Wireshark 那种图形界面。但真正落到工程里这个标题指向的是一类更朴素的东西一个常驻进程从网卡或抓包文件里持续拿数据包边收边解析实时吐出流量统计、协议分布、会话排行而不是先把几个 G 的 pcap 存下来再离线跑。它解决的核心痛点是「在线」两个字——数据包不等你缓冲区满了就丢所以解析路径必须足够快、内存必须可控。这套东西适合谁一类是做网络运维、想自己搭一套轻量流量看板的工程师不想为商业探针付费一类是学 C 语言学到指针、结构体、链表之后想找一个能把 c语言内存管理、c语言结构体、c语言链表真正用起来的实战项目还有一类是做嵌入式或网关设备的需要在资源受限环境里塞一个流量统计模块。标题里「全部资料齐全部署文档」说明它偏工程交付重点不在算法炫技而在能不能编译、能不能跑起来、能不能长期稳定。我下面按「先立住原理再动手复现最后讲坑」的顺序拆。抓包用 libpcap解析自己写以太网/IP/TCP 头部统计用哈希表加链表输出可以先落终端再考虑 Web。全程 C 语言不依赖重型框架vscode 配置 c语言环境就能开工。2. 抓包与协议解析把原始字节流拆成能统计的结构2.1 为什么选 libpcap 而不是自己写 raw socket在线分析的第一步是拿到包。常见做法有三种raw socket、AF_PACKET、libpcap。raw socket 需要自己处理链路层跨平台差异大AF_PACKET 是 Linux 专有性能好但不可移植libpcap 是事实标准tcpdump、Wireshark 底层都用它接口稳定支持抓包过滤表达式还能直接读 pcap 文件做回放测试。对一个要交付部署文档的项目来说libpcap 是最稳的选择装个libpcap-dev就能编译。抓包的核心是回调模型pcap_loop或pcap_dispatch每来一个包就调一次你的处理函数。这里有个关键决策——回调里绝对不要做耗时操作比如写磁盘、发网络请求。一旦回调阻塞内核缓冲区默认几 MB很快填满pcap_stats里的ps_drop就会飙升你统计到的流量就是残缺的。正确做法是回调里只做解析和计数重活丢给另一个线程或队列。#include pcap.h #include stdio.h /* 回调每收到一个包调用一次只做轻量解析 */ void packet_handler(u_char *user, const struct pcap_pkthdr *hdr, const u_char *bytes) { /* hdr-caplen 是实际抓到的长度hdr-len 是原始长度 */ if (hdr-caplen 14) return; /* 连以太网头都不够丢弃 */ /* user 参数可传统计上下文避免用全局变量 */ flow_stats_t *stats (flow_stats_t *)user; parse_ethernet(bytes, hdr-caplen, stats); } int main(void) { char errbuf[PCAP_ERRBUF_SIZE]; /* snaplen 设 65535 保证不截断promisc1 抓所有经过的包 */ pcap_t *handle pcap_open_live(eth0, 65535, 1, 1000, errbuf); if (!handle) { fprintf(stderr, open: %s\n, errbuf); return 1; } /* 只抓 IP 流量过滤表达式交给内核减少用户态负担 */ struct bpf_program fp; if (pcap_compile(handle, fp, ip, 0, PCAP_NETMASK_UNKNOWN) -1) { fprintf(stderr, compile: %s\n, pcap_geterr(handle)); return 1; } pcap_setfilter(handle, fp); flow_stats_t stats; stats_init(stats); /* cnt0 表示无限循环直到收到信号 */ pcap_loop(handle, 0, packet_handler, (u_char *)stats); pcap_close(handle); return 0; }逻辑说明pcap_open_live的 snaplen 参数决定每个包最多抓多少字节设小了会截断TCP 载荷统计就不准。pcap_compile把过滤表达式编译成 BPF 字节码下推到内核这样不匹配的包根本不会复制到用户态是性能优化的第一刀。参数上timeout 设 1000 毫秒是让pcap_loop有机会返回方便你处理退出信号设 0 会一直阻塞。2.2 以太网/IP/TCP 头部解析与字节序陷阱拿到bytes之后要一层层剥。以太网头固定 14 字节6 字节目的 MAC、6 字节源 MAC、2 字节类型。类型字段 0x0800 是 IPv40x86DD 是 IPv60x0806 是 ARP。这里第一个坑就是字节序——网络字节序是大端x86 是小端所有多字节字段都要ntohs/ntohl转换忘了转就会出现「端口号 80 变成 20480」这种玄学现象。IP 头长度不固定因为有选项字段。ihl首部长度字段单位是 4 字节所以真实长度是ihl * 4最小 20。协议字段告诉你上层是 TCP(6)、UDP(17) 还是 ICMP(1)。TCP 头同理data offset字段单位也是 4 字节。解析时每一步都要先检查剩余长度够不够否则越界读会直接段错误。#include arpa/inet.h #include netinet/ip.h #include netinet/tcp.h void parse_ethernet(const u_char *bytes, uint32_t caplen, flow_stats_t *s) { const struct ether_header *eth (struct ether_header *)bytes; uint16_t type ntohs(eth-ether_type); /* 必须转字节序 */ if (type ! ETHERTYPE_IP) return; /* 只处理 IPv4 */ const u_char *ip_start bytes 14; if (caplen 14 20) return; /* IP 头最小 20 字节 */ const struct ip *iph (const struct ip *)ip_start; uint32_t ip_hlen iph-ip_hl * 4; /* 单位是 4 字节 */ if (ip_hlen 20 || caplen 14 ip_hlen) return; uint32_t src ntohl(iph-ip_src.s_addr); uint32_t dst ntohl(iph-ip_dst.s_addr); uint16_t sport 0, dport 0; uint8_t proto iph-ip_p; if (proto IPPROTO_TCP) { const u_char *tcp_start ip_start ip_hlen; if (caplen 14 ip_hlen 20) return; const struct tcphdr *tcph (const struct tcphdr *)tcp_start; sport ntohs(tcph-th_sport); dport ntohs(tcph-th_dport); } /* 用五元组更新统计 */ stats_update(s, src, dst, sport, dport, proto, caplen); }逻辑说明每个return都是一道边界检查这是 C 语言解析网络数据必须养成的习惯——你永远不能假设包是完整的、合法的。参数上ip_hl和 TCP 的th_off都要乘 4这是协议规定的单位。stats_update里做五元组归一化比如源 IP 小于目的 IP 时交换保证双向流量算一条会话这是会话统计的关键。3. 统计引擎用哈希表加链表扛住高并发流量3.1 会话表的数据结构选型为什么不用数组流量统计的本质是「按某个 key 聚合」。key 可以是五元组会话、源 IP主机流量、协议号协议分布。最朴素的做法是数组但 IP 有 2^32 种可能端口有 65536 种数组直接爆内存。所以必须用哈希表。C 语言没有标准哈希表得自己写这正好是练 c语言结构体和 c语言链表的地方。我一般用「哈希桶 链表」的拉链法固定大小的桶数组比如 65536 个每个桶挂一条链表冲突的 key 挂在同一链表上。桶数选 2 的幂取模用位运算hash (BUCKET_NUM - 1)代替除法快很多。哈希函数用经典的 FNV-1a对五元组这种短 key 分布均匀。#define BUCKET_NUM 65536 /* 必须是 2 的幂 */ #define BUCKET_MASK (BUCKET_NUM - 1) typedef struct flow_entry { uint32_t src_ip, dst_ip; uint16_t src_port, dst_port; uint8_t proto; uint64_t packets; /* 包计数 */ uint64_t bytes; /* 字节计数 */ struct flow_entry *next; /* 链表指针 */ } flow_entry_t; typedef struct { flow_entry_t *buckets[BUCKET_NUM]; uint64_t total_flows; } flow_table_t; /* FNV-1a 哈希对短 key 分布好 */ static uint32_t fnv1a(const void *data, size_t len) { const uint8_t *p data; uint32_t h 2166136261u; for (size_t i 0; i len; i) { h ^ p[i]; h * 16777619u; } return h; }逻辑说明flow_entry_t里存了完整的五元组作为 key加上计数和链表指针。BUCKET_NUM取 65536 是经验值能撑住几十万并发会话而冲突可控。FNV-1a 的初始值和质数是标准常量别改。这里next指针就是 c语言链表 的典型用法插入用头插法 O(1)查找平均 O(1)。3.2 插入、查找与老化内存不能只增不减哈希表写好了但有个致命问题会话是有限的一条 TCP 连接结束后它的表项如果一直留着内存会无限涨。所以必须有老化aging机制。常见做法是给每个 entry 加一个last_seen时间戳起一个后台线程定期扫描超过阈值比如 300 秒没新包就删掉。扫描全表很慢可以分桶轮询每次扫一部分摊平开销。#include time.h #include stdlib.h #include string.h flow_entry_t *flow_lookup(flow_table_t *t, const flow_entry_t *key) { uint32_t h fnv1a(key, sizeof(uint32_t)*2 sizeof(uint16_t)*2 1); flow_entry_t *e t-buckets[h BUCKET_MASK]; while (e) { if (e-src_ip key-src_ip e-dst_ip key-dst_ip e-src_port key-src_port e-dst_port key-dst_port e-proto key-proto) return e; e e-next; } return NULL; } void stats_update(flow_table_t *t, uint32_t s, uint32_t d, uint16_t sp, uint16_t dp, uint8_t proto, uint32_t len) { flow_entry_t key {.src_ips, .dst_ipd, .src_portsp, .dst_portdp, .protoproto}; flow_entry_t *e flow_lookup(t, key); if (!e) { e calloc(1, sizeof(*e)); /* 新会话 */ if (!e) return; /* 内存不足时静默丢弃别崩 */ *e key; uint32_t h fnv1a(key, sizeof(uint32_t)*2 sizeof(uint16_t)*2 1); e-next t-buckets[h BUCKET_MASK]; /* 头插 */ t-buckets[h BUCKET_MASK] e; t-total_flows; } e-packets; e-bytes len; e-last_seen time(NULL); }逻辑说明flow_lookup先算哈希再遍历链表比对完整五元组不能只比哈希否则哈希冲突会算错。stats_update里calloc失败时直接返回而不是 abort因为在线系统不能因为一次内存分配失败就整个挂掉。参数上last_seen用time(NULL)秒级精度够用如果要做毫秒级老化可以换clock_gettime。提示老化线程和抓包线程会同时访问哈希表必须加锁。用读写锁pthread_rwlock_t比互斥锁好因为读多写少。但锁粒度要细别锁整张表可以每个桶一把锁。4. 避坑与排查在线分析系统最容易翻车的五个地方4.1 现象跑几分钟后丢包率飙升统计数字对不上原因回调函数里做了耗时操作或者统计逻辑里有锁竞争导致pcap_loop处理不过来内核缓冲区溢出。也可能是 snaplen 设太小大包被截断后解析失败被丢弃。解决先用pcap_stats打印ps_recv和ps_drop确认是不是真丢包。如果是把回调里的日志、磁盘 IO 全部移走只留内存操作。锁改成每桶一把读写锁。snaplen 统一设 65535。还可以调大内核缓冲区pcap_set_buffer_size设成 16MB 以上。4.2 现象端口号显示成 20480、IP 显示成乱码原因忘了字节序转换。网络字节序是大端x86 是小端ntohs/ntohl漏了任何一个多字节字段都会出这种问题。20480 正好是 80 左移 8 位是典型症状。解决养成习惯凡是struct里从网络包直接读出来的多字节字段用之前一律ntohs/ntohl。打印时用inet_ntop把uint32_t转成点分十进制别自己拼字符串。4.3 现象程序跑几小时后内存涨到几个 G 被 OOM kill原因会话表只增不删老化机制没生效或者阈值设太大。也可能是calloc的 entry 在删除时没free内存泄漏。解决确认老化线程真的在跑加日志打印当前total_flows。老化阈值按业务定一般 TCP 300 秒、UDP 60 秒。删除时先摘链表再free顺序反了会野指针。用 valgrind 跑一遍确认无泄漏。4.4 现象编译报pcap.h: No such file or directory原因没装 libpcap 开发包。运行库和开发头文件是两个包只装运行库编译不过。解决Debian/Ubuntu 装libpcap-devCentOS 装libpcap-devel。编译时加-lpcap。vscode 里配 c语言环境时在tasks.json的 args 里加上-lpcap在c_cpp_properties.json的 includePath 里加上/usr/include。4.5 现象抓不到任何包pcap_loop一直不回调原因网卡名写错或者权限不够。抓包需要 root 或CAP_NET_RAW能力。也可能是网卡处于 down 状态或者过滤表达式把所有包都过滤掉了。解决用pcap_findalldevs列出所有可用网卡名别硬编码eth0现在很多机器是ens33、enp0s3。用sudo跑或者setcap cap_net_rawep ./your_program赋权。过滤表达式先用空字符串测试确认能抓到再逐步加条件。5. 从终端统计到可视化让这套系统真正能交付5.1 输出格式与性能的平衡统计出来了怎么展示最省事的是定时打印到终端但生产环境更常见的是暴露一个 HTTP 接口或者写时序数据库。这里有个取舍如果每来一个包就更新一次全局计数器并加锁性能会被锁拖垮。我的做法是每个抓包线程维护本地计数器定时比如每秒汇总到全局这样锁竞争降到每秒一次。/* 每线程本地统计避免每包加锁 */ typedef struct { uint64_t pkt_cnt; uint64_t byte_cnt; flow_table_t local_table; /* 也可以每线程一张表定期合并 */ } thread_stat_t; /* 定时器线程每秒把本地统计汇总到全局并输出 */ void *report_thread(void *arg) { thread_stat_t *ts (thread_stat_t *)arg; while (running) { sleep(1); pthread_mutex_lock(g_lock); g_total_pkt ts-pkt_cnt; g_total_byte ts-byte_cnt; ts-pkt_cnt 0; /* 清零下个周期重新累计 */ ts-byte_cnt 0; pthread_mutex_unlock(g_lock); printf(pps%llu, bps%llu\n, (unsigned long long)g_total_pkt, (unsigned long long)g_total_byte * 8); } return NULL; }逻辑说明本地计数器用普通变量不加锁因为只有本线程写。汇总时加锁锁持有时间极短。bps是比特每秒乘 8 是因为byte_cnt统计的是字节。参数上sleep(1)的粒度决定了看板刷新率要更细可以换nanosleep。5.2 验证统计准不准用 tcpreplay 做回放对比系统写完了怎么证明它统计得对最可靠的办法是用tcpreplay把一份已知的 pcap 文件回放到网卡然后对比你的统计结果和tcpdump或 Wireshark 的统计。比如 pcap 里有 10000 个包、总字节数 8MB你的系统跑完应该输出接近这个数。差太多就说明有丢包或解析错误。验证项工具期望结果总包数tcpreplay tcpdump两者差值小于 0.1%总字节数Wireshark Statistics与系统输出一致会话数tcpdump 去重五元组数量级一致协议分布Wireshark Protocol Hierarchy各协议占比接近回放时注意tcpreplay --loop0 --pps1000控制速率别用最大速率否则网卡和内核都扛不住丢包是你自己造成的不是系统的锅。验证通过后再上真实流量。5.3 部署文档该写什么让接手的人十分钟跑起来标题里强调「部署文档」这东西的价值在于别人拿到代码能跑起来。我一般会写清楚依赖包及版本libpcap、gcc、make、编译命令、运行命令及参数说明、网卡名怎么查、权限怎么配、常见报错对照表。别写「安装依赖」四个字就完事要给出具体命令。比如apt-get install -y libpcap-dev build-essential。运行参数用getopt解析支持-i指定网卡、-f指定过滤表达式、-o指定输出文件这样部署时不用改代码。最后说个我自己的习惯这套系统我从来不在回调里写printf。早期图省事在回调里打日志结果流量一上来光printf的锁就把性能拖垮了丢包丢到怀疑人生。后来改成环形缓冲区抓包线程只写内存单独的日志线程负责落盘世界才清净。做在线系统记住一句话——回调里只做非做不可的事其他全部异步。希望帮到你。本文还有配套的精品资源点击获取