IP协议说明


前言

IP 指网际互连协议, Internet Protocol 的缩写,是 TCP/IP 体系中的网络层协议。设计 IP 的目的是提高网络的可扩展性:一是解决互联网问题,实现大规模、异构网络的互联互通;二是分割顶层网络应用和底层网络技术之间的耦合关系,以利于两者的独立发展。根据端到端的设计原则,IP 只为主机提供一种无连接、不可靠的、尽力而为的数据包传输服务。


一、IP协议的简介

IP 协议是整个 TCP/IP 协议族的核心,也是构成互联网的基础。 IP 位于 TCP/IP 模型的网络层(相当于 OSI 模型的网络层),它可以向传输层提供各种协议的信息,例如 TCP、 UDP 等;对下可将 IP 信息包放到链路层,通过以太网、令牌环网络等各种技术来传送。 为了能适应异构网络, IP 强调适应性、简洁性和可操作性,并在可靠性做了一定的牺牲。

二、IP数据报

IP 层数据报也叫做 IP 数据报或者 IP 分组, IP 数据报组装在以太网帧中发送的,它通常由两个部分组成,即 IP 首部与数据区域, 其中 IP 的首部是 20 字节大小,数据区域理论上可以多达65535 个字节, 由于以太网网络接口的最大传输单元为 1500,所以一个完整的数据包不能超出 1500 字节大小。
 IP 数据报结构
(1) 版本: 占 4 位指 IP 协议的版本。通信双方使用的 IP 协议版本必须一致。广泛使用的IP 协议版本号为 4(即 IPv4)。
(2) 首部长度: 占 4 位可表示的最大十进制数值是 15。请注意,这个字段所表示数的单位是 32 位字长(1 个 32 位字长是 4 字节),因此,当 IP 的首部长度为 1111 时(即十进制的 15),首部长度就达到 60 字节。当 IP 分组的首部长度不是 4 字节的整数倍时,必须利用最后的填充字段加以填充。因此数据部分永远在 4 字节的整数倍开始,这样在实现 IP 协议时较为方便。首部长度限制为 60 字节的缺点是有时可能不够用。但这样做是希望用户尽量减少开销。最常用的首部长度就是 20 字节(即首部长度为 0101),这时不使用任何选项。
(3) 区分服务: 占 8 位, 用来获得更好的服务。这个字段在旧标准中叫做服务类型,但实际上一直没有被使用过。
(4) 总长度: 总长度指首部和数据之和的长度,单位为字节。总长度字段为 16 位,因此数据报的最大长度为 2^16-1=65534 字节。在 IP 层下面的每一种数据链路层都有自己的帧格式,其中包括帧格式中的数据字段的最大长度,这称为最大传送单元 MTU。当一个数据报封装成链路层的帧时,此数据报的总长度(即首部加上数据部分)一定不能超过下面的数据链路层的 MTU 值。
(5) 标识(identification): 占 16 位 IP 软件在存储器中维持一个计数器,每产生一个数据报,计数器就加 1,并将此值赋给标识字段。但这个“标识”并不是序号,因为 IP 是无连接服务,数据报不存在按序接收的问题。当数据报由于长度超过网络的 MTU 而必须分片时,这个标识字段的值就被复制到所有的数据报的标识字段中。相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。
(6) 标志(flag): 占 3 位但只有 2 位有意义的。

  1. 标志字段中的最低位记为 MF(More Fragment)。 MF=1 即表示后面“还有分片”的数据报。 MF=0 表示这已是若干数据报片中的最后一个。
  2. 标志字段中间的一位记为 DF(Don’ t Fragment),意思是“不能分片”。只有当 DF=0时才允许分片。
    (7) 片偏移: 占 13 位片偏移指出:较长的分组在分片后,某片在原分组中的相对位置。也就是说,相对用户数据字段的起点,该片从何处开始。片偏移以 8 个字节为偏移单位。这就是说,除了最后一个分片,每个分片的长度一定是 8 字节(64 位)的整数倍。
    (8) 生存时间: 占 8 位生存时间字段常用的的英文缩写是 TTL(Time To Live),表明是数据报在网络中的寿命。由发出数据报的源点设置这个字段。其目的是防止无法交付的数据报无限制地在因特网中兜圈子,因而白白消耗网络资源。最初的设计是以秒作为 TTL 的单位。每经过一个路由器时,就把 TTL 减去数据报在路由器消耗掉的一段时间。若数据报在路由器消耗的时间小于 1 秒,就把 TTL 值减 1。当 TTL 值为 0 时,就丢弃这个数据报。后来把 TTL 字段的功能改为“跳数限制”(但名称不变)。路由器在转发数据报之前就把 TTL 值减 1.若 TTL 值减少到零,就丢弃这个数据报,不再转发。因此, TTL 的单位不再是秒,而是跳数。 TTL 的意义是指明数据报在网络中至多可经过多少个路由器。显然,数据报在网络上经过的路由器的最大数值是 255。 若把 TTL 的初始值设为 1,就表示这个数据报只能在本局域网中传送。
    (9) 协议: 占 8 位协议字段指出此数据报携带的数据是使用何种协议,以便使目的主机的IP 层知道应将数据部分上交给哪个处理过程。
    (10) 首部检验和: 占 16 位这个字段只检验数据报的首部,但不包括数据部分。这是因为数据报每经过一个路由器,路由器都要重新计算一下首部检验和(一些字段,如生存时间、标志、片偏移等都可能发生变化)。不检验数据部分可减少计算的工作量。
    (11) 源地址: 占 32 位。
    (12) 目的地址: 占 32 位。
    (13) 数据区域: 这是 IP 数据报的最后的一个字段,也是最重要的内容, lwIP 发送数据报是把该层的首部封装到数据包里面,在 IP 层也是把 IP 首部封装在其中,因为有数据区域才会有数据报首部的存在,在大多数情况下, IP 数据报中的数据字段包含要交付给目标 IP 地址的运输层(TCP 协议或 UDP 协议),当然数据区域也可承载其他类型的报文,如 ICMP 报文等。

