Linux C Socket 编程

1 Socket 是什么

Socket(套接字),就是对 网络上进程通信端点抽象。一个 Socket 就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制

从所处的位置来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信交互的接口。如下图所示:

2 Socket 类型

2.1 标准套接字

标准套接字是在传输层使用的套接字,分为流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)。

标准套接字在接收和发送时只能操作数据部分(TCP Payload / UDP Payload),而不能对 IP 首部或TCP 首部和 UDP 首部进行操作。

2.1.1 流套接字(SOCK_STREAM)

流套接字(SOCK_STREAM)用于提供 面向连接(可靠)的数据传输服务。

流套接字保证数据能够实现无差错、无重复发数据,并按顺序接收。

流套接字(SOCK_STREAM)使用 TCP(The Transmission Control Protocol)协议 进行数据的传输

2.1.2 数据报套接字(SOCK_DGRAM)

数据报套接字(SOCK_DGRAM)用于提供 无连接(不可靠)的数据传输服务。

数据报套接字不保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。

数据报套接字(SOCK_DGRAM)使用 UDP(User DatagramProtocol)协议 进行数据的传输

2.2 原始套接字(SOCK_RAW)

原始套接字(SOCK_RAW)可以做到标准套接字做到的事,更可以做到标准套接字做不到的事。

原始套接字是在传输层及传输层以下使用的套接字。

原始套接字在接收和发送时不仅能操作数据部分(TCP Payload / UDP Payload),也能对 IP 首部或TCP 首部和 UDP 首部进行操作。

因此如果我们开发的是更底层的应用,比如发送一个自定义的 IP 包、UDP 包、TCP 包或 ICMP 包,捕获所有经过本机网卡的数据包(sniffer),伪装本机的 IP ,拒绝服务攻击(DOS)等,都可以通过原始套接字(SOCK_RAW)实现。

注意:必须在管理员权限下才能使用原始套接字。

3 Socket() 函数 介绍

3.1 功能

分配文件描述符,创建 socket,即创建网络上进程通信的端点。

3.2 头文件

#include <sys/types.h>
#include <sys/socket.h>

3.3 函数原型

int socket(int domain, int type, int protocol)

3.4 参数

注意:type 和 protocol 不可以随意组合,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。

具体的组合和应用场景可以参考 4 创建 Socket 及其应用场景

3.4.1 domain

domain:即协议域,又称为协议族(family),如下所示:

  • AF_INET / PF_INET(2):IPv4,获取 网络层的数据

  • AF_INET6:IPv6

  • AF_UNIX:UNIX 系统本地通信

  • AF_PACKET / PF_PACKET(17):以太网包,获取 数据链路层的数据

注:

  1. AF = Address Family(地址族),PF = Protocol Family(协议族)

  2. 理论上建立 socket 时是指定协议,应该用 PF_xxxx,设置地址时应该用 AF_xxxx。当然 AF_xxxx和 PF_xxxx 的值是相同的,混用也不会有太大的问题。

3.4.2 type

type:指定 socket 类型,如下所示:

  • SOCK_STREAM(1):面向连接的流式套接字(TCP)

  • SOCK_DGRAM(2):面向无连接的数据包套接字(UDP)

  • SOCK_RAW(3):接收 底层数据报文 的原始套接字

  • SOCK_PACKET(10):过时类型,可以使用,但是已经废弃,以后不保证还能支持,不推荐使用。

3.4.3 protocol

protocol:指定协议,如下所示:

  • 0:自动选择 type 类型对应的默认协议。

  • IPPROTO_IP(0):接受 TCP 类型的数据帧

  • IPPROTO_ICMP(1):接受 ICMP 类型的数据帧

  • IPPROTO_IGMP(2)接受 IGMP 类型的数据帧

  • IPPROTO_TCP(6):接受 TCP 类型的数据帧

  • IPPROTO_UDP(17):接受 UDP 类型的数据帧

  • ETH_P_IP(0x800):接收发往本机 MAC 的 IP 类型的数据帧

  • ETH_P_ARP(0x806):接受发往本机 MAC 的 ARP 类型的数据帧

  • ETH_P_RARP(0x8035):接受发往本机 MAC 的 RARP 类型的数据帧

  • ETH_P_ALL(0x3):接收发往本机 MAC 的所有类型 IP ARP RARP 的数据帧,接收从本机发出的所有类型的数据帧。(混杂模式打开的情况下,会接收到非发往本地 MAC 的数据帧)

3.5 返回值

  • 成功:返回一个文件描述符

  • 失败:返回 -1,并设置 errno

