网络诊断与状态查询(ICMPv4 + ICMPv6)
1.1 ICMP协议简介
ICMP(Internet Control Message Protocol)提供相应的错误检验与状态查询机制。
ICMP协议的主要功能包括,确认IP包是否成功送达目标地址,通知在发送过程当中IP包被废弃的具体原因,改善网络设置等。有了这些功能后,就可以获得网络是否正常、设置是否有误以及设备有何异常等信息,从而便于进行网络上的问题诊断。
在IP通信中如果某个IP包因为某种原因未能到达目标地址,那么这个具体的原因将由ICMP负责通知。ICMP的这种通知消息会使用IP数据报进行发送,从这点看ICMP有点像上层传输层协议,但由于ICMP并不为应用程序提供传输服务,所以仍算作网络层协议。ICMP报文封装位置与格式如下图示:
1.ICMPv4报文功能
从功能上划分,ICMP消息报文大致可以分为两类:一类是通知出错原因的差错报告报文;另一类是用于诊断的查询消息报文。差错报告报文主要用来向IP数据报源主机返回一个差错报告信息,这个错误报告信息产生的原因是路由器或主机不能对当前数据报进行正常的处理,例如无法将数据报递交给有效的上层协议、数据报因为生存时间TTL减为0而被删除等。查询报文用于一台主机向另一台主机查询特定的信息,通常查询报文都是成对出现的,即源主机发起一个查询报文,在目的主机收到该报文后,会按照查询报文约定的格式为源主机返回一个应答报文。两大种类的ICMPv4报文及其常见类型如下表示:
目的站不可达:IP路由器无法将UO数据报发送到目的地址时,会给发送端主机返回一个目的不可达的ICMP消息报文,并在这个消息报文中显示不可达的具体原因(前篇介绍的路径MTU发现就是根据代码4的分片位实现的),如下表示:
数据报超时:数据报超时可以用来防止数据报在网络中被循环的路由,在IP包中有一个字段叫TTL(Time To Live,生存时间),它的值随着每经过一次路由器就会减1,直到减到0时该IP包会被丢弃,IP路由器将会发送一个ICMP超时消息报文给发送端主机以通知该包已被丢弃(网络上常用的traceroute命令就是充分利用ICMP超时消息实现的),超时原因主要有以下两类:
源站抑制:为了给IP协议增加一种流量控制而设计,当路由器或主机因拥塞而丢弃数据报时,它可以向源站发送ICMP源站抑制报文,这个报文将告诉源站两个消息:第一,你的数据报发得太快,我已经丢弃了;第二,路径中出现了拥塞,请放慢你的数据报发送频率。
重定向:如果路由器发现发送端主机使用了次优的路径发送数据包,那么它会返回一个ICMP重定向消息报文告诉源主机改变它的路由表,这个消息报文中包含了最合适的路由信息和源数据,以提高数据报的递交效率。
数据报参数错误:数据报在网络中传输时,其首部中出现的任何二义性都可能会产生严重的问题,如果路由器或主机发现了这种二义性或者数据报中的某个字段丢失,路由器会直接丢弃数据报,并向源主机返回一个数据报参数错误报文。
回送请求或应答:用于进行通信的主机或路由器之间,判断所发送的数据包是否已经成功到达对端的一种消息报文。可以向对端主机发送回送请求消息,也可以接收对端主机发回来的回送应答消息,网络上最常用的ping命令就是利用这个消息报文实现的。
路由器询问和通告:主要用于发现与自己相连网络中的路由器,当一台主机发出ICMP路由器请求时,路由器则返回相应的通告报文。
时间戳请求或回答:在互联网中的两台主机能够使用时间戳请求或回答报文来确定数据报在彼此之间往返所需要的时间。
地址掩码请求或回答:主要用于主机或路由器想要了解子网掩码的情况,可以向那些目标主机或路由器发送ICMP地址掩码请求报文,然后通过接收ICMP地址掩码应答报文获取子网掩码的信息。
2.ICMPv6报文功能
IPv4中ICMP仅作为一个辅助作用支持IPv4,即使没有ICMP仍可以实现IP通信。然而在IPv6中,ICMP的作用被扩大了,如果没有ICMPv6,IPv6就无法进行正常通信。比如在IPv6中从IP地址定位MAC地址的协议从ARP转为ICMP的邻居探索消息(Neighbor Discovery),这种邻居探索消息融合了IPv4的ARP、ICMP重定向以及ICMP路由器选择消息等功能于一体,甚至还提供自动设置IP地址的功能。
ICMPv6中将ICMP也大致分为两类:一类是错误报告消息(类型0–127);另一类是信息查询消息(类型128–255)。常用的消息类型如下表示:
上面的错误报告消息含义跟ICMPv4类似,其中的数据包过大是指数据包在传递过程中,其大小超过了链路的MTU值,路由器会向源节点发送此消息,此消息也被用于链路MTU发现协议。
上面的信息查询报文多数也跟ICMPv4类似,下面主要介绍下多播监听发现消息(Multicast Listener Discovery)、邻居探索消息(Neighbor Discovery Protocol)、反邻居探索消息等。
多播监听发现:包括从类型130至类型132的消息,在多播通信中,用于确认是否有接收端,这里的MLD(Multicast Listener Discovery)可以实现IPv4中IGMP(Internet Group Management Protocol)的功能。其中多播监听报告与结束消息用于通知多播路由器(需要支持多播路由协议,便于将组关系转发给互联网上其它的多播路由器)加入与退出多播组,多播监听查询消息用于周期性探寻本地局域网上主机是否还为多播组成员。
邻居探索消息:ICMPv6中从类型133至类型137的消息叫做邻居探索消息,其中邻居请求消息用于查询IPv6的地址与MAC地址的对应关系(与IPv4中的ARP协议功能类似),并由邻居宣告消息得知MAC地址,邻居请求消息利用IPv6的多播地址实现传输。
反邻居探索消息:反向邻居探索请求消息用于查询MAC地址与IPv6地址的对应关系(与IPv4中的RARP协议功能类似),并由反向邻居探索宣告消息得知IPv6地址,反向邻居探索消息也利用了IPv6的多播地址实现传输。
由于在IPv6中实现了即插即用的功能,所以在没有DHCP服务器的环境下也能实现IP地址的自动获取。如果是一个没有路由器的网络,就使用MAC地址作为链路本地单播地址(前篇介绍过IPv6地址中的一种,网络标识为FE80::/10,主机标识为64比特版的MAC地址EUI-64)。而在一个有路由器的网络环境中,可以从路由器获得IPv6地址的前面部分的网络标识,后面部分的主机标识则由MAC地址进行设置(需要转换为EUI-64),此时可以利用路由器请求与宣告消息进行设置。
1.2 PC常用网络命令
两个常用的网络命令ping与traceroute:ping命令利用ICMP的回送请求/应答消息报文检查网络的连通性与往返估计时间;traceroute命令利用ICMP超时消息报文显示出由执行程序的源主机到达目的主机之前历经多少路由器。这两个命令也是在进行网络错误监测时最常用到的命令,前面介绍的查看ARP缓存表,查看路由表,查看网卡接口信息,甚至后面将要介绍的查询网络端口连接信息都有相关的命令提供功能支持,下面列举出linux常用的网络命令如下:
Linux常用网络命令 | 命令功能描述 |
ifconfig | 可以手动启动、查看、修改网络接口的相关参数,可以修改的参数包括IP地址、子网掩码、默认网关、MTU等; |
iwlist iwconfig | iwlist可以利用无线网卡进行无线AP的检测并获得相关数据; iwconfig可以设置无线网卡的相关参数; |
ip | 网络参数综合命令,除了可以设置一些基本的网络参数外,还能执行额外的IP协议,包括多IP的设置,功能很强大; |
arp | 查看IP地址与MAC地址对的缓存表信息; |
route | 查看目的IP地址、子网掩码、默认网关等路由状态信息; |
nslookup host | 查看主机名与IP地址的对应关系信息; |
ping | 查看目的主机是否可以访问到,并可获知往返时间等信息; |
traceroute | 跟踪源主机到目的主机所通过的各个路由节点信息; |
netstat | 查看网络传输层各端口的连接状态信息,比如目前有多少连接已建立或出现问题等; |
telnet | 可用于远程登录并访问目的主机; |
ftp lftp | 可与远程主机间进行文件传送; |
tcpdump wireshark | 可捕获网络数据包,用于分析数据包流向甚至监听数据包内容; 其中tcpdump是命令接口式数据包分析软件,wireshark是图形接口数据包分析软件; |
上面的命令使用时可以直接查看命令帮助,如果只查看简略的命令参数信息,可以使用–help(或-h)获得简略命令帮助信息,如果想查看详细的帮助信息可以使用man,即在命令名前加man(全称manual使用手册的意思)。下面看看windows系统的常用网络命令:
windows常用网络命令 | 命令功能描述 |
ipconfig | 查询网络接口信息,包括各网卡的MAC地址、IP地址/子网掩码/默认网关,甚至DHCP/DNS服务器地址等信息; |
netsh | Network Shell是一个 Windows 系统本身提供的网络配置命令行工具; |
arp | 查看IP地址与MAC地址对的缓存表信息; |
route | 查看或修改目的IP地址、子网掩码、默认网关等路由状态信息; |
nslookup | 查看主机名与IP地址的对应关系信息; |
ping | 查看目的主机是否可以访问到,并可获知往返时间等信息; |
tracert | 跟踪源主机到目的主机所通过的各个路由节点信息; |
netstat | 查看网络传输层各端口的连接状态信息,比如目前有多少连接已建立或出现问题等; |
net | 可以查看我们的管理网络环境、服务、用户、登陆等信息内容; |
telnet | 可用于远程登录并访问目的主机; |
ftp | 可与远程主机间进行文件传送; |
wireshark | 可捕获网络数据包,用于分析数据包流向甚至监听数据包内容; |
上面的命令依然可以直接查看帮助信息获得所支持的参数及用法,在命令后加上"/?"即可获得该命令的帮助信息。也可以在命令名前加help查询该命令的用法,但这种方式支持的命令相对较少,如果想获得更强大的命令交互支持,可以使用powershell。
1.3 ICMP协议实现
LWIP中实现了ICMP协议的哪些功能?在数据报处理过程中,根据差错情况的不同,能够发送两种类型的差错报文:目的站不可达差错报文和数据报超时差错报文。此外,LWIP能够响应一种查询报文,即回送请求报文,协议栈会根据收到的回送请求报文产生一个回送应答报文。
1.ICMPv4数据报描述
ICMP的数据报格式前面介绍过,其中的首部剩余字节在差错报文与查询报文中有些不同,两种报文的结构分别如下图示:
ICMP报文相比IP报文简单些,在LWIP中描述ICMP报文的数据结构如下:
// rt-thread\components\net\LWIP-1.4.1\src\include\ipv4\LWIP\icmp.h #define ICMP_ER 0 /* echo reply */ #define ICMP_DUR 3 /* destination unreachable */ #define ICMP_SQ 4 /* source quench */ #define ICMP_RD 5 /* redirect */ #define ICMP_ECHO 8 /* echo */ #define ICMP_TE 11 /* time exceeded */ #define ICMP_PP 12 /* parameter problem */ #define ICMP_TS 13 /* timestamp */ #define ICMP_TSR 14 /* timestamp reply */ #define ICMP_IRQ 15 /* information request */ #define ICMP_IR 16 /* information reply */ enum icmp_dur_type { ICMP_DUR_NET = 0, /* net unreachable */ ICMP_DUR_HOST = 1, /* host unreachable */ ICMP_DUR_PROTO = 2, /* protocol unreachable */ ICMP_DUR_PORT = 3, /* port unreachable */ ICMP_DUR_FRAG = 4, /* fragmentation needed and DF set */ ICMP_DUR_SR = 5 /* source route failed */ }; enum icmp_te_type { ICMP_TE_TTL = 0, /* time to live exceeded in transit */ ICMP_TE_FRAG = 1 /* fragment reassembly time exceeded */ }; PACK_STRUCT_BEGIN struct icmp_echo_hdr { PACK_STRUCT_FIELD(u8_t type); PACK_STRUCT_FIELD(u8_t code); PACK_STRUCT_FIELD(u16_t chksum); PACK_STRUCT_FIELD(u16_t id); PACK_STRUCT_FIELD(u16_t seqno); } PACK_STRUCT_STRUCT; PACK_STRUCT_END #define ICMPH_TYPE(hdr) ((hdr)->type) #define ICMPH_CODE(hdr) ((hdr)->code) /** Combines type and code to an u16_t */ #define ICMPH_TYPE_SET(hdr, t) ((hdr)->type = (t)) #define ICMPH_CODE_SET(hdr, c) ((hdr)->code = (c)) |
上面这些宏及数据结构的定义相对简单,前面的宏定义主要定义了ICMP的报文类型,接下来的枚举类型定义了目的不可达和数据报超时的报文代码。后面的结构体定义了ICMP回送报文首部(PACK_STRUCT_FIELD禁止编译器自对齐),这个结构体也可以拿来描述其他类型的首部;最后的宏定义分别用于查询、设置ICMP首部中的部分字段,其中宏变量hdr指向ICMP首部结构体指针。
2.ICMPv4数据报操作函数
在数据报不能递交给任何一个上层协议时,函数icmp_dest_unreach会被调用,以发送一个目的不可达ICMP差错报文给源主机,引起目的不可达的原因是协议不可达;在UDP层处理时还将看到如果UDP数据不能被递交给任何一个应用程序,函数icmp_dest_unreach也会被调用,这里引起目的不可达的具体原因是端口不可达。另一种差错报文是超时报文,发送超时报文的函数叫icmp_time_exceeded,在数据报转发和分片重装过程中,都可能调用该函数,引发超时的具体原因可能有两种:一种是数据报TTL为0;另一种是分片重装时间超时。两种差错报文具体是怎么被发送的?
这里的重点是icmp_send_response函数,它为报文申请空间,然后根据报文类型和代码字段值填写数据,然后计算校验和,最后通过函数ip_output将数据报发送出去。
IP层收到ICMP报文会调用icmp_input函数处理,该函数根据报文的不同类型做出不同处理。目前LWIP只支持ICMP回送请求报文的处理,而对其他类型的ICMP报文直接丢弃,不做任何响应,这在嵌入式产品中也够用了。
3.ICMPv6数据报描述与操作
LWIP 1.4.1版本中对IPv6的支持有限,并没有实现邻居发现和多播监听发现功能。报文跟ICMPv4类似,LWIP也只实现了目的不可达、超时、回送请求/应答报文,不同的是ICMPv6三种报文分别用三个数据结构来描述了,代码如下:
// rt-thread\components\net\LWIP-1.4.1\src\include\ipv6\LWIP\icmp.h #define ICMP6_DUR 1 #define ICMP6_TE 3 #define ICMP6_ECHO 128 /* echo */ #define ICMP6_ER 129 /* echo reply */ enum icmp_dur_type { ICMP_DUR_NET = 0, /* net unreachable */ ICMP_DUR_HOST = 1, /* host unreachable */ ICMP_DUR_PROTO = 2, /* protocol unreachable */ ICMP_DUR_PORT = 3, /* port unreachable */ ICMP_DUR_FRAG = 4, /* fragmentation needed and DF set */ ICMP_DUR_SR = 5 /* source route failed */ }; enum icmp_te_type { ICMP_TE_TTL = 0, /* time to live exceeded in transit */ ICMP_TE_FRAG = 1 /* fragment reassembly time exceeded */ }; struct icmp_echo_hdr { u8_t type; u8_t icode; u16_t chksum; u16_t id; u16_t seqno; }; struct icmp_dur_hdr { u8_t type; u8_t icode; u16_t chksum; u32_t unused; }; struct icmp_te_hdr { u8_t type; u8_t icode; u16_t chksum; u32_t unused; }; |
该版本协议栈在ICMPv6中实现的三种报文操作函数:
位于文件LWIP-1.4.1\src\core\ipv6\icmp6.c中,函数为:
void icmp_input(struct pbuf *p, struct netif *inp)
void icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)
void icmp_time_exceeded(struct pbuf *p, enum icmp_te_type t)
4.如何发送ping命令
前面介绍了如何发送ICMP目的不可达报文与超时报文,也介绍了对于接收到的ICMP回送请求报文如何回送应答报文,但ICMP回送请求报文是如何发送的呢?前面介绍的ping命令是根据ICMP回送请求/应答报文实现的,下面就以ping命令的实现过程为例,介绍ICMP回送请求报文如何发送。
在前篇介绍IP协议时,IP层输入函数ip_input对于每个输入的数据包都会调用raw_input进行处理,这是IP层为应用程序直接获取IP数据包提供的一种机制,Socket编程中将这种机制称为原始套接字,而在LWIP内核中,我们可以把它称为原始协议控制块raw_pcb,对raw_pcb的描述如下:
// rt-thread\components\net\LWIP-1.4.1\src\include\ipv4\LWIP\ip.h /* This is the common part of all PCB types. It needs to be at the beginning of a PCB type definition. It is located here so that changes to this common part are made in one location instead of having to change all PCB structs. */ #define IP_PCB \ /* ip addresses in network byte order */ \ ip_addr_t local_ip; \ ip_addr_t remote_ip; \ /* Socket options */ \ u8_t so_options; \ /* Type Of Service */ \ u8_t tos; \ /* Time To Live */ \ u8_t ttl struct ip_pcb { /* Common members of all PCB types */ IP_PCB; }; // rt-thread\components\net\LWIP-1.4.1\src\include\LWIP\raw.h /** Function prototype for raw pcb receive callback functions. * @param arg user supplied argument (raw_pcb.recv_arg) * @param pcb the raw_pcb which received data * @param p the packet buffer that was received * @param addr the remote IP address from which the packet was received * @return 1 if the packet was 'eaten' (aka. deleted), * 0 if the packet lives on * If returning 1, the callback is responsible for freeing the pbuf * if it's not used any more. */ typedef u8_t (*raw_recv_fn)(void *arg, struct raw_pcb *pcb, struct pbuf *p, ip_addr_t *addr); struct raw_pcb { /* Common members of all PCB types */ IP_PCB; struct raw_pcb *next; u8_t protocol; /** receive callback function */ raw_recv_fn recv; /* user-supplied argument for the recv callback */ void *recv_arg; }; |
原始协议控制块raw_pcb如上所示,每一个控制块raw_pcb可定制一个特定协议类型的IP数据包,如ICMP包、TCP包、UDP包等。当IP层收到一个数据包后,如果该包首部中的IP地址和协议字段与某个raw_pcb吻合,则数据包会被递交给这个raw_pcb处理。raw_pcb对数据包的处理过程很简单,直接调用raw_pcb上注册的recv回调函数,用户根据自己的需要编写这个回调函数,从而完成对该IP包的特定处理。内核中可能同时存在多个raw_pcb,它们各自定制不同连接上的不同协议包,因此内核利用next字段将所有raw_pcb组织在一个名为raw_pcbs的链表上,方便对各个原始协议控制块进行遍历操作。下面看看前篇提到的ip_input函数内调用的raw_input函数是如何工作的:
// rt-thread\components\net\LWIP-1.4.1\src\core\raw.c /** * Determine if in incoming IP packet is covered by a RAW PCB * and if so, pass it to a user-provided receive callback function. * Given an incoming IP datagram (as a chain of pbufs) this function * finds a corresponding RAW PCB and calls the corresponding receive * callback function. * @param p pbuf to be demultiplexed to a RAW PCB. * @param inp network interface on which the datagram was received. * @return - 1 if the packet has been eaten by a RAW PCB receive * callback function. The caller MAY NOT not reference the * packet any longer, and MAY NOT call pbuf_free(). * @return - 0 if packet is not eaten (pbuf is still referenced by the * caller). */ u8_t raw_input(struct pbuf *p, struct netif *inp) { struct raw_pcb *pcb, *prev; struct ip_hdr *iphdr; s16_t proto; u8_t eaten = 0; iphdr = (struct ip_hdr *)p->payload; proto = IPH_PROTO(iphdr); prev = NULL; pcb = raw_pcbs; /* loop through all raw pcbs until the packet is eaten by one */ /* this allows multiple pcbs to match against the packet by design */ while ((eaten == 0) && (pcb != NULL)) { if ((pcb->protocol == proto) && (ip_addr_isany(&pcb->local_ip) || ip_addr_cmp(&(pcb->local_ip), ¤t_iphdr_dest))) { /* receive callback function available? */ if (pcb->recv != NULL) { /* the receive callback function did not eat the packet? */ if (pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr()) != 0) { /* receive function ate the packet */ p = NULL; eaten = 1; if (prev != NULL) { /* move the pcb to the front of raw_pcbs so that is found faster next time */ prev->next = pcb->next; pcb->next = raw_pcbs; raw_pcbs = pcb; } } } /* no receive callback function was set for this raw PCB */ } /* drop the packet */ prev = pcb; pcb = pcb->next; } return eaten; } |
可见,raw_input的处理过程就是一个循环查找的过程,它为IP数据包查找一个协议字段和IP地址都吻合的原始协议控制块,并调用该控制块注册的回调函数recv处理数据包。其余的raw_pcb的操作函数见下表:
raw_pcb操作函数 | 函数功能描述 |
struct raw_pcb * raw_new(u8_t proto) | 创建一个raw_pcb并插入raw_pcbs链表首部,以proto作为协议类型初始化该控制块; |
void raw_remove(struct raw_pcb *pcb) | 从raw_pcbs链表中移除某raw_pcb并释放其内存空间; |
err_t raw_bind(struct raw_pcb *pcb, ip_addr_t *ipaddr) | 将本地IP地址绑定到raw_pcb上(设置IP_PCB中的local_ip); |
err_t raw_connect(struct raw_pcb *pcb, ip_addr_t *ipaddr) | 将对端IP地址绑定到raw_pcb上(设置IP_PCB中的remote_ip); |
void raw_recv(struct raw_pcb *pcb, raw_recv_fn recv, void *recv_arg) | 向raw_pcb中注册回调函数recv及其参数recv_arg; |
err_t raw_sendto(struct raw_pcb *pcb, struct pbuf *p, ip_addr_t *ipaddr) | 将一个raw IP数据包发送到目的IP地址对应的主机,raw IP数据包首部字段值由raw_pcb提供; |
err_t raw_send(struct raw_pcb *pcb, struct pbuf *p) | 实际调用raw_sendto; |
利用raw_pcb的结构体及其操作函数,我们可以注册一个ICMP协议的原始协议控制块,用来接收IP层的ping响应包,同时利用内核的定时机制,周期性地往对端IP地址构造并发送ping请求包,而在原始协议控制块的recv回调函数中接收并处理ping响应。按照上述原理发送ping请求的实现代码如下:
#define PING_DELAY 1000 #define PING_ID 0xAFAF #define PING_DATA_SIZE 32 /* ping variables */ static u16_t ping_seq_num; static u32_t ping_time; static struct raw_pcb *ping_pcb = NULL; static ip_addr_t ping_dst; /** Prepare a echo ICMP request */ static void ping_prepare_echo( struct icmp_echo_hdr *iecho, u16_t len) { size_t i; size_t data_len = len - sizeof(struct icmp_echo_hdr); ICMPH_TYPE_SET(iecho, ICMP_ECHO); ICMPH_CODE_SET(iecho, 0); iecho->chksum = 0; iecho->id = PING_ID; iecho->seqno = htons(++ping_seq_num); /* fill the additional data buffer with some data */ for(i = 0; i < data_len; i++) { ((char*)iecho)[sizeof(struct icmp_echo_hdr) + i] = (char)i; } iecho->chksum = inet_chksum(iecho, len); } /* Ping using the raw ip */ static u8_t ping_recv(void *arg, struct raw_pcb *pcb, struct pbuf *p, ip_addr_t *addr) { struct icmp_echo_hdr *iecho; //we can also check src ip here, but just egnore it if ((p->tot_len >= (PBUF_IP_HLEN + sizeof(struct icmp_echo_hdr)))) { iecho = (struct icmp_echo_hdr *)((u8_t*)p->payload + PBUF_IP_HLEN); if ((iecho->type == ICMP_ER) && (iecho->id == PING_ID) && (iecho->seqno == htons(ping_seq_num))) { LWIP_DEBUGF( PING_DEBUG, ("ping: recv ")); ip_addr_debug_print(PING_DEBUG, addr); LWIP_DEBUGF( PING_DEBUG, (" time=%"U32_F" ms\n", (sys_now()-ping_time))); pbuf_free(p); return 1; /* eat the packet */ } } return 0; /* don't eat the packet */ } static void ping_send(struct raw_pcb *raw, ip_addr_t *addr) { struct pbuf *p; struct icmp_echo_hdr *iecho; size_t ping_size = sizeof(struct icmp_echo_hdr) + PING_DATA_SIZE; p = pbuf_alloc(PBUF_IP, (u16_t)ping_size, PBUF_RAM); if (!p) { return; } if ((p->len == p->tot_len) && (p->next == NULL)) { iecho = (struct icmp_echo_hdr *)p->payload; ping_prepare_echo(iecho, (u16_t)ping_size); raw_sendto(raw, p, addr); ping_time = sys_now(); LWIP_DEBUGF(PING_DEBUG, ("ping:[%"U32_F"] send ", ping_seq_num)); ip_addr_debug_print(PING_DEBUG, addr); LWIP_DEBUGF( PING_DEBUG, ("\n")); } pbuf_free(p); } static void ping_timeout(void *arg) { struct raw_pcb *pcb = (struct raw_pcb*)arg; ping_send(pcb, &ping_dst); sys_timeout(PING_DELAY, ping_timeout, pcb); } static void ping_raw_init(void) { ping_pcb = raw_new(IP_PROTO_ICMP); raw_recv(ping_pcb, ping_recv, NULL); raw_bind(ping_pcb, IP_ADDR_ANY); sys_timeout(PING_DELAY, ping_timeout, ping_pcb); } void ping_init(void) { IP4_ADDR(&ping_dst, 192,168,1,103); ping_raw_init(); } |
上面的代码主要功能有两个:一是通过注册到新建raw_pcb上的ping_recv函数,当接收到IP层的ping回送请求包时,通过串口打印ping响应信息;二是通过周期性函数ping_timeout不断调用ping_send向对端IP地址发送ping请求包(由函数ping_prepare_echo构造),并通过串口打印ping请求包发送信息。
更多内容详见下一节:LWIP协议栈解析(七)——网络传输管理之UDP协议