多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Linux C语言网络抓包实战:基于libpcap实现协议分析与流量捕获

Linux C语言网络抓包实战:基于libpcap实现协议分析与流量捕获 1. 项目概述与核心价值最近在后台和社群里看到不少朋友对网络协议分析很感兴趣尤其是想自己动手写点东西而不是仅仅停留在使用Wireshark这样的图形化工具上。确实Wireshark功能强大但它的底层抓包能力尤其是在Linux环境下很大程度上依赖于一个名为libpcap的C语言库。今天我就想和大家聊聊如何抛开Wireshark的GUI外壳直接用C语言在Linux系统上实现最核心的网络数据包捕获功能。这不仅仅是“造轮子”更是一次深入理解网络栈、系统调用以及数据包生命周期的绝佳实践。无论你是想为嵌入式设备开发轻量级监控工具还是希望深入网络安全领域编写自己的嗅探器亦或是单纯想巩固C语言和Linux系统编程的知识这个项目都能给你带来实实在在的收获。整个过程我们将聚焦于libpcap库的使用从环境搭建、原理剖析到代码实现一步步拆解最终完成一个可以捕获并简单解析网络流量的命令行工具。2. 环境准备与核心库libpcap解析2.1 为什么选择libpcap在动手之前我们必须先搞清楚工具链。在Linux上抓包本质上需要内核的配合。最原始的方式是使用PF_PACKET套接字但这需要处理大量的底层细节比如帧对齐、过滤规则的手动实现等复杂度很高。而libpcapPacket Capture library的出现正是为了封装这些底层复杂性为应用程序提供一个统一、高效且跨平台的抓包接口。Wireshark以及其命令行版本tshark的抓包引擎就是基于libpcap在Linux上或WinPcap在Windows上构建的。因此学习使用libpcap就等于掌握了Wireshark抓包功能的核心。要使用libpcap首先得确保你的Linux系统已经安装了它。绝大多数主流发行版如Ubuntu、CentOS、Fedora都自带了libpcap的开发包。安装命令示例Ubuntu/Debian:sudo apt-get update sudo apt-get install libpcap-devCentOS/RHEL/Fedora:sudo yum install libpcap-devel或sudo dnf install libpcap-devel安装成功后你就可以在C代码中通过#include pcap.h来引用相关的头文件和函数了。同时编译时需要链接libpcap库通常使用-lpcap参数。2.2 开发环境搭建与第一个测试我个人的开发习惯是使用VSCode配合gcc。首先确保gcc编译器已安装。然后创建一个简单的测试文件test_pcap.c来验证环境是否正常。// test_pcap.c - 验证libpcap环境 #include stdio.h #include pcap.h int main() { char errbuf[PCAP_ERRBUF_SIZE]; // libpcap专用的错误信息缓冲区 // 获取第一个可用的网络设备名 char *device pcap_lookupdev(errbuf); if (device NULL) { fprintf(stderr, 找不到网络设备: %s\n, errbuf); return 1; } printf(找到网络设备: %s\n, device); return 0; }使用以下命令编译并运行gcc -o test_pcap test_pcap.c -lpcap ./test_pcap如果输出类似于找到网络设备: eth0或找到网络设备: wlan0那么恭喜你libpcap开发环境已经就绪。这里的pcap_lookupdev()是一个辅助函数用于自动找到一个可能用于抓包的设备。但在实际生产代码中更推荐使用pcap_findalldevs()来列出所有设备让用户选择因为自动选择的设备可能不是我们想要的比如一个没有流量的本地回环接口。注意普通用户通常没有权限直接访问原始网络套接字进行抓包。因此编译出的可执行文件需要以root权限运行例如使用sudo。这是Linux系统出于安全考虑的限制。后续的所有示例代码在运行时都需要注意这一点。3. 抓包程序的核心架构与实现一个完整的、基于libpcap的抓包程序其工作流程可以概括为四个核心步骤1. 选择设备2. 打开设备3. 设置过滤规则可选4. 开始捕获并处理数据包。下面我们逐一拆解。3.1 设备发现与选择如前所述pcap_lookupdev()过于简单。更健壮的方式是使用pcap_findalldevs()。#include stdio.h #include pcap.h void list_devices() { pcap_if_t *alldevs; // 设备列表指针 char errbuf[PCAP_ERRBUF_SIZE]; // 获取所有设备列表 if (pcap_findalldevs(alldevs, errbuf) -1) { fprintf(stderr, 获取设备列表失败: %s\n, errbuf); return; } printf(可用的网络设备:\n); pcap_if_t *d; int i 0; // 遍历设备列表 for (d alldevs; d ! NULL; d d-next) { printf(%d. %s, i, d-name); if (d-description) { printf( (%s)\n, d-description); // 打印设备描述 } else { printf( (无描述)\n); } } if (i 0) { printf(未找到任何设备请检查权限或驱动。\n); } // 释放设备列表内存 pcap_freealldevs(alldevs); }这段代码会列出系统上所有可用于抓包的接口。你会看到像eth0、wlan0、any一个特殊的虚拟设备可以捕获所有接口的流量以及lo本地回环等设备。3.2 打开设备与设置抓包参数选定设备例如eth0后我们需要用pcap_open_live()打开它。这是最关键的函数之一。pcap_t *handle; // pcap会话句柄后续所有操作都围绕它进行 char *dev eth0; char errbuf[PCAP_ERRBUF_SIZE]; // 打开网络设备进行抓包 handle pcap_open_live(dev, // 设备名 BUFSIZ, // 捕获每个数据包的最大字节数 1, // 混杂模式 (1开启0关闭) 1000, // 超时时间毫秒 errbuf); // 错误缓冲区 if (handle NULL) { fprintf(stderr, 无法打开设备 %s: %s\n, dev, errbuf); return; }参数详解dev: 设备名称字符串。snaplen(BUFSIZ): 抓取的每个数据包的最大长度。BUFSIZ是标准C库定义的一个缓冲区大小通常为8192。设置这个值是为了性能考虑我们可能只关心包头而不需要完整的应用层数据比如一个大文件传输的TCP负载。对于协议分析65535PCAP_ERRBUF_SIZE定义的值是一个常见选择以确保捕获完整报文。promisc(1): 是否开启混杂模式。网卡通常只接收发给本机MAC地址、广播或组播的帧。开启混杂模式后网卡会接收所有流经其物理介质的帧这对于网络监控和分析至关重要。注意在某些网络或虚拟化环境中即使开启混杂模式也可能无法捕获其他主机的流量这取决于交换机配置或虚拟网络设备的设置。to_ms(1000): 读取超时时间毫秒。这个参数影响pcap_dispatch()或pcap_loop()的行为。设置为0意味着没有超时读取可能会一直阻塞直到有数据包到达设置为-1则行为未定义。设置为一个正数如1000意味着函数会等待指定的毫秒数来收集数据包然后返回。这对于需要定期执行其他任务的程序很有用。errbuf: 存储错误信息的缓冲区。实操心得snaplen的设置需要权衡。设置太小会截断数据包丢失高层协议信息设置太大会增加内存拷贝开销影响性能。对于一般性网络监控65535是安全且通用的选择。to_ms在编写交互式或需要控制循环的工具时很有用但对于一个简单的、持续抓包直到用户中断的程序设置为0或一个较大的值如1000都是可以的。3.3 编译与设置过滤器直接捕获所有流量会产生海量数据。libpcap允许我们设置一个BPFBerkeley Packet Filter过滤器只捕获我们感兴趣的流量。这类似于在Wireshark的过滤栏中输入表达式。struct bpf_program fp; // 编译后的过滤器程序 char filter_exp[] tcp port 80; // 过滤表达式只抓TCP 80端口(HTTP)流量 bpf_u_int32 netmask; // 设备的网络掩码用于优化过滤器 // 假设我们已经通过pcap_lookupnet()获取了设备的IP和netmask此处简化 // 在实际代码中应先调用 pcap_lookupnet(dev, net, netmask, errbuf) // 编译过滤器 if (pcap_compile(handle, fp, filter_exp, 0, netmask) -1) { fprintf(stderr, 无法解析过滤器 %s: %s\n, filter_exp, pcap_geterr(handle)); pcap_close(handle); return; } // 应用过滤器到当前会话 if (pcap_setfilter(handle, fp) -1) { fprintf(stderr, 无法安装过滤器 %s: %s\n, filter_exp, pcap_geterr(handle)); pcap_freecode(fp); pcap_close(handle); return; } // 释放编译后的过滤器代码所占用的内存 pcap_freecode(fp);BPF过滤器表达式功能非常强大支持协议ip,arp,tcp,udp,icmp、主机host 192.168.1.1、端口port 80、网络net 192.168.1.0/24以及逻辑运算符and,or,not。例如icmp: 只抓ICMP包如ping。host 10.0.0.5 and not port 22: 抓取发往或来自10.0.0.5且不是SSH端口的流量。dst net 192.168.0.0/24: 抓取目标网络为192.168.0.0/24的流量。3.4 捕获循环与数据包处理设备打开、过滤器设置好后就进入主循环——捕获并处理数据包。libpcap提供了两种主要方式pcap_loop()和pcap_dispatch()。两者都要求你提供一个回调函数当抓到数据包时libpcap会调用这个函数。这里我们使用pcap_loop()它会持续抓包直到抓满指定数量或发生错误。// 数据包处理回调函数的原型 void packet_handler(u_char *args, const struct pcap_pkthdr *header, const u_char *packet); // 开始抓包循环捕获最多100个包 pcap_loop(handle, 100, packet_handler, NULL);pcap_loop参数handle: 打开的pcap会话句柄。count(100): 捕获数据包的数量上限。设置为-1或0表示无限循环直到出错或被pcap_breakloop()中断。packet_handler: 回调函数指针。user_args(NULL): 传递给回调函数的用户自定义参数。现在让我们实现核心的packet_handler函数。这个函数负责解析和打印每个捕获到的数据包的基本信息。#include netinet/ip.h // 提供IP头结构体struct ip #include netinet/tcp.h // 提供TCP头结构体struct tcphdr #include arpa/inet.h // 提供inet_ntoa等函数 void packet_handler(u_char *args, const struct pcap_pkthdr *header, const u_char *packet) { static int count 0; // 包计数器 count; // 打印基本信息序号、时间戳、包长度、捕获长度 printf(\n 数据包 #%d \n, count); printf(捕获时间: %s, ctime((const time_t*)header-ts.tv_sec)); printf(数据包长度: %d 字节\n, header-len); printf(捕获长度: %d 字节\n, header-caplen); // 检查捕获长度是否小于数据包长度可能因snaplen设置被截断 if (header-caplen header-len) { printf([警告] 数据包被截断\n); } // 解析以太网帧头 (假设是以太网环境) struct ethhdr *eth (struct ethhdr *)packet; printf(以太网类型: 0x%04x\n, ntohs(eth-h_proto)); // 只处理IPv4包 (以太网类型 0x0800) if (ntohs(eth-h_proto) ETH_P_IP) { // 跳过以太网头(14字节)定位到IP头 struct ip *iph (struct ip*)(packet sizeof(struct ethhdr)); char src_ip[INET_ADDRSTRLEN]; char dst_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, (iph-ip_src), src_ip, INET_ADDRSTRLEN); inet_ntop(AF_INET, (iph-ip_dst), dst_ip, INET_ADDRSTRLEN); printf(IP协议: %s - %s\n, src_ip, dst_ip); printf(IP协议号: %d\n, iph-ip_p); // 6TCP, 17UDP, 1ICMP // 只处理TCP包 if (iph-ip_p IPPROTO_TCP) { // 计算IP头长度 (IHL字段单位是4字节) int ip_header_len iph-ip_hl * 4; // 定位TCP头 packet 以太网头 IP头长度 struct tcphdr *tcph (struct tcphdr*)(packet sizeof(struct ethhdr) ip_header_len); printf(TCP端口: %d - %d\n, ntohs(tcph-source), ntohs(tcph-dest)); printf(TCP标志: ); if (tcph-syn) printf(SYN ); if (tcph-ack) printf(ACK ); if (tcph-fin) printf(FIN ); if (tcph-rst) printf(RST ); if (tcph-psh) printf(PSH ); if (tcph-urg) printf(URG ); printf(\n); } // 可以类似地添加UDP和ICMP的处理逻辑 } // 这里可以添加对ARP(0x0806)等其他协议的处理 }代码解析与注意事项pcap_pkthdr结构体包含了libpcap添加的元数据主要是时间戳(ts)、数据包原始长度(len)和捕获长度(caplen)。协议头解析我们手动跳过了以太网帧头14字节然后解析IP头。这里使用了系统定义的struct ip和struct tcphdr。非常重要的一点是这些结构体中的多字节字段如端口号、IP地址、长度字段是网络字节序大端序必须使用ntohs()16位或ntohl()32位函数转换为主机字节序后才能正确解读。IP头长度IP头的ip_hl字段表示IP头长度单位是4字节。所以实际的IP头字节数是ip_hl * 4。这是定位上层协议如TCP、UDP头起始位置的关键。数据对齐与通用性上面的代码假设了以太网帧头是标准的14字节并且没有802.1Q VLAN标签。在实际更健壮的工具中需要根据eth-h_proto来动态判断并计算网络层头的起始位置。例如如果h_proto是ETH_P_8021Q(0x8100)则需要跳过额外的4字节VLAN标签。回调函数性能packet_handler会在每个数据包到达时被调用。因此这个函数内的操作应尽可能高效。避免在回调函数内进行复杂的计算、大量的内存分配或阻塞式I/O如写入慢速的磁盘文件否则在高流量下可能导致丢包。如果需要进行耗时操作最好将数据包信息存入队列由另一个线程处理。4. 完整示例程序与编译运行将上述所有步骤整合我们得到一个完整的、可以编译运行的简易抓包程序simple_sniffer.c。// simple_sniffer.c #include stdio.h #include stdlib.h #include string.h #include time.h #include pcap.h #include netinet/ip.h #include netinet/tcp.h #include netinet/if_ether.h #include arpa/inet.h void packet_handler(u_char *args, const struct pcap_pkthdr *header, const u_char *packet) { static long count 0; count; printf(\n[包 #%ld] 长度: %d/%d 字节 | , count, header-caplen, header-len); struct ethhdr *eth (struct ethhdr *)packet; u_short eth_type ntohs(eth-h_proto); if (eth_type ETH_P_IP) { // IPv4 struct ip *iph (struct ip*)(packet sizeof(struct ethhdr)); char src_ip[INET_ADDRSTRLEN], dst_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, (iph-ip_src), src_ip, INET_ADDRSTRLEN); inet_ntop(AF_INET, (iph-ip_dst), dst_ip, INET_ADDRSTRLEN); printf(IP: %s - %s | 协议: , src_ip, dst_ip); if (iph-ip_p IPPROTO_TCP) { int iphdr_len iph-ip_hl * 4; struct tcphdr *tcph (struct tcphdr*)(packet sizeof(struct ethhdr) iphdr_len); printf(TCP %d-%d , ntohs(tcph-source), ntohs(tcph-dest)); // 简单打印标志位 if (tcph-syn !tcph-ack) printf([SYN] ); else if (tcph-syn tcph-ack) printf([SYN-ACK] ); else if (tcph-fin) printf([FIN] ); else if (tcph-rst) printf([RST] ); else printf([Data/ACK] ); } else if (iph-ip_p IPPROTO_UDP) { printf(UDP); } else if (iph-ip_p IPPROTO_ICMP) { printf(ICMP); } else { printf(Other(%d), iph-ip_p); } } else if (eth_type ETH_P_ARP) { printf(ARP); } else { printf(EthType: 0x%04x, eth_type); } fflush(stdout); // 立即输出避免缓冲区延迟 } int main(int argc, char *argv[]) { char *dev NULL; char errbuf[PCAP_ERRBUF_SIZE]; pcap_t *handle; struct bpf_program fp; char filter_exp[] ip; // 默认过滤所有IP流量 bpf_u_int32 net; // 1. 获取设备列表或使用指定设备 if (argc 2) { dev argv[1]; } else { dev pcap_lookupdev(errbuf); if (dev NULL) { fprintf(stderr, 未指定设备且自动查找失败: %s\n, errbuf); return 2; } } printf(使用设备: %s\n, dev); // 2. 打开设备 // snaplen设为65535保证抓完整包开启混杂模式超时1秒 handle pcap_open_live(dev, 65535, 1, 1000, errbuf); if (handle NULL) { fprintf(stderr, 无法打开设备 %s: %s\n, dev, errbuf); return 2; } // 3. 获取网络地址和掩码用于优化过滤器 if (pcap_lookupnet(dev, net, netmask, errbuf) -1) { fprintf(stderr, 无法获取网络掩码: %s\n, errbuf); netmask 0; } // 4. 编译并设置过滤器 if (pcap_compile(handle, fp, filter_exp, 0, netmask) -1) { fprintf(stderr, 过滤器编译错误: %s\n, pcap_geterr(handle)); pcap_close(handle); return 2; } if (pcap_setfilter(handle, fp) -1) { fprintf(stderr, 过滤器设置错误: %s\n, pcap_geterr(handle)); pcap_freecode(fp); pcap_close(handle); return 2; } pcap_freecode(fp); printf(开始捕获IP流量 (CtrlC 停止)...\n); // 5. 开始捕获循环 (无限循环) pcap_loop(handle, 0, packet_handler, NULL); // 6. 清理 (pcap_loop在正常结束或出错后返回) pcap_close(handle); return 0; }编译与运行# 编译 gcc -o simple_sniffer simple_sniffer.c -lpcap # 运行 (需要root权限) sudo ./simple_sniffer # 或者指定设备例如监听wlan0 sudo ./simple_sniffer wlan0运行后程序会开始打印捕获到的每个IP数据包的基本信息。你可以打开浏览器访问网页或者ping一个地址观察控制台的输出变化。5. 进阶话题与性能优化一个基础的抓包工具已经成型但要将其变得实用、健壮还需要考虑更多。5.1 处理链路层多样性我们的示例假设了标准的以太网Ethernet II帧。但在实际中你可能会遇到Linux的“cooked”捕获模式SOCKET_DGRAM、802.11无线帧、或者带VLAN标签的帧。libpcap的pcap_datalink()函数可以返回当前捕获句柄的链路层类型。int link_type pcap_datalink(handle); switch (link_type) { case DLT_EN10MB: // 以太网 printf(链路类型: 以太网 (EN10MB)\n); break; case DLT_LINUX_SLL: // Linux “cooked” 捕获 printf(链路类型: Linux Cooked\n); // 此时帧头是sll_header结构不是ethhdr break; case DLT_IEEE802_11_RADIO: // 802.11 with radio information printf(链路类型: 802.11 with Radiotap\n); break; default: printf(不支持的链路类型: %d\n, link_type); pcap_close(handle); return; }根据不同的link_type在packet_handler中需要使用不同的偏移量和方法来解析网络层头部。libpcap的文档和tcpdump源码是学习如何处理各种链路类型的绝佳资料。5.2 异步处理与性能考量pcap_loop()是阻塞的。对于需要同时处理用户输入例如输入命令停止抓包或更新UI的应用程序这不够友好。libpcap提供了非阻塞模式结合select()/poll()的用法。// 设置非阻塞模式 if (pcap_setnonblock(handle, 1, errbuf) -1) { fprintf(stderr, 设置非阻塞失败: %s\n, errbuf); } int pcap_fd pcap_get_selectable_fd(handle); // 获取文件描述符 if (pcap_fd -1) { fprintf(stderr, 无法获取selectable fd\n); } // 在主循环中使用select/poll监听pcap_fd fd_set readfds; FD_ZERO(readfds); FD_SET(pcap_fd, readfds); struct timeval timeout; timeout.tv_sec 1; timeout.tv_usec 0; int ret select(pcap_fd 1, readfds, NULL, NULL, timeout); if (ret -1) { // 错误处理 } else if (ret 0) { // 超时可以处理其他任务 } else if (FD_ISSET(pcap_fd, readfds)) { // 有数据包到达使用pcap_dispatch处理 pcap_dispatch(handle, -1, packet_handler, NULL); }性能优化点增大缓冲区pcap_set_buffer_size(handle, buffer_size_in_bytes)可以设置内核缓冲区大小。在高流量场景下增大缓冲区可以减少因应用程序处理不及时而导致的丢包。使用pcap_dispatch()与pcap_loop()类似但pcap_dispatch()只处理一批数据包然后返回更适合在非阻塞或事件驱动架构中集成。回调函数轻量化再次强调packet_handler要快。可以考虑只做最必要的拷贝和标记将深度解析和输出工作交给其他线程。过滤器前置尽可能使用BPF过滤器在内核层面过滤掉不关心的数据包这是提升性能最有效的手段避免了无用的数据在用户空间和内核空间之间的拷贝。5.3 将抓包结果保存为PCAP文件libpcap提供了直接写入标准.pcap文件格式的函数这使得我们的工具抓到的包可以用Wireshark打开进行更深入的分析。pcap_dumper_t *pcap_dump_file; // ... 打开设备、设置过滤器等步骤之后 ... // 打开一个pcap文件用于写入 pcap_dump_file pcap_dump_open(handle, capture.pcap); if (pcap_dump_file NULL) { fprintf(stderr, 无法打开输出文件: %s\n, pcap_geterr(handle)); pcap_close(handle); return; } // 修改packet_handler将包写入文件 void packet_handler(u_char *dump_handle, const struct pcap_pkthdr *header, const u_char *packet) { // 将数据包写入pcap文件 pcap_dump(dump_handle, header, packet); // ... 同时也可以进行屏幕输出 ... } // 在主函数中将pcap_dump_file作为参数传递给pcap_loop pcap_loop(handle, 0, packet_handler, (u_char *)pcap_dump_file); // 循环结束后关闭dump文件 pcap_dump_close(pcap_dump_file);6. 常见问题排查与调试技巧在实际开发和使用过程中你肯定会遇到各种问题。这里记录一些典型的坑和解决方法。问题1编译时提示undefined reference to pcap_xxx原因没有正确链接libpcap库。解决确保编译命令末尾加上了-lpcap。问题2运行时报错找不到网络设备或无法打开设备原因A权限不足。抓包需要root权限。解决A使用sudo运行程序。原因B设备名错误或接口未启动。解决B运行ip link show或ifconfig确认可用接口名。对于无线网卡可能需要先sudo ip link set wlan0 up启动它。问题3能抓到包但解析出来的协议或端口号是乱码/巨大数字原因没有进行字节序转换。网络字节序是大端序而x86/x86-64 CPU是小端序。解决对所有从网络包中读取的、超过1字节的整型字段如端口号tcph-source、IP地址iph-ip_src.s_addr、以太网类型eth-h_proto都必须使用ntohs()或ntohl()进行转换。问题4程序在高流量下崩溃或大量丢包原因Apacket_handler回调函数处理太慢。解决A优化回调函数只做最小化工作如存入环形缓冲区将解析和输出移到另一个线程。原因B内核缓冲区太小。解决B使用pcap_set_buffer_size()将缓冲区设置为更大的值例如128MB。原因C过滤器不够精确捕获了太多无关流量。解决C收紧BPF过滤器表达式。问题5抓不到其他主机的流量混杂模式无效原因在交换网络环境中交换机不会将其他主机的单播流量转发到你的端口。无线网络中也可能因为驱动或硬件限制无法进入真正的监控模式。解决有线网络在交换机上配置端口镜像SPAN/RSPAN或者使用集线器现已罕见。无线网络尝试将网卡设置为监控模式sudo airmon-ng start wlan0这通常需要aircrack-ng套件和支持的网卡驱动。注意监控模式与普通的混杂模式不同。调试技巧从简单开始先把过滤器设为icmp然后ping一个地址看是否能抓到并正确解析ICMP包。这是验证链路层解析逻辑的好方法。使用tcpdump验证在另一个终端运行sudo tcpdump -i eth0 -nn对比你的程序输出和tcpdump的输出能快速定位是抓包问题还是解析问题。打印原始字节在解析出错时可以在packet_handler开头打印数据包的前64个字节十六进制与Wireshark抓到的同一包进行对比。for(int i0; iheader-caplen i64; i) { printf(%02x , packet[i]); if((i1)%16 0) printf(\n); } printf(\n);通过这个项目我们不仅实现了一个功能性的抓包工具更重要的是深入理解了网络数据包从网卡到应用程序的旅程以及libpcap这个强大库的工作原理。你可以在此基础上继续扩展比如添加HTTP协议解析、实现流量统计、构建简单的入侵检测规则甚至尝试绕过libpcap直接使用PF_PACKET套接字来追求极致的性能和控制力。网络编程的世界很深但每一步实践都会让你对“数据如何在网络中流动”有更清晰的认识。
返回列表