3.6 备注

详情查看 man 手册:man 2 socket

4 创建 Socket 及其应用场景

5 bind() 函数

5.1 功能

将 IP 地址信息绑定到 socket。

5.2 头文件

#include <sys/types.h>          
#include <sys/socket.h>

5.3 函数原型

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

5.4 参数

5.4.1 sockfd

通信 socket

5.4.2 addr

要绑定的地址信息(包括IP地址,端口号)。

通用地址结构体定义:

struct sockaddr 
{
    sa_family_t sa_family;   // 地址族, AF_xxx
    char        sa_data[14]; // 包括 IP 和端口号
}

新型的地址结构体定义:(查看新型的结构体信息: gedit /usr/include/linux/in.h )

struct sockaddr_in 
{
  __kernel_sa_family_t    sin_family;    // 地址族,IP 协议。默认:AF_INET
  __be16                  sin_port;      // 端口号
  struct in_addr          sin_addr;      // 网络 IP 地址

  unsigned char           __pad          // 8 位的预留接口
};

5.4.3 addrlen

地址信息大小

5.5 返回值

  • 成功:返回 0

  • 失败:返回 -1,并设置 errno

5.6 备注

详细查看 man 手册:man 2 bind

6 listen() 函数

6.1 功能

监听指定端口,socket() 创建的 socket 是主动的,调用 listen 使得该 socket 成为 监听 socket ,变主动为被动。

6.2 头文件

#include <sys/socket.h>

6.3 函数原型

int listen(int sockfd, int backlog);

6.4 参数

6.4.1 sockfd

通信 socket

6.4.2 backlog

同时能处理的最大连接要求

6.5 返回值

  • 成功:返回 0

  • 失败:返回 -1,并设置 errno

6.6 备注

详细查看 man 手册:man 2 listen

7 accept() 函数

7.1 功能

提取出 监听 socket 的等待连接队列中 第一个连接请求,创建 一个新的 socket,即 连接 socket

新建立的 连接 socket 用于发送数据和接受数据。

7.2 头文件

#include <sys/socket.h>

7.3 函数原型

#include <sys/types.h>          
#include <sys/socket.h>

7.4 参数

7.4.1 sockfd

监听 socket,即 在 调用 listen() 后的 监听 socket。

7.4.2 addr