1.IP 数据报结构

在 lwIP 中,为了描述 IP 报文结构, 它在 ip4.h 文件中定义了一个 ip_hdr 结构体来描述 IP数据报的内容:

struct ip_hdr {
/* 版本号+首部长度+服务类型 */
PACK_STRUCT_FLD_8(u8_t _v_hl);
/* 服务类型 */
PACK_STRUCT_FLD_8(u8_t _tos);
/* 总长度(IP 首部+数据区) */
PACK_STRUCT_FIELD(u16_t _len);
/* 数据包标识(编号) */
PACK_STRUCT_FIELD(u16_t _id);
/* 标志+片偏移 */
PACK_STRUCT_FIELD(u16_t _offset);
/* IP 首部标志定义 */
#define IP_RF 0x8000U /* 保留 */
#define IP_DF 0x4000U /* 是否允许分片 */
#define IP_MF 0x2000U /* 后续是否还有更多分片 */
#define IP_OFFMASK 0x1fffU /* 片偏移域掩码 */
/* 生存时间(最大转发次数)+协议类型(IGMP:1、 UDP:17、 TCP:6) */
PACK_STRUCT_FLD_8(u8_t _ttl);
/* 协议*/
PACK_STRUCT_FLD_8(u8_t _proto);
/* 校验和(IP 首部) */
PACK_STRUCT_FIELD(u16_t _chksum);
/* 源 IP 地址/目的 IP 地址 */
PACK_STRUCT_FLD_S(ip4_addr_p_t src);
PACK_STRUCT_FLD_S(ip4_addr_p_t dest);
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

2.IP 数据报的分片解析

TCP/IP 协议栈为什么具备分片的概念,因为应用程序处理的数据是不确定的,可能超出网络接口最大传输单元,为此 TCP/IP 协议栈引入了分片概念,它是以 MTU 为界限对这个大型的数据切割成多个小型的数据包。这些小型的数据叫做 IP 的分组和分片,它们在接收方进行重组处理,这样,接收方的应用程序接收到这个大型的数据了。总的来讲, IP 数据报的分片概念是为了解决 IP 数据报数据过大的问题而诞生。 注:以太网最大传输单元 MTU 为 1500。
假设 IP 数据报整体的大小为 4000 字节, IP 首部默认为 20 字节, 而数据区域为 3980。由于以太网最大传输单元为 1500, 所以 lwIP 内核会把这个数据报进行分片处理。

