Gannicus Guo的DIY TCP/IP之旅

Gannicus Guo的DIY TCP/IP之旅,描述本人自己动手写的一个简化的TCP/IP协议。经测试该协议可以运行在Ubuntu 16.10 x86_64 操作系统用户空间。便于描述,将该TCP/IP协议称为DIY TCP/IP 。专栏内容包括DIY TCP/IP实现的可行性分析,依赖的编程接口,ARP数据帧的收发,ICMP Echo (PING)数据帧的收发,简单的基于ARP表的IP路由,局域网内IP数据帧的收发,IP数据帧的分片,IP分片的重组,TCP数据帧的收发,TCP连接状态机,以及TCP滑动窗口的实现等全部内容。方便感兴趣的朋友参考并自己动手实现。
自己动手实现DIY TCP/IP,硬件依赖:一台个人电脑(带网卡,无线有线均可),软件依赖:Ubuntu操作系统(虚拟机,硬盘安装均可)。DIY TCP/IP支持ARP,Ping,Large Packet Ping和局域网内的iperf tcp吞吐量测试。

  1. 可行性分析
    Linux Kernel 提供PF_PACKET域的socket,通过PF_PACKET socket可以收发链路层的原始数据帧。基于链路层的原始数据帧,可以在Linux Kernel的用户空间实现TCP/IP协议。区别于Linux kernel内核空间的TCP/IP协议栈,DIY TCP/IP虚拟一个局域网内不存在的IP地址,数据帧到达Linux kernel的网络设备层后,由DIY TCP/IP接收,不经过kernel的TCP/IP协议栈。假设局域网内一台测试机器上运行iperf,目的IP址是虚拟出来的IP,只要DIY TCP/IP 回复测试机发出的ARP Request,建立虚拟IP和本机MAC地址的映射,DIY TCP/IP即可收到测试机发出的IP和TCP数据帧。在此基础上实现DIY TCP/IP的IP模块,TCP模块,即可完成DIY TCP/IP和测试机器的TCP/IP通信, 不需要对Linux kernel做任何配置或和修改。
  2. DIY TCP/IP软件架构
    Gannicus Guo的DIY TCP/IP之旅_第1张图片
    DIY TCP/IP实现依赖的编程接口:libpcap,POSIX Pthread,RAW Socket,Linux C文件操作,Linux C的时间操作等。
  3. 自己动手实现
    本人搜所过网络上现存的实现TCP/IP协议的内容,有的是通过libnet来实现组帧,有的没有完整的讲解实现过程。由于本人深受于渊老师“自己动手写操作系统”一书的影响,希望尽可能多的自己动手实现需要的功能,除去”DIY TCP/IP软件架构”一节中列出的对现成库函数的依赖,DIY TCP/IP 的实现:编译用到的Makefile,链表,队列,buffer 管理,组帧,各个协议模块的细节均是参考开源代码后自己动手通过C语言实现。以测试通过ARP,PING,Large Packet Ping和iperf TCP为目标,详细的描述DIY TCP/IP的全部实现内容。
  4. 从0开始
    在介绍从0开始的代码实现之前,先介绍用到的5个libpcap的库函数。从http://www.tcpdump.org/ 可以获取到lipcap库的源代码。Ubuntu上使用libpcap库,需要先通过”sudo apt-get install libpcap-dev”命令安装。获取libpcap库的源代码,有助于更深入的了解libpcap库API的实现,正确的设置库函数参数。如果读者已经安装过libpcap库可以通过如下命令查看libpcap库的版本:
	$ ldconfig -p | grep pcap
	libpcap.so.0.8 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libpcap.so.0.8
	libpcap.so.0.8 (libc6) => /usr/lib/i386-linux-gnu/libpcap.so.0.8
	libpcap.so (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libpcap.so

	$ ls -l /usr/lib/x86_64-linux-gnu/libpcap* 
	-rw-r--r-- 1 root root 433728  2月  4  2014 libpcap.a
	lrwxrwxrwx 1 root root     14  2月  4  2014 libpcap.so -> libpcap.so.0.8
	lrwxrwxrwx 1 root root     16 11月  8  2016 libpcap.so.0.8 -> libpcap.so.1.5.3

本人Ubuntu机器上链接的libpcap库的版本是1.5.3。使用libpcap库函数,gcc编译时需指定链接参数-lpcap。本节介绍用到的libpcap库函数时引入的代码实现,也是基于libpcap1.5.3。更多libpcap库函数的使用手册,读者可以在ubuntu终端中输入man “库函数名称”,例如man pcap_open_live, 也可以直接访问”Linux man page”官方网址https://www.die.net/。下面介绍的libpcap库函数的函数原型,基本描述,返回值,以及出错处理,均参考Linux man page。
4.1 pcap_open_live库函数
从0开始的代码使用的第一个libpcap的库函数为pcap_open_live,该库函数的实现在pcap.c文件中。该函数初始化pcap_t结构,创建socket,domain为PF_PACKET,type为SOCK_RAW,protocol为ETH_P_ALL ,也就是从链路层接收所有协议的数据帧,初始化socket选项PACKET_RX_RING,通过内存映射的方式从RAW socket上接收原始数据帧。
在pcap_open_live的linux man page, 可以看到该库函数的函数原型和使用时需要引入的头文件。依照Linux man page 里的内容逐一介绍该库函数的基本描述,参数,返回值和出错处理。

#include 
 
char errbuf[PCAP_ERRBUF_SIZE];

pcap_t *pcap_open_live(const char *device, int snaplen,
        int promisc, int to_ms, char *errbuf);

第一个参数device,网络接口,例如”eth0”或”wlan0”等。如果指定device为NULL或”any”,则从所有网络接口接收数据帧。
第二个参数snaplen,指定抓取数据帧的长度,类似tcpdump的”-s”参数,如果不需要完整的数据帧,可以指定需要的长度。如果指定snaplen < 0或者大于MAXMUM_SNAPLEN,libpcap库会将其设置为MAXIMUM_SNAPLEN,MAXIMUM_SNAPLEN的值可以查看pcap-int.h。回到参数sanplen的介绍,库函数会将sanplen赋值给pcap_t结构中的snapshot成员。DIY TCP/IP需要的完整的数据包的长度为1514字节,IP层的最大传输单元为1500,MAC头的开销为 源地址 (6字节) + 目的地址 (6字节) + Type (2字节) ,共1514字节。使用过iperf测试的朋友应该发现过长度远于1518字节的TCP数据帧,并且IP层没有分片,此处先说明这种超过1514字节的TCP数据帧与网卡驱动的GRO有关。GRO相关内容在DIY TCP/IP的TCP模块的实现章节介绍。
第三个参数promisc,指定是否将device指定的网络接口设置为混杂模式,1为混杂模式。
第四个参数to_ms,以毫秒为单位,是libpcap通过 poll检查 raw socket是否有待接收的数据帧的超时时间。libpcap 将socket设置为non-block模式并设置超时时间,用于处理被打断的system call的poll event和检查结束条件是否满足,超时处理的实现见pcap_wait_for_frames_mmap函数。
第五个参数errbuf,用于返回pcap_open_live的出错信息,如果strlen(errbuf)不为0时,表明有出错信息,errbuf的最小长度为PCAP_ERRBUF_SIZE(256),定义见pcap.h头文件。
pcap_open_live出错返回NULL,出错信息存放在errbuf中,成功时返回pcap_t类型的指针,指向libpcap库创建的数据结构pcap_t。pcap_t结构体定义见pcap-int.h头文件。
4.2 pcap_compile & pcap_setfilter库函数

#include 

int pcap_compile(pcap_t *p, struct bpf_program *fp,
        const char *str, int optimize, bpf_u_int32 netmask);
      
int pcap_setfilter(pcap_t *p, struct bpf_program *fp);

pcap_compile生成bpf_grogram数据结构,可以理解为数据帧的过滤器,pap_setfilter设置pcap_compile生成的过滤器。DIY TCP/IP需要接收以太网类型为0x0800 (IP) 和0x0806 (ARP)类型的数据帧。
pcap_compile的第一个参数p是4.1节介绍的pcap_open_live返回的pcap_t类型的指针;第二个参数fp是指向bfp_program数据结构的指针;第三个参数参数str指向模式字符串,str的语法见pcap-filter的man page。过滤IP和ARP类型的数据帧,str为“ether proto 0x0800 or ether proto 0x0806”。pcap_compile根据str,填充bfp_program数据结构的成员。第四个参数optimize,控制pcap_compile是否执行bfp_optimize,将其设置为1,第五个参数netmask是网络接口的子网掩码,只有在过滤广播包时有用,将其设置为0xffffff00 (255.255.255.0) 即可。
pcap_setfitler的第一个参数是pcap_open_live返回的pcap_t类型的指针,第二个参数是pcap_compile生成的过滤器。
pcap_compile和pcap_setfilter库函数函数执行成功返回0,不成功返回-1。返回-1时, 可以通过pcap_geterr获取出错信息。
4.3 pcap_loop库函数

#include 

typedef void (*pcap_handler)(u_char *user,
              const struct pcap_pkthdr *h, const u_char *bytes);
                          
int pcap_loop(pcap_t *p, int cnt,
              pcap_handler callback, u_char *user);


pcap_loop从raw socket上读取数据帧,通过callback回调函数将数据帧传给调用者,pcap_loop的实现代码见pcap.c。
先介绍pcap_loop的第三个参数callback,pcap_handler类型的函数指针。callback函数在pcap_loop接收到数据帧时调用,由调用者实现,完成对捕获的数据帧的处理。pcap_loop函数的第一个参数user是调用者传递给callback函数的参数,第二个参数h是libpcap对接收到的网络数据帧的描述,包括长度,时间戳等。第三个参数bytes指向数据帧的有效载荷。
pcap_loop的第一个参数是pcap_open_live返回的pcap_t指针,第二个参数cnt指定需要获取的网络数据帧的数量,pcap_loop在接收到cnt个数据帧时返回。如果cnt为0或-1,pcap_loop会一直执行,这种情况要用pcap_breakloop库函数打断pcap_loop。第四个参数user是传给callback回调函数的参数。

4.4 pcap_breakloop库函数

#include 
void pcap_breakloop(pcap_t *);


pcap_breakloop设置pcap_loop的返回条件为真,pcap_loop每次循环,接收到数据帧,或超时,或被中断时检查该返回条件。
4.5 接收链路层数据帧
从0开始的代码一共91行,先介绍主函数,然后是头文件和数据结构的定义。本节使用4.1-4.4节介绍的libpcap库函数,从eth0网络接口链路层接收数据帧,实现pcap_loop回调函数,打印出数据帧以太网头部的类型,main函数如下:

pcap_t *pcap_dev = NULL;

void signal_handler(int sig_num)
{
 pcap_breakloop(pcap_dev);
}
 
int main(int argc, char *argv[])
{
 char pcap_packet_filter[FILTER_BUFFER_SIZE];
 char err_buf[PCAP_ERRBUF_SIZE];
 struct bpf_program filter_code;

 /* init signal handler */
 signal(SIGINT, signal_handler);

 /* obtain packet capture handle */
 pcap_dev = pcap_open_live("eth0", MAX_NETWORK_SEGMENT_SIZE, 0,
                              TIMEOUT_MS, err_buf);
 if (pcap_dev == NULL) {
        printf("pcap_open_live failed, %s (%d)\n",
                       strerror(errno), errno);
     return -1;
 }
 /* set pcap filter */
 memset(pcap_packet_filter, 0, FILTER_BUFFER_SIZE);
 init_packet_filter(pcap_packet_filter, FILTER_BUFFER_SIZE);
 if (pcap_compile(pcap_dev, &filter_code,
                 pcap_packet_filter, 1, IPV4_NETWORK_MASK) < 0) {
          pcap_perror(pcap_dev, "pcap_compile");
          return -1;
 }
 if (pcap_setfilter(pcap_dev, &filter_code) < 0) {
         pcap_perror(pcap_dev, "pcap_setfilter");
         return -1;
 }

 pcap_loop(pcap_dev, -1, pcap_callback, NULL);

 printf("pcap_loop ended\n");
}


line1-6:定义全局变量pcap_dev,方便在信号处理函数中使用,信号处理函数调用pcap_breakloop终止pcap_loop循环。
line8-24: 注册信号处理函数,捕获SIGINT信号,终端键入ctrl+c时,前台进程组会收到SIGINT信号,终止进程。pcap_open_live指定从eth0网络接口的链路层接收数据帧。长度65536 > 1518(字节),0为不打开混杂模式,由于虚拟IP也是与本机MAC地址建立映射,所以不必打开混杂模式。TIMEOUT_MS为512,pcap_loop在没有数据帧接收时,poll也没有被信号打断的情况下,内部循环512ms一次,err_buf返回pcap_open_live出错时的信息。
line26-37: init_packet_filter初始化帧过滤字符串,将清零之后的pcap_packet_filter初始化为”ether proto 0x0800 or ether proto 0x0806”。pcap_complie生成帧过滤器filter_code,optimize值为1,子网掩码为255.255.255.0,再调用pcap_setfilter设置帧过滤器生效。
Line39: 开始接收链路层数据帧,个数为无限 (-1) ,回调函数pcap_callback处理接收到的数据帧,pcap_callback的参数为NULL。
接下来给出main函数中的头文件,宏定义:

  #include 
  #include 
  #include 
  #include 
  #include 
  
  #define MAX_NETWORK_SEGMENT_SIZE        65535
  #define PROMISC_ENABLE                  1
  #define TIMEOUT_MS                      512
  #define FILTER_BUFFER_SIZE              256
  #define IPV4_NETWORK_MASK               0xffffff00
  #define ETHERNET_IP                     0x0800
  #define ETHERNET_ARP                    0x0806
  #define ETHERNET_ADDR_LEN               6
 
  #define MACSTR "%02x:%02x:%02x:%02x:%02x:%02x"
  #define MAC2STR(x) (x)[0], (x)[1], (x)[2], (x)[3], (x)[4], (x)[5]
 
  typedef struct _ethhdr {
         unsigned char dst[ETHERNET_ADDR_LEN];
         unsigned char src[ETHERNET_ADDR_LEN];
         unsigned short type;
  } __attribute__((packed)) ethhdr_t;


line1-14: 加入引用的头文件,最大数据帧长度65535,大于1500 + sizeof(Ethernet Header)1514字节即可。PROMISC_ENABLE为1,对于从0开始的代码实现,为丰富测试数据,接收目的MAC地址不是本机MAC地址的数据帧,使用混杂模式。对于DIY TCP/IP的实现,映射虚拟IP到本机MAC地址,不需要打开混杂模式。
ETHERNET_IP,ETHERNET_ARP为以太网类型,分别带表IPv4网际协议和ARP地址解析协议类型。通过wireshark捕获数据帧,验证定义的正确性:
Gannicus Guo的DIY TCP/IP之旅_第2张图片
上图为wireshark 抓到的TCP数据帧,以太网类型字段为0x0800。
Gannicus Guo的DIY TCP/IP之旅_第3张图片
有的朋友会注意到,还有以太网类型为0x8100的ARP数据帧,用于802.1Q VLAN。对于DIY TCP/IP,过滤接收IPv4和ARP数据帧即可。
line16-17:MACSTR为MAC地址的格式化输出字符串,MAC2STR和MACSTR配合打印输出MAC地址。
line19-23: 以太网头部结构体定义,从图1或图2中可以看到,以太网头部分为三个部分,目的MAC地址:6 bytes, 源MAC地址:6 bytes,类型:2 bytes。
最后给出main函数中用到的init_packet_filter和pcap_callback的实现:

 static void init_packet_filter(char *pcap_packet_filter, unsigned 
                                  int size)
 {
         if (pcap_packet_filter == NULL || size == 0) {
                 printf("Null packet filter: %p or Size: %u\n",
                         pcap_packet_filter, size);
                 return;
         }
 
         snprintf(pcap_packet_filter, size,
                 "ether proto 0x%04x or ether proto 0x%04x",
                 ETHERNET_IP, ETHERNET_ARP);
         printf("filter: %s\n", pcap_packet_filter);
 }


line1-14: 通过sprintf将pcap_packet_filter格式化为” ether proto 0x0800 or ether proto 0x0806”,同时打印出格式化后的字符串验证。

 static void pcap_callback(unsigned char *arg,
                           const struct pcap_pkthdr *pkthdr, 
                             const unsigned char *packet)
 {
  ethhdr_t *ethpkt = NULL;
  if (packet == NULL)
      return;
  ethpkt = (ethhdr_t *)packet;
         printf("%ld.%06u: capture length: %u, pkt length: %u, ethernet type: %04x, "MACSTR " --> " MACSTR"\n",
  pkthdr->ts.tv_sec, pkthdr->ts.tv_usec, pkthdr->caplen, pkthdr->len,
  ethpkt->type, MAC2STR(ethpkt->src), MAC2STR(ethpkt->dst));
 }


pcap_callback用于打印输出收到数据帧的以太网头。
line4-8: 回调函数中pack是pcap_loop回传的有效载荷,pkthdr描述接收到的数据帧的长度。
line9-11: 打印输出数据帧的时间戳,有效载荷的长度,以太网类型字段,源MAC地址和目的MAC地址。
编译,运行:

  $ gcc -o device device.c -lpcap
  $ sudo ./device 
  filter: ether proto 0x0800 or ether proto 0x0806
  1528450824.298054: capture length: 243, pkt length: 243, ethernet type: 0008, fc:4d:d4:39:c4:5e --> ff:ff:ff:ff:ff:ff
  1528450824.298331: capture length: 235, pkt length: 235, ethernet type: 0008, bc:30:5b:a2:ee:24 --> ff:ff:ff:ff:ff:ff
  1528450824.677061: capture length: 60, pkt length: 60, ethernet type: 0608, c8:5b:76:04:fd:94 --> ff:ff:ff:ff:ff:ff
  1528450825.030632: capture length: 92, pkt length: 92, ethernet type: 0008, 28:d2:44:7e:62:32 --> ff:ff:ff:ff:ff:ff
  1528450825.168767: capture length: 64, pkt length: 64, ethernet type: 0081, 8c:b6:4f:57:9e:bc --> ff:ff:ff:ff:ff:ff
  1528450825.387166: capture length: 64, pkt length: 64, ethernet type: 0081, 00:19:2f:91:cd:ff --> ff:ff:ff:ff:ff:ff
 ^Cpcap_loop ended


从0开始的代码保存为device.c,后续章节将基于device.c实现DIY TCP/IP的网络设备模块。
line1: gcc编译device.c指定动态链接库-lpcap
line2: 超级权限运行device, 使用PF_PACK域的socket需要超级权限才能从链路层接收数据帧。
line3: 格式化输出的帧过滤字符串。
line4-9: 接收到的链路层数据帧,以太网类型字段是大端格式,后面章节介绍大小端转换的函数的实现。
line10: 在终端键入ctrl+c,终止pcap_loop函数。打印输出”pcap_loop ended”,此处的打印输出,在后续章节扩展实现成DIY TCP/IP在pcap_loop运行结束后,销毁网络设备模块,IP模块,TCP模块以及其他模块的代码。
4.6 Makfile
现在已经可以从链路层接收数据帧了,并简单实现了以太网头的解析。本节来动手写Makefile,后续章节会不断的扩充本节实现的Makefile,加入后续代码的编译。
目前只有一个device.c文件,把device.c用到的宏定义,结构体的定义,可以暴露给别的模块使用的函数接口等,写到头文件中。只有device.c文件用到的,例如PROMISC_ENABLE宏,单独放在device.h文件中,以太网头结构体的定义,不仅仅只有device.c用到,ARP模块的实现也会用到,将其放在common.h头文件中。按照使用场景分类,将代码模块化如下:
device.h

 #ifndef _DEVICE_H_
 #define _DEVICE_H_
 #define MAX_NETWORK_SEGMENT_SIZE       65535
 #define PROMISC_ENABLE                   1
 #define PROMISC_DISABLE                  0
 #define TIMEOUT_MS                        512
 #define FILTER_BUFFER_SIZE              256
 #endif

把调用libpcap库函数时用到的参数,定义成对应的宏,放在device.h头文件中,包括snaplen, promisc, timeout的数值, 生成帧过滤器的buffer size。
init.h

 #ifndef _INIT_H_
 #define _INIT_H_
 //#define DEFAULT_IFNAME "eth0"
 #define DEFAULT_IFNAME "ens33"
 #endif

init.h存放DIY TCP/IP的初始化信息,目前只有默认网络接口的名称eth0。ens33是在更新的ubuntu版本上出现的由udev命名规则确定的网络接口名称。
common.h

  #ifndef _COMMON_H_
  #define _COMMON_H_
  
  #define IPV4_NETWORK_MASK    0xffffff00
  #define ETHERNET_IP           0x0800
  #define ETHERNET_ARP          0x0806
  #define ETHERNET_ADDR_LEN    6
  
  #define MACSTR "%02x:%02x:%02x:%02x:%02x:%02x"
  #define MAC2STR(x) (x)[0], (x)[1], (x)[2], (x)[3], (x)[4], (x)[5]
 
  typedef struct _ethhdr {
          unsigned char dst[ETHERNET_ADDR_LEN];
          unsigned char src[ETHERNET_ADDR_LEN];
          unsigned short type;
  } __attribute__((packed)) ethhdr_t;
 
  #endif 

common.h存放子网掩码,以太网类型:IP,ARP,MAC地址格式化输出宏,以太网头部结构体定义,packed限定ethhdr_t数据结构按字节对齐。解析从链路层接收到的数据帧,直接把payload的指针强制转换为以太网头部数据结构,以太网头是14字节,不是4字节对齐的,所以结构体定义要加上对齐属性。
代码已经划分完成,现在有4个文件device.c,device.h,init.h,common.h,开始写Makefile,之后的代码实现会不断扩充本节划分出来的模块。
Makefile语法规则如下
target : prerequisites
command
target是目标名称,prerequesties是生成目标文件的依赖,依赖可以是其他目标文件或头文件,command是生成target需要执行的命令行。先不去了解Makefile其他复杂的语法规则,就以能够编译生成tcp_ip_stack目标文件为目的,在prerequesties中加入对头文件的依赖,command写成gcc编译的命令行。

 tcp_ip_stack:device.o
   gcc -o tcp_ip_stack device.o -lpcap
 device.o:device.c device.h init.h common.h
   gcc -c device.c
 clean:
   rm -rf *.o
   rm -rf tcp_ip_stack

line1-2: target是tcp_ip_stack elf文件,依赖的目标文件为device.o,命令行gcc –o 生成可执行文件,动态库为lpcap。
line3-4: target是device.o目标文件,依赖为device.c和编译需要的头文件,命令行是gcc –c生成device.o目标文件。
line5-7: target是clean,没有依赖,命令行是shell的命令,清除当前目录的所有目标文件和生成的elf tcp_ip_stack可执行文件。
下面是在终端键入make和make clean时的输出结果

 $ make
 gcc -c device.c
 gcc -o tcp_ip_stack device.o -lpcap
 
 
 $ make clean
 rm -rf *.o
 rm -rf tcp_ip_stack

本章小结之前先来扩充一下common.h头文件,实现NTOHS,NTOHL完成16位无符号整型值unsigned short和32位无符号整型值unsigned int的大小端转换,以小端格式输出以太网头部的类型字段。
将NTOHS,NTOHL,HSTON和HLTON放在common.h头文件中,便于各个模块调用。

  #define NTOHS(x) ({\
           unsigned short val = (x);\
           unsigned char *b = (unsigned char *)&(val);\
           b[0] << 8 | b[1]; })
  #define NTOHL(x) ({\
           unsigned int val = (x);\
           unsigned char *b = (unsigned char *)&(val); \
           b[0] << 24 | b[1] << 16 | b[2] << 8 | b[3];  })
                                                          
  #define HSTON    NTOHS                                   
  #define HLTON    NTOHL                                   