(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。

7.4.3 addrlen

(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。

7.5 返回值

  • 成功:指向 新的 socket(连接 socket)的文件描述符。

  • 失败:返回 -1,并设置 errno

7.6 备注

详细查看 man 手册:man 2 listen

8 connect() 函数

8.1 功能

发送连接请求

8.2 头文件

#include <sys/types.h>          
#include <sys/socket.h>

8.3 函数原型

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

8.4 参数

8.4.1 sockfd

通信 socket

8.4.2 addr

要连接的服务器地址

8.4.3 addrlen

地址信息大小

8.5 返回值

  • 成功:返回 0

  • 失败:返回 -1,并设置 errno

8.6 备注

详细查看 man 手册:man 2 connect

9 sendto() 函数

9.1 功能

将数据由指定的 socket 传给对方主机

9.2 头文件

#include <sys/types.h>          
#include <sys/socket.h>

9.3 函数原型

int sendto (int sockfd , const void * msg, int len, unsigned int flags, const
struct sockaddr * to , int tolen);

9.4 参数

9.4.1 sockfd

已建立连接的 socket,如果利用 UDP 协议则不需建立连接。

9.4.2 msg

发送数据的缓冲区。

9.4.3 len

缓冲区长度。

9.4.4 flags

调用方式标志位,一般设为 0 。

9.4.5 to

用来指定要传送的网络地址,结构 sockaddr

9.4.6 tolen

sockaddr 的长度

9.5 返回值

  • 成功:返回实际传送出去的字符数

  • 失败:返回 -1,并设置 errno

9.6 备注

详细查看 man 手册:man 2 sendto

10 recvfrom() 函数

10.1 功能

接收远程主机经指定的 socket 传来的数据,并把数据传到由参数 buf 指向的内存空间。

10.2 头文件

#include <sys/types.h>          
#include <sys/socket.h>

10.3 函数原型

int recvfrom(int sockfd,void *buf,int len,unsigned int flags, struct sockaddr *from,int *fromlen);

10.4 参数

10.4.1 sockfd

已建立连接的 socket,如果利用 UDP 协议则不需建立连接。

10.4.2 buf

接收数据缓冲区。

10.4.3 len

缓冲区长度。

10.4.4 flags

调用方式标志位,一般设为 0 。

10.4.5 from

(可选)指针,指向装有源地址的缓冲区,结构 sockaddr

10.4.6 fromlen

(可选)指针,指向 from 缓冲区长度值,sockaddr 的结构长度

10.5 返回值

  • 成功:返回实际接受到的字符数

  • 失败:返回 -1,并设置 errno

10.6 备注

详细查看 man 手册:man 2 recvfrom

11 字节序

字节序,是 大于一个字节类型的数据在内存中的存放顺序,由 CPU 架构决定,与操作系统无关。是在跨平台和网络编程中,时常要考虑的问题。

11.1 高低地址

在内存中,栈是向下生长的,以char arr[4]为例,(因为 char 类型数据只有一个字节,不存在字节序的问题)依次输出每个元素的地址,可以发现,arr[0] 的地址最低,arr[3] 的地址最高,如图:

11.2 高低字节

在十进制中靠左边的是高位,靠右边的是低位,在其他进制也是如此。

例如: 0x12345678,从高位到低位的字节依次是 0x12、0x34、0x56 和 0x78。

11.3 字节序分类 - 大小端模式

字节序被分为两类:

  1. 大端模式(Big-endian):内存的 低地址 存放 数据的高字节,内存的 高地址 存放 数据的低字节。(与人类阅读顺序一致)

2.** 小端模式**(Little-endian),是指内存的 低地址 存放 数据的低字节,内存的 高地址 存放 数据的高字节

大端模式 CPU 代表是 IBM Power PC,小端模式 CPU 代表是 Intel X86、ARM。

11.4 大小端示例

以 0x12345678 为例,两种模式在内存中的存储情况,如下表所示:

11.5 判断大小端

利用 C 语言 union 联合体所有成员共用同一块内存的特性,可以用联合体快速实现判断大小端。

#include <stdio.h>
union u
{
    char c[4];
    int i;
};
int main(void)
{
    union u test;
    int j;

    test.i = 0x12345678;

    for(j = 0; j < sizeof(test.c); j++)
    {
        printf("0x%x
",test.c[j]);
    }

    return 0;
}

运行后结果:

可以看出,我的机器是小端字节序。

11.6 网络字节序与本机字节序

网络字节序(NBO,Network Byte Order),是 TCP/IP 中规定好的一种数据表示格式。它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。

网络字节序采用大端(Big-endian)字节序排序方式。

主机字节顺序(HBO,Host Network Order),与机器 CPU 相关,数据的存储顺序由 CPU 决定。

11.6.1 转换函数

socket 编程中经常会用到 4 个网络字节顺序与本地字节顺序之间的转换函数:htons()、ntohl()、 ntohs()、htons()。

htonl()--"Host to Network Long"        // 长整型数据主机字节顺序转网络字节顺序
ntohl()--"Network to Host Long"        // 长整型数据网络字节顺序转主机字节顺序
htons()--"Host to Network Short"       // 短整型数据主机字节顺序转网络字节顺序
ntohs()--"Network to Host Short"       // 短整型数据网络字节顺序转主机字节顺序

在使用小端字节序的系统中,这些函数会把字节序进行转换。

在使用大端字节序的系统中,这些函数会定义成空宏。

12 代码示例

12.1 标准套接字(SOCK_STREAM - TCP)

12.1.1 TCP Socket 通信过程

12.1.1.1 服务器

1. 建立连接阶段

  • 调用 socket(),分配文件描述符,创建 服务器 socket

  • 调用 bind(),将 socket 与本地 IP 地址和端口绑定

  • 调用 listen(),监听指定端口,socket() 创建的 socket 是主动的,调用 listen 使得该 socket 成为监听 socket ,变主动为被动

  • 调用 accept(),获得 连接 socket,阻塞等待客户端发起连接

2. 数据交互阶段

  • 调用 read(),阻塞等待客户端发送的数据请求,收到请求后从 read() 返回,处理客户端请求

  • 调用 write(),将数据发送给客户端

3. 关闭连接

  • 当 read() 返回 0 的时候,说明客户端发来了 FIN 数据包,即关闭连接,调用 close() 关闭 连接 socket 和 监听 socket

12.1.1.2 客户端

1. 建立连接阶段

  • 调用 socket(),分配文件描述符,创建 客户端 socket

  • 调用 connect(),向服务器发送建立连接请求

2. 数据交互阶段

  • 调用 write(),向服务器发送数据

  • 调用 read(),阻塞等待服务器应答

3. 关闭连接

  • 当没有数据发送的时候,调用 close() 关闭 客户端 socket ,即关闭连接,向服务器发送 FIN 数据报

12.1.2 单个客户端单个服务器的 TCP 通信

Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子1

12.1.3 多线程实现 - 单个客户端单个服务器的 TCP 通信

Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子2

12.1.4 多路复用实现 - 单个客户端单个服务器的 TCP 通信

Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子3

12.1.5 多个客户端单个服务器的 TCP 通信

Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子4

12.1.6 多线程实现 - 多个客户端单个服务器的 TCP 通信

Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子5

12.1.7 多路复用实现 - 多个客户端单个服务器的 TCP 通信

Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子6

12.2 标准套接字(SOCK_DGRAM- UDP)

12.2.1 UDP Socket 通信过程

12.2.1.1 服务器

1. 建立连接阶段

  • 调用 socket(),分配文件描述符,创建 服务器 socket

  • 调用 bind(),将 socket 与本地 IP 地址和端口绑定

2. 数据交互阶段

  • 调用 recvfrom(),阻塞,接受客户端的数据

  • 调用 sendto(),将数据发送给客户端

3. 关闭连接

  • 调用 close() 关闭 服务器 socket

12.2.1.2 客户端

1. 建立连接阶段

  • 调用 socket(),分配文件描述符,创建 客户端 socket

2. 数据交互阶段

  • 调用 sendto(),向服务器发送数据

  • 调用 recvfrom(),阻塞,接受服务器的数据

3. 关闭连接

  • 调用 close() 关闭 客户端 socket ,即关闭连接。

12.2.2 单个客户端单个服务器的 UDP 通信

代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子1

12.2.3 多线程实现 - 单个客户端单个服务器的 UDP 通信

代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子2

12.2.4 多路复用实现 - 单个客户端单个服务器的 UDP 通信

代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子3

12.2.4 UDP 通信组播

代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子4

12.2.4 UDP 通信广播

代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子5

12.3 原始套接字

12.3.1 抓取以太网上的所有数据帧

代码来源:GitHub - zhouyingjiu - https://github.com/zouyingjiu/sniffer


/* 
 *        sniffer.c 
 * 
 *        功能: 
 *                linux rawSocket 抓取以太网上的所有数据帧 
 * 
 *        参数: 
 *                无 
 * 
 *  注意: 
 *      执行该程序需要 root 权限 sudo ./  
 */ 
 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
#ifdef __linux__ 
        #include <unistd.h> 
        #include <errno.h> 
        #include <sys/socket.h> 
        #include <sys/types.h> 
        #include <netinet/in.h> 
        #include <netinet/ip.h> 
        #include <netinet/tcp.h> 
        #include <netinet/udp.h> 
        #include <netinet/ip_icmp.h> 
        #include <net/if_arp.h> 
        #include <netinet/if_ether.h> 
        #include <net/if.h> 
        #include <sys/ioctl.h> 
#elif __win32__ 
        #include <windows.h> 
 
#endif 
 
void UnpackARP(char *buff); 
void UnpackIP(char *buff); 
void UnpackTCP(char *buff); 
void UnpackUDP(char *buff); 
void UnpackICMP(char *buff); 
void UnpackIGMP(char *buff); 
 
int main(int argc, char **argv) 
{ 
        int sockfd, i; 
        char buff[2048]; 
         
      /* 
       *   监听以太网上的所有数据帧 
       */ 
        if(0 > (sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))))         
        { 
                perror("socket error!"); 
         
                exit(-1); 
        } 
 
        while(1) 
        { 
                memset(buff, 0, 2048); 
                 
                int n = recvfrom(sockfd, buff, 2048, 0, NULL, NULL); 
 
                printf("%s
",buff); 
                 
                printf("开始解析数据包============
"); 
                 
                printf("大小: %d
", n); 
                 
                struct ethhdr *eth = (struct ethhdr*)buff; 
                 
                char *nextStack = buff + sizeof(struct ethhdr); 
                 
                int protocol = ntohs(eth->h_proto); 
                switch(protocol)  
                { 
                        case ETH_P_IP: 
                                UnpackIP(nextStack); 
                                break; 
                         
                        case ETH_P_ARP: 
                                UnpackARP(nextStack); 
                                break; 
                } 
                 
                printf("解析结束=================

"); 
        } 
 
        return 0; 
} 
 
void getAddress(long saddr, char *str)  
{ 
        sprintf(str, "%d.%d.%d.%d",                          
                        ((unsigned char*)&saddr)[0],          
                        ((unsigned char*)&saddr)[1],          
                        ((unsigned char*)&saddr)[2],          
                        ((unsigned char*)&saddr)[3]); 
} 
 
void UnpackARP(char *buff)  
{ 
        printf("ARP数据包
"); 
} 
 
void UnpackIP(char *buff)  
{ 
        struct iphdr *ip = (struct iphdr*)buff; 
        char *nextStack = buff + sizeof(struct iphdr); 
        int protocol = ip->protocol; 
        char data[20]; 
 
        getAddress(ip->saddr, data); 
        printf("来源ip %s
", data); 
         
        bzero(data, sizeof(data)); 
 
        getAddress(ip->daddr, data); 
        printf("目标ip %s
", data); 
 
        switch(protocol) 
         { 
                case 0x06: 
                        UnpackTCP(nextStack); 
                        break; 
 
                case 0x17: 
                        UnpackUDP(nextStack); 
                        break; 
                 
                case 0x01: 
                        UnpackICMP(nextStack); 
                        break; 
 
                case 0x02: 
                        UnpackIGMP(nextStack); 
                        break; 
 
                default: 
                        printf("unknown protocol
"); 
                        break; 
        } 
} 
 
void UnpackTCP(char *buff)  
{ 
        struct tcphdr *tcp = (struct tcphdr*)buff; 
         
        printf("传输层协议:tcp
"); 
     
        printf("来源端口:%d
", ntohs(tcp->source)); 
        printf("目标端口:%d
", ntohs(tcp->dest)); 
} 
 
void UnpackUDP(char *buff)  
{ 
        struct udphdr *udp = (struct udphdr*)buff; 
         
        printf("传输层协议:udp
"); 
     
        printf("来源端口:%d
", ntohs(udp->source)); 
        printf("目的端口:%d
", ntohs(udp->dest)); 
} 
 
void UnpackICMP(char *buff)  
{ 
        printf("ICMP数据包
");         
} 
 
void UnpackIGMP(char *buff)  
{ 
        printf("IGMP数据包
"); 
} 

12.3.2 抓取以太网上的所有数据帧,匹配 HTTP 协议并发送 TCP RST

代码来源:我的 Github - https://github.com/PikapBai/sniffer_cmpHTTP_sendTCP

13 参考资料

  1. 套接字 - 百度百科 - https://baike.baidu.com/item/套接字/9637606?fromtitle=socket&fromid=281150&fr=aladdin

  2. RAW SOCKET - 百度百科 - https://baike.baidu.com/item/RAW SOCKET/995623?fromtitle=原始套接字&fromid=23692610&fr=aladdin#ref_[1]_4263346

  3. 原始套接字简介 - chengqiuming - https://blog.csdn.net/chengqiuming/article/details/89577351

  4. 原始套接字概述 - anton_99 - https://blog.csdn.net/anton_99/article/details/95646879?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

  5. Linux 原始套接字抓取底层报文 - 2603898260 - https://blog.csdn.net/s2603898260/article/details/85020006?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

  6. Linux-C TCP 简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144

  7. 【Linux网络编程】socket编程“网络字节顺序”和“主机字节顺序” - qq_20553613 - https://blog.csdn.net/qq_20553613/article/details/86385271

  8. 网络字节序 - 百度百科 - https://baike.baidu.com/item/网络字节序/12610557?fr=aladdin

  9. 字节序(大小端)理解 - sunflower_della - https://blog.csdn.net/sunflower_della/article/details/90439935

  10. 理解大小端字节序 - fan-yuan - https://www.cnblogs.com/fan-yuan/p/10406315.html

  11. linux网络编程之TCP/IP的TCP socket通信过程(含实例代码) - 知乎 - linux服务器开发专栏 - https://zhuanlan.zhihu.com/p/148739946

  12. Linux C Socket UDP编程详解及实例分享 - 知乎 - linux服务器开发专栏 - https://zhuanlan.zhihu.com/p/131402832

  13. Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233

  14. 《图解 TCP/IP》(第 5 版)[日]竹下隆史 /[日]村山公保/ [日]荒井透 / [日]苅田幸雄

  15. 浅谈linux下原始套接字 SOCK_RAW 的内幕及其应用 - 知乎 - linux服务器开发专栏 - https://zhuanlan.zhihu.com/p/254912774

  16. GitHub - zhouyingjiu - https://github.com/zouyingjiu/sniffer

原文地址:https://www.cnblogs.com/PikapBai/p/13964866.html