  1. 第一个 IP 分片:
    分片数据大小: 20(IP 首部) + 1480(数据区域)。
    标识: 888。
    标志: IP_MF = 1 后续还有分片。
    片偏移量: 片偏移量是 0, 单位是 8 字节, 本片偏移量相当于 0 字节。
  2. 第二片 IP 数据报:
    分片数据大小: 20(IP 首部) + 1480(数据区域)。
    标识: 888。
    标志: IP_MF = 1 后续还有分片。
    片偏移量: 片偏移量是 185(1480/8), 单位是 8 字节, 本片偏移量相当于 1480 字节。
  3. 第三片 IP 数据报:
    分片数据大小: 20(IP 首部) + 1020(数据区域)。
    标识: 888。
    标志: IP_MF = 0, 后续没有分片。
    片偏移量: 片偏移量是 370(185+185), 单位是 8 字节, 本片偏移量相当于 2960 字节。
    注:这些分片的标识都是一致的,而 IP_MF 表示后续有没有分片,若 IP_MF 为 0,则这个分片为最后一个分片。
     IP 数据报分片示意图
    从上图可以看出,一个大型的 IP 数据包经过网络层处理,它会被分成两个或者两个以上的 IP 分片,这些分片的数据组合起来就是应用程序发送的数据与传输层的首部。
    lwIP实现它的函数为 ip4_frag,代码如下(示例):
/**
* 如果 IP 数据报对 netif 来说太大,则将其分片,
将数据报切成 MTU 大小的块,然后按顺序发送通过将 pbuf_ref 指向 p
* @param p:要发送的 IP 数据包
* @param netif:发送的 netif
* @param dest:目的 IP 地址
* @return ERR_OK:发送成功, err_t:其他
*/
err_t
ip4_frag(struct pbuf *p, struct netif *netif, const ip4_addr_t *dest)
{
struct pbuf *rambuf;
#if !LWIP_NETIF_TX_SINGLE_PBUF
struct pbuf *newpbuf;
u16_t newpbuflen = 0;
u16_t left_to_copy;
#endif
struct ip_hdr *original_iphdr;
struct ip_hdr *iphdr;
/* (1500 - 20)/8 = 偏移 185 */
const u16_t nfb = (u16_t)((netif->mtu - IP_HLEN) / 8);
u16_t left, fragsize;
u16_t ofo;
int last;
u16_t poff = IP_HLEN; /* IP 头部长度 */
u16_t tmp;
int mf_set;
original_iphdr = (struct ip_hdr *)p->payload; /* 指向数据报 */
iphdr = original_iphdr;
/* 判断 IP 头部是否为 20 */
if (IPH_HL_BYTES(iphdr) != IP_HLEN) {
return ERR_VAL;
}
/* tmp 变量获取标志和片偏移数值 */
tmp = lwip_ntohs(IPH_OFFSET(iphdr));
/* ofo = 片偏移 */
ofo = tmp & IP_OFFMASK;
/* mf_set = 分片标志 */
mf_set = tmp & IP_MF;
/* left = 总长度减去 IP 头部等于有效数据长度, 4000 - 20 = 3980 */
left = (u16_t)(p->tot_len - IP_HLEN);
/* 判断 left 是否为有效数据 */
while (left) {
/* 判断有效数据和偏移数据大小, fragsize = 1480 (3980 < 1480 ? 3980 : 1480) */
fragsize = LWIP_MIN(left, (u16_t)(nfb * 8));
/* rambuf 申请 20 字节大小的内存块 */
rambuf = pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);
if (rambuf == NULL) {
goto memerr;
}
/* 这个 rambuf 有效数据指针指向 original_iphdr 数据报 */
SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
/* iphdr 指向有效区域地址 rambuf->payload */
iphdr = (struct ip_hdr *)rambuf->payload;
/* left_to_copy = 偏移数据大小(1480) */
left_to_copy = fragsize;
while (left_to_copy) {
struct pbuf_custom_ref *pcr;
/* 当前 pbuf 中数据的长度,plen = 3980 - 20 = 3960 */
u16_t plen = (u16_t)(p->len - poff);
/* newpbuflen = 1480 (1480 < 3960 ? 1480 : 3960) */
newpbuflen = LWIP_MIN(left_to_copy, plen);
if (!newpbuflen) {
poff = 0;
p = p->next;
continue;
}
/* pcr 申请内存 */
pcr = ip_frag_alloc_pbuf_custom_ref();
if (pcr == NULL) {
pbuf_free(rambuf);
goto memerr;
}
/* newpbuf 申请内存 1480 字节,
保存了这个数据区域偏移 poff 字节的数据(p->payload + poff) */
newpbuf = pbuf_alloced_custom(PBUF_RAW, newpbuflen, PBUF_REF, &pcr->pc,
(u8_t *)p->payload + poff, newpbuflen);
if (newpbuf == NULL) {
/* 释放内存 */
ip_frag_free_pbuf_custom_ref(pcr);
pbuf_free(rambuf);
goto memerr;
}
/* 增加 pbuf 的引用计数 */
pbuf_ref(p);
pcr->original = p;
pcr->pc.custom_free_function = ipfrag_free_pbuf_custom;
/* 将它添加到 rambuf 的链的末尾 */
pbuf_cat(rambuf, newpbuf);
/* left_to_copy = 0 (1480 - 1480) */
left_to_copy = (u16_t)(left_to_copy - newpbuflen);
if (left_to_copy) {
poff = 0;
p = p->next;
}
}
/* poff = 1500 (20 + 1480) */
poff = (u16_t)(poff + newpbuflen);
/* last = 0 (3980 <= (1500 - 20)) */
last = (left <= netif->mtu - IP_HLEN);
/* 设置新的偏移量和 MF 标志 */
tmp = (IP_OFFMASK & (ofo));
/* 判断是否是最后一个分片 */
if (!last || mf_set) {
/* 最后一个片段设置了 MF 为 0 */
tmp = tmp | IP_MF;
}
/* 分段偏移与标志字段 */
IPH_OFFSET_SET(iphdr, lwip_htons(tmp));
/* 设置数据报总长度 = 1500 (1480 + 20) */
IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + IP_HLEN)));
/* 校验为 0 */
IPH_CHKSUM_SET(iphdr, 0);
/* 发送 IP 数据报 */
netif->output(netif, rambuf, dest);
IPFRAG_STATS_INC(ip_frag.xmit);
/* rambuf 释放内存 */
pbuf_free(rambuf);
/* left = 2500 (3980 - 1480) */
left = (u16_t)(left - fragsize);
/* 片偏移 ofo = 185(0 + 185) */
ofo = (u16_t)(ofo + nfb);
}
MIB2_STATS_INC(mib2.ipfragoks);
return ERR_OK;
memerr:
MIB2_STATS_INC(mib2.ipfragfails);
return ERR_MEM;
}
MIB2_STATS_INC(mib2.ipfragoks);
return ERR_OK;
memerr:
MIB2_STATS_INC(mib2.ipfragfails);
return ERR_MEM;
}