line1-4: NTOHS宏,N代表网络端(大端),H代表主机端(小端),S代表16无符号整型值,该宏用于16位无符号整型值大端转换成小端,宏定义返回最后一条语句的结果。
line5-8: 同NTOHS,该宏用于32位无符号整型值大端转换成小端。
line10-11: 定义主机端16位和32位无符号整型值,小端向大端转换。
再来修改一下,pcap_callback回调函数中,对以太网头部类型的打印,以太网头部类型是unsigned short,用NTOHS将其转换成小端格式。

  static void pcap_callback(unsigned char *arg,
                                 const struct pcap_pkthdr *pkthdr,
const unsigned char *packet)
  {
   ethhdr_t *ethpkt = NULL;
   if (packet == NULL)
       return;
   ethpkt = (ethhdr_t *)packet;
          printf("%ld.%06ld: capture length: %u, pkt length: %u
ethernet type: %04x, "MACSTR " --> " MACSTR"\n",
   pkthdr->ts.tv_sec, pkthdr->ts.tv_usec, pkthdr->caplen, pkthdr->len,
  NTOHS(ethpkt->type), MAC2STR(ethpkt->src), MAC2STR(ethpkt->dst));
  }

NTOHS高亮显示改动的部分,编译运行,打印输出如下:

  $ make
  gcc -c device.c
  gcc -o tcp_ip_stack device.o -lpcap
  $ sudo ./tcp_ip_stack 
  [sudo] password for gannicus: 
  filter: ether proto 0x0800 or ether proto 0x0806
  1529750254.336010: capture length: 131, pkt length: 131, ethernet type: 0800, dc:33:0d:2a:95:33 --> ff:ff:ff:ff:ff:ff
  1529750255.394534: capture length: 421, pkt length: 421, ethernet type: 0800, 20:6b:e7:4a:a8:2b --> 01:00:5e:7f:ff:fa
  1529750255.398825: capture length: 430, pkt length: 430, ethernet type: 0800, 20:6b:e7:4a:a8:2b --> 01:00:5e:7f:ff:fa
  1529750255.403594: capture length: 493, pkt length: 493, ethernet type: 0800, 20:6b:e7:4a:a8:2b --> 01:00:5e:7f:ff:fa
  1529750255.408557: capture length: 489, pkt length: 489, ethernet type: 0800, 20:6b:e7:4a:a8:2b --> 01:00:5e:7f:ff:fa
  1529750255.412843: capture length: 469, pkt length: 469, ethernet type: 0800, 20:6b:e7:4a:a8:2b --> 01:00:5e:7f:ff:fa

可以看到ethernet type已经变成小端值输出。
4.7 小结
本章讲述了从零开始的实现代码,现在已经可从链路层接收到数据帧了, 完成了代码模块化,并且已经自己动手写了Makefile。DIY TCP/IP的每个章节结束时,会列出对应代码实现的目录结构:
Gannicus Guo的DIY TCP/IP之旅_第4张图片
下一篇: DIY TCP/IP网络设备模块1

你可能感兴趣的:(DIY,TCP/IP)