此函数非常简单,首先判断这个大型数据包的有效区域总长度,系统根据这个总长度划分数据区域,接着申请 20+sizeof(struct pbuf)字节的 rampbuf 来存储 IP 首部,然后根据 poff 数值让被分片数据包的 payload 指针偏移 poff 大小,它所指向的地址由 newpbuf 数据包的 payload指针指向,最后调用 netif->output 函数发送该分片,其他分片一样操作。
IP 数据报分片原理示意图
newpbuf 的 payload 指针指向的地址由左边的 payload 指针经过偏移得来的。

3.IP 数据报的分片重装

由于 IP 分组在网络传输过程中到达目的地点的时间是不确定的,所以后面的分组可能比前面的分组先达到目的地点。 为此, lwIP 内核需要将接收到的分组暂存起来,等所有的分组都接收完成之后,再将数据传递给上层。
在 lwIP 中,有专门的结构体负责缓存这些分组,这个结构体为 ip_reassdata 重装数据链表,该结构体在 ip4_frag.h 文件中定义:

/* 重装数据结构体 */
struct ip_reassdata {
struct ip_reassdata *next; /* 指向下一个重装节点 */
struct pbuf *p; /* 指向分组的 pbuf */
struct ip_hdr iphdr; /* IP 数据报的首部 */
u16_t datagram_len; /* 已收到数据的长度 */
u8_t flags; /* 标志是否最后一个分组 */
u8_t timer; /* 超时间隔 */
};

IP 分组重装示意图
可以看到,这些分片挂载到同一个重装节点上,它们挂载之前,是把 IP 首部的前 8 字节强制转换成三个字段,其中 next_pbuf 指针用来链接这些 IP 分组,形成了单向链表,而 start和 end 字段用来描述分组的顺序, lwIP 系统根据这些数值对分组进行排序。

三、IP 数据报的输出

无论是 UDP 还是 TCP,它们的数据段递交至网络层的接口是一致的,这个接口函数如下所示:

	err_t
	ip4_output_if_src(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,
	u8_t ttl, u8_t tos,
	u8_t proto, struct netif *netif)
	{
	struct ip_hdr *iphdr;
	ip4_addr_t dest_addr;
	if (dest != LWIP_IP_HDRINCL){
	u16_t ip_hlen = IP_HLEN;
	/* 第一步: 生成 IP 报头 */
	if (pbuf_header(p, IP_HLEN)){
	return ERR_BUF;
	}
	/* 第二步: iphdr 指向 IP 头部指针 */
	iphdr = (struct ip_hdr *)p->payload;
	/* 设置生存时间(最大转发次数) */
	IPH_TTL_SET(iphdr, ttl);
	/* 设置协议类型(IGMP:1、 UDP:17、 TCP:6) */
	IPH_PROTO_SET(iphdr, proto);
	/* 设置目的 IP 地址 */
	ip4_addr_copy(iphdr->dest, *dest);
	/* 设置版本号+设置首部长度 */
	IPH_VHL_SET(iphdr, 4, ip_hlen / 4);
	/* 服务类型 */
	IPH_TOS_SET(iphdr, tos);
	/* 设置总长度(IP 首部+数据区) */
	IPH_LEN_SET(iphdr, lwip_htons(p->tot_len));
	/* 设置标志+片偏移 */
	IPH_OFFSET_SET(iphdr, 0);
	/* 设置数据包标识(编号) */
	IPH_ID_SET(iphdr, lwip_htons(ip_id));
	/* 每发送一个数据包,编号加一 */
	++ip_id;
	/* 没有指定源 IP 地址 */
	if (src == NULL){
	/* 将当前网络接口 IP 地址设置为源 IP 地址 */
	ip4_addr_copy(iphdr->src, *IP4_ADDR_ANY4);
	}
	else{
	/* 复制源 IP 地址 */
	ip4_addr_copy(iphdr->src, *src);
	}
	}
	else{
	/* IP 头部已经包含在 pbuf 中 */
	iphdr = (struct ip_hdr *)p->payload;
	ip4_addr_copy(dest_addr, iphdr->dest);
	dest = &dest_addr;
	}
	IP_STATS_INC(ip.xmit);
	ip4_debug_print(p);
	/* 如果数据包总长度大于 MTU,则分片发送 */
	if (netif->mtu && (p->tot_len > netif->mtu)){
	return ip4_frag(p, netif, dest);
	}
	/* 如果数据包总长度不大于 MTU,则直接发送 */
	return netif->output(netif, p, dest);
	}

函数 ip4_output_if_src()流程图
此函数首先判断目标 IP 地址是否为 NULL,若目标 IP 地址不为空,则偏移 payload 指针添加 IP 首部,偏移完成之后设置 IP 首部字段信息,接着判断该数据包的总长度是否大于以太网传输单元,若大于,则调用 ip4_frag 函数对这个数据包分组并且逐一发送,否则直接调用ethrap_output 函数把数据包递交给 ARP 层处理。

四、IP 数据报的输入

数据包提交给网络层之前,系统需要判断接收到的数据包是 IP 数据包还是 ARP 数据包,若接收到的是 IP 数据包,则 lwIP 内核调用 ip4_input 函数处理这个数据包,该函数如下所示:

err_t
ip4_input(struct pbuf *p, struct netif *inp)
{
struct ip_hdr *iphdr;
struct netif *netif;
u16_t iphdr_hlen;
u16_t iphdr_len;
#if IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP
int check_ip_src = 1;
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP */
IP_STATS_INC(ip.recv);
MIB2_STATS_INC(mib2.ipinreceives);
/* 识别 IP 报头 */
iphdr = (struct ip_hdr *)p->payload;
/* 第一步:判断版本是否为 IPv4 */
if (IPH_V(iphdr) != 4)
{
ip4_debug_print(p);
pbuf_free(p); /* 释放空间 */
IP_STATS_INC(ip.err);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinhdrerrors);
return ERR_OK;
}
/* 以 4 字节(32 位)字段获得 IP 头的长度 */
iphdr_hlen = IPH_HL(iphdr);
/* 以字节计算 IP 报头长度 */
iphdr_hlen *= 4;
/* 以字节为单位获取 ip 长度 */
iphdr_len = lwip_ntohs(IPH_LEN(iphdr));
/* 修剪 pbuf。这对于< 60 字节的数据包尤其需要。 */
if (iphdr_len < p->tot_len)
{
pbuf_realloc(p, iphdr_len);
}
/* 第二步:标头长度超过第一个 pbuf 长度,或者 ip 长度超过总 pbuf 长度 */
if ((iphdr_hlen > p->len) || (iphdr_len > p->tot_len)
|| (iphdr_hlen < IP_HLEN))
{
if (iphdr_hlen < IP_HLEN)
{ }
if (iphdr_hlen > p->len)
{ }
if (iphdr_len > p->tot_len)
{ }
/* 释放空间 */
pbuf_free(p);
IP_STATS_INC(ip.lenerr);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipindiscards);
return ERR_OK;
}
/* 第三步:验证校验和 */
#if CHECKSUM_CHECK_IP
/* 省略代码 */
#endif
/* 将源 IP 地址与目标 IP 地址复制到对齐的 ip_data.current_iphdr_src 和
ip_data.current_iphdr_dest */
ip_addr_copy_from_ip4(ip_data.current_iphdr_dest, iphdr->dest);
ip_addr_copy_from_ip4(ip_data.current_iphdr_src, iphdr->src);
/* 第四步:匹配数据包和接口,即这个数据包是否发给本地 */
if (ip4_addr_ismulticast(ip4_current_dest_addr()))
{
#if LWIP_IGMP
/* 省略代码 */
#else /* LWIP_IGMP */
/* 如果网卡已经挂载了和 IP 地址有效 */
if ((netif_is_up(inp)) && (!ip4_addr_isany_val(*netif_ip4_addr(inp))))
{
netif = inp;
}
else
{
netif = NULL;
}
#endif /* LWIP_IGMP */
}
/* 如果数据报不是发给本地 */
else
{
int first = 1;
netif = inp;
do
{
/* 接口已启动并配置? */
if ((netif_is_up(netif)) &&
(!ip4_addr_isany_val(*netif_ip4_addr(netif))))
{
/* 单播到此接口地址? */
if (ip4_addr_cmp(ip4_current_dest_addr(),
netif_ip4_addr(netif)) ||
/* 或广播在此接口网络地址? */
ip4_addr_isbroadcast(ip4_current_dest_addr(), netif)
#if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF
|| (ip4_addr_get_u32(ip4_current_dest_addr()) ==
PP_HTONL(IPADDR_LOOPBACK))
#endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */
)
{
break;
}
#if LWIP_AUTOIP
if (autoip_accept_packet(netif, ip4_current_dest_addr()))
{
/* 跳出 if 循环 */
break;
}
#endif /* LWIP_AUTOIP */
}
if (first)
{
#if !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF
/* 检查一下目标 IP 地址是否是环回地址 */
if (ip4_addr_isloopback(ip4_current_dest_addr()))
{
netif = NULL;
break;
}
#endif /* !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF */
first = 0;
netif = netif_list;
}
else
{
netif = netif->next;
}
if (netif == inp)
{
netif = netif->next;
}
} while (netif != NULL);
}
#if IP_ACCEPT_LINK_LAYER_ADDRESSING
if (netif == NULL)
{
/* 远程端口是 DHCP 服务器? */
if (IPH_PROTO(iphdr) == IP_PROTO_UDP)
{
struct udp_hdr *udphdr = (struct udp_hdr *)
((u8_t *)iphdr + iphdr_hlen);
if (IP_ACCEPT_LINK_LAYER_ADDRESSED_PORT(udphdr->dest))
{
netif = inp;
check_ip_src = 0;
}
}
}
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */
#if LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING
if (check_ip_src
#if IP_ACCEPT_LINK_LAYER_ADDRESSING
&& !ip4_addr_isany_val(*ip4_current_src_addr())
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */
)
#endif /* LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING */
{
/* 第五步: IP 地址,源 IP 地址不能是多播或者广播地址 */
if ((ip4_addr_isbroadcast(ip4_current_src_addr(), inp)) ||
(ip4_addr_ismulticast(ip4_current_src_addr())))
{
/* 释放空间 */
pbuf_free(p);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinaddrerrors);
MIB2_STATS_INC(mib2.ipindiscards);
return ERR_OK;
}
}
/* 第六步:如果还没找到对应的网卡,数据包不是给我们的 */
if (netif == NULL)
{
/* 路由转发或者丢弃。如果 IP_FORWARD 宏定义被使能,则进行转发 */
#if IP_FORWARD
/* 非广播包? */
if (!ip4_addr_isbroadcast(ip4_current_dest_addr(), inp))
{
/* 尝试在(其他)网卡上转发 IP 数据包 */
ip4_forward(p, iphdr, inp);
}
else
#endif /* IP_FORWARD */
{
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinaddrerrors);
MIB2_STATS_INC(mib2.ipindiscards);
}
/* 释放空间 */
pbuf_free(p);
return ERR_OK;
}
/* 第七步:如果数据报由多个片段组成(分片处理)? */
if ((IPH_OFFSET(iphdr) & PP_HTONS(IP_OFFMASK | IP_MF)) != 0)
{
/* 重装数据报*/
p = ip4_reass(p);
/* 如果重装没有完成 */
if (p == NULL)
{
return ERR_OK;
}
/* 分片重装完成,将数据报首部强制转换为 ip_hdr 类型 */
iphdr = (struct ip_hdr *)p->payload;
}
#if IP_OPTIONS_ALLOWED == 0
#if LWIP_IGMP
if ((iphdr_hlen > IP_HLEN) && (IPH_PROTO(iphdr) != IP_PROTO_IGMP))
{
#else
/* 第八步:如果 IP 数据报首部长度大于 20 字节,就表示错误 */
if (iphdr_hlen > IP_HLEN)
{
#endif /* LWIP_IGMP */
/* 释放空间 */
pbuf_free(p);
IP_STATS_INC(ip.opterr);
IP_STATS_INC(ip.drop);
/* u 不受支持的协议特性 */
MIB2_STATS_INC(mib2.ipinunknownprotos);
return ERR_OK;
}
#endif /* IP_OPTIONS_ALLOWED == 0 */
/* 第九步: 发送到上层协议 */
ip4_debug_print(p);
ip_data.current_netif = netif;
ip_data.current_input_netif = inp;
ip_data.current_ip4_header = iphdr;
ip_data.current_ip_header_tot_len = IPH_HL(iphdr) * 4;
#if LWIP_RAW
/* RAW API 输入 */
if (raw_input(p, inp) == 0)
#endif /* LWIP_RAW */
{
/* 转移到有效载荷(数据区域),不需要检查 */
pbuf_header(p, -(s16_t)iphdr_hlen);
/* 根据 IP 数据报首部的协议的类型处理 */
switch (IPH_PROTO(iphdr))
{
#if LWIP_UDP
/* UDP 协议 */
case IP_PROTO_UDP:
#if LWIP_UDPLITE
case IP_PROTO_UDPLITE:
#endif /* LWIP_UDPLITE */
MIB2_STATS_INC(mib2.ipindelivers);
/* IP 层递交给网络层的函数 */
udp_input(p, inp);
break;
#endif /* LWIP_UDP */
#if LWIP_TCP
/* TCP 协议 */
case IP_PROTO_TCP:
MIB2_STATS_INC(mib2.ipindelivers);
/* IP 层递交给网络层的函数 */
tcp_input(p, inp);
break;
#endif /* LWIP_TCP */
pbuf_free(p);/* 释放空间*/
IP_STATS_INC(ip.proterr);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinunknownprotos);
}
}
/* 全局变量清零*/
ip_data.current_netif = NULL;
ip_data.current_input_netif = NULL;
ip_data.current_ip4_header = NULL;
ip_data.current_ip_header_tot_len = 0;
ip4_addr_set_any(ip4_current_src_addr());
ip4_addr_set_any(ip4_current_dest_addr());
return ERR_OK;
}

第一步:判断 IP 数据报的版本是否是 IPv4,如果不是,那么 lwIP 会掉弃该数据报。
第二步:判断标头长度超过第一个 pbuf 长度,或者 ip 长度超过总 pbuf 长度,如果是,那么 lwIP 会丢弃该数据报。
第三步: 验证校验和,如果不正确, 那么 lwIP 会掉弃该数据报。
第四步: 匹配数据包和接口,这个数据包是否发给本地。
第五步:判断 IP 数据报是否是广播或者多播,如果是, 那么 lwIP 会丢弃该数据报。
第六步:如果到了这一步,没有发现网络接口, 那么 lwIP 会丢弃该数据报。
第七步:如果如 IP 数据报不能分片处理, 那么 lwIP 会丢弃该数据报。
第八步:如果 IP 数据报的 IP 首部大于 20 字节, 那么 lwIP 会丢弃该数据报。
第九步: 把数据包递交给上层。
第十步:判断该数据报的协议为 TCP/UDP/ICMP/IGMP,如果不是这四个协议,则丢弃该
数据报。

  • 25
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Modbus TCP/IP协议是一种常用的工业通信协议,用于在计算机网络上实现设备之间的数据交换。它是基于Modbus协议的传统串行通信协议的扩展,通过使用TCP/IP协议栈来实现数据传输,具备更高的可靠性和灵活性。 Modbus TCP/IP协议采用客户端-服务器体系结构,其中客户端是发送请求的设备,而服务器是接收并响应请求的设备。通信过程中,客户端通过发送请求帧到服务器,请求读取或写入特定寄存器的数据。服务器收到请求后,执行相关操作,并以响应帧的形式返回结果给客户端。这种方式方便了设备之间的数据交互,使得多种设备可以方便地共享和访问数据。 Modbus TCP/IP协议定义了一系列功能码,每个功能码对应不同的操作,包括读取和写入寄存器、读取和写入线圈等。通信数据以二进制的形式进行传输,其中每个字节包含8位数据。协议规定了通信的数据格式、消息结构和流程,以确保数据的准确性和一致性。 Modbus TCP/IP协议具有许多优点,例如简单易学、占用带宽低、支持多种网络拓扑结构等。另外,它还具备广泛的应用领域,包括工业自动化、建筑自动化、能源管理等。多种设备和系统都可以通过Modbus TCP/IP协议进行通信,实现数据的共享和控制。 总结来说,Modbus TCP/IP协议是一种功能强大且广泛应用的工业通信协议,通过使用TCP/IP协议栈实现设备的数据交换。它具有简单易学、占用带宽低、支持多种网络拓扑结构等优点,适用于各种工业自动化和建筑自动化应用。 ### 回答2: Modbus TCP/IP是一种基于TCP/IP协议的通信协议,用于在工业自动化系统中实现设备之间的通信。它是Modbus协议的一种变体,用于通过TCP/IP网络连接来传输数据。 Modbus TCP/IP协议的工作方式如下:首先,客户端应用程序通过使用一个TCP/IP连接向Modbus TCP/IP服务器发送请求。服务器接收到请求后,会解析请求的内容,并根据请求执行相应的功能。服务器将执行结果返回给客户端。客户端可以通过多个请求来获取不同数据,例如读取数据、写入数据或执行功能码操作。 Modbus TCP/IP协议中定义了一些常用功能码,用于在不同设备之间进行通信。常见的功能码包括读取保持寄存器、写入单个寄存器、读取输入寄存器等。这些功能码使得设备之间可以进行数据的读取和写入,从而实现了设备之间的数据交换。 Modbus TCP/IP协议允许多个设备通过一个以太网连接进行通信。每个设备都具有唯一的IP地址和端口号,以便在网络上进行识别和通信。此外,Modbus TCP/IP协议还支持多种数据格式,例如32位整数、16位整数、浮点数等。这使得不同类型的数据可以在设备之间进行传输。 总之,Modbus TCP/IP协议是一种用于在工业自动化系统中实现设备通信的协议。它基于TCP/IP协议,通过TCP/IP网络连接来传输数据。该协议提供了一种简单而有效的方式,使得不同设备可以通过以太网进行数据的读取和写入。 ### 回答3: Modbus TCP/IP协议是一种基于以太网的通信协议,用于在不同设备之间进行数据交换。该协议广泛应用于工业领域,可以实现设备之间的实时监控、控制和数据采集等功能。 Modbus TCP/IP协议的结构相对简单,主要分为三部分:传输层、应用层和物理层。 传输层使用TCP协议,保证数据的可靠传输。TCP协议提供了数据包的分组和排序功能,确保数据正确地到达目标设备。 应用层负责定义数据传输格式和协议命令。其中,Modbus协议定义了常见的数据类型和寄存器地址,例如16位整数、32位浮点数、线圈和寄存器等。协议命令包括读取数据、写入数据和异常处理等功能。 物理层使用TCP/IP协议栈,通过以太网进行通信。以太网提供了传输速度快、可扩展性强的优势,适用于高速数据传输和大规模设备互联。 在实际应用中,Modbus TCP/IP协议常用于连接PLC(可编程逻辑控制器)、传感器、监控设备和仪器仪表等。通过该协议,可以实现设备之间的数据采集和监控,以及远程控制和调试。 总的来说,Modbus TCP/IP协议是一种可靠、高效的通信协议,适用于工业自动化和远程监控系统。它的简单结构和可扩展性使得设备之间的数据交换变得更加方便和高效。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

理想本征半导体

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值