LWIP学习笔记2——ARP协议

6 ARP协议

地址解析协议(Address Resolution Protocol, ARP)是通过解析 IP 地址得到数据链路层地址的,是一个在网络协议包中极其重要的网络传输协议,它与网卡有着极其密切的关系,在 TCP/IP 分层结构中,把 ARP 划分为网络层。
在这里插入图片描述

8 网际控制报文协议 ICMP

ICMP 最典型的用途是差错报告。如果由于网络状况、链路不通等数据报无法到达目标主机,ICMP 就会返回一个差错报文,让源主机知道数据没能正常到达目标主机,接着进行重发或者放弃发送都可以。
ICMP 通常被认为是 IP 的一部分,但从体系结构上讲它是位于 IP 之上的,因为 ICMP报文是承载在 IP 数据报中的。这就是说, ICMP 报文是作为 IP 数据报数据区域的(有一些书籍也称之为有效载荷) ,就像 TCP 与 UDP 报文段作为 IP 数据报数据区域那样。类似地,当一台主机收到一个指明上层协议为 ICMP 的 IP 数据报时,它将分解出该数据报的内容给ICMP,就像分解出一个数据报的内容给 TCP 或 UDP 一样,但与 TCP 或 UDP 协议又有所不同, ICMP 出现的目的不是为上层应用程序提供服务,只是在 IP 层传递差错的报文,依赖于 IP 协议进行传输。

8.1 ICMP 报文结构

在这里插入图片描述
在这里插入图片描述

8.2 ICMP 报文类型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.3 ICMP 查询报文

ping 命令使用的就是 ICMP 查询报文,若能 ping 成功,说明网卡、 IP 层、ICMP 层都能通信正常, 所以能证明 LwIP 已经移植成功了,我们一般在移植完成的时候都会测试一下 ping 命令,查看一下是否移植成功。
在这里插入图片描述

8 TCP 协议

TCP 协议(TransmissionControl Protocol,传输控制协议) 它是最常用传输层协议,也是最稳定传输层协议,很多上层应用都是依赖于 TCP 协议进程传输数据,如SMTP、 FTP 等等。
一个完整的 TCP 传输必须有数据的交互,接收方在接收到数据之后必须正面进行确认,向发送方报告接收的结果,而发送方在发送数据之后必须等待接收方的确认,同时发送的时候会启动一个定时器,在指定超时时间内没收到确认,发送方就会认为发送失败,然后进行重发操作,这就是重传报文。
TCP 提供可靠的运输层, 但它依赖的是 IP 层的服务, IP 数据报的传输是无连接、 不可靠的,因此它要通过确认来知道接收方确实已经收到数据了。但数据和确认都有可能会丢失, 因此 TCP 通过在发送时设置一个超时机制(定时器) 来解决这种问题, 如果当超时时间到达的时候还没有收到对方的确认,它就重传该数据。
CP 协议是全双工通信。

8.5 窗口的概念

TCP 提供了流量控制服务(flow-control service)以消除发送方使接收方缓冲区溢出的可能性。流量控制是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配, TCP 通过让发送方维护一个称为接收窗口(receive window)的变量来提供流量控制。
TCP 协议中使用滑动窗口的流量控制方法,它允许接收方根据自身的处理能力来确定能接收数据的多少,因此会告诉发送方可以发送多少数据过来,即窗口的大小,而发送方尽可能将数据都多发到对方那里,所以发送方会根据这个窗口的大小发送对应的数据 。
在这里插入图片描述
在这里插入图片描述
关于窗口的概念必须强调三点:

  1. 发送方的发送窗口并不总是和 接收方接收窗口一样大,因为有一定的时间滞后。
  2. TCP 标准没有规定对不按序到达的数据应如何处理, 通常是先临时存放在接收窗
    口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程。
  3. TCP 要求接收方必须有确认的功能,这样可以减小传输开销。

8.2 端口号的概念

TCP 协议的连接是包括上层应用间的连接,简单来说, TCP 连接是两个不同主机的应用连接,而传输层与上层协议是通过端口号进行识别的,如 IP 协议中以 IP 地址作为识别一样,端口号的取值范围是 0~65535,这些端口标识着上层应用的不同线程,一个主机内可能只有一个 IP 地址,但是可能有多个端口号,每个端口号表示不同的应用线程。
在这里插入图片描述
在这里插入图片描述

8.3TCP 报文段结构

在这里插入图片描述
在这里插入图片描述

8.4 TCP 连接

8.4.1 “三次握手” 建立连接

在这里插入图片描述

8.4.2 “四次挥手” 终止连接

在这里插入图片描述

8.5 TCP 状态

在这里插入图片描述
TCP 状态转移:
在这里插入图片描述
对图 13-14 的补充说明(很重要,要牢牢记住):
 虚线:表示服务器的状态转移。
 实线:表示客户端的状态转移。
 图中所有“关闭”、“打开”都是应用程序主动处理。
 图中所有的“超时”都是内核超时处理。

8.6 TCP 层的工作流程示意图

在这里插入图片描述

8 使用 NETCONN 接口编程

NETCONN API 使用了操作系统的 IPC 机制, 对网络连接进行了抽象,用户可以像操作文件一样操作网络连接(打开/关闭、读/写数据)。 但是 NETCONN API 并不如操作文件的 API 那样简单易用。举个例子,调用 f_read 函数读文件时,读到的数据会被放在一个用户指定的数组中,用户操作起来很方便,而 NETCONN API 的读数据 API,就没有那么人性化了。 用户获得的不是一个数组,而是一个特殊的数据结构 netbuf,用户如果想使用好它,就需要对内核的 pbuf 和 netbuf 结构体有所了解。
netbuf 结构体:
LwIP 为了更好描述应用线程发送与接收的数据,并且为了更好管理这些数据的缓冲区,LwIP 定义了一个 netbuf 结构体,它是基于 pbuf 上更高一层的封装,记录了主机的 IP 地址与端口号。

struct netbuf
 {
	 struct pbuf *p, *ptr; (1)
	 ip_addr_t addr; (2)
	 u16_t port; (3)
 };

在这里插入图片描述
netbuf 相关函数说明:
netbuf 是 LwIP 描述用户数据很重要的一个结构体,因为 LwIP 是不可能让我们直接操作 pbuf 的,因为分层的思想,应用数据必然是由用户操作的, 因此 LwIP 会提供很多函数接口让用户对 netbuf 进行操作,无论是 UDP 报文还是 TCP 报文段,其本质都是数据,要发送出去的数据都会封装在 netbuf 中,然后通过邮箱发送给内核线程(tcpip_thread 线程),然后经过内核的一系列处理,放入发送队列中,然后调用底层网卡发送函数进行发送,反之,应用线程接收到数据,也是通过 netbuf 进行管理。

netconn 结构体:
在 LwIP 中,如 TCP 连接, UDP 通信,都是需要提供一个编程接口给用户使用的,那么为了描述这样子的一个接口, LwIP 抽象出来一个 nettonn 结构体,它能描述一个连接,供应用程序使用,同时内核的 NETCONN API 接口也对各种连接操作函数进行了统一的封装,这样子,用户程序可以很方便使 netconn 和编程函数,我们暂且将 netconn 称之为连接结体。一个连接结构体中包含的成员变量很多,如描述连接的类型,连接的状态(主要是在TCP 连接中使用),对应的控制块(如 UDP 控制块、 TCP 控制块等等),还有对应线程的消息邮箱以及一些记录的信息。

struct netconn
 {
 /** netconn 类型 */
 enum netconn_type type;
 /** 当前 netconn 状态 */
 enum netconn_state state;
 /** LwIP 的控制块指针,如 TCP 控制块、 UDP 控制块 */
 union
 {
	struct ip_pcb *ip;
 	struct tcp_pcb *tcp;
 	struct udp_pcb *udp;
	 struct raw_pcb *raw;
 } pcb;
 err_t pending_err;/** 这个 netconn 最后一个异步未报告的错误 */
 sys_sem_t op_completed; //信号量
 /** 消息邮箱,存储接收的数据,直到它们被提取 */
 sys_mbox_t recvmbox;
 /** 用于 TCP 服务器上的请求连接缓冲区 */
 sys_mbox_t acceptmbox;

 /** socket 描述符,用于 Socket API */
 #if LWIP_SOCKET
 int socket;
 #endif /* LWIP_SOCKET */


 /** 标志 */
 u8_t flags;
 #if LWIP_TCP
 /** 当调用 netconn_write()函数发送的数据不适合发送缓冲区时,
 数据会暂时存储在 current_msg 中,等待数据合适的时候进行发送 */
 struct api_msg *current_msg;
 #endif /* LWIP_TCP */
 /** 连接相关的回调函数 */
 netconn_callback callback;
 };

netconn 函数接口说明:

netconn_new()

函数 netconn_new ()本质上是一个宏定义,它用来创建一个新的连接结构, 连接结构的类型可以选择为 TCP 或 UDP 等,参数 type 描述了连接的类型,可以为 NETCONN_TCP或 NETCONN_UDP 等, 在这个函数被调用时,会初始化相关的字段,而并不会创建连接。

netconn_bind()

netconn_bind()函数用于将一个 IP 地址及端口号与 netconn 连接结构进行绑定,如果作为服务器端,这一步操作是必然需要的,作为客户端,不需要这一步,系统会自动分配端口号,使用默认网卡发送数据。同样的, 该函数会调用 netconn_apimsg()函数构造一个 API 消息,并且请求内核执行 lwip_netconn_do_bind()函数, 然后通过 netconn 连接结构的信号量进行同步,事实上内核线程的处理也是通过函数调用 xxx_bind(xxx_bing 可以是 udp_bing、 tcp_bing、 raw_bing,具体是哪个函数内核是根据 netconn 的类型决定的) 完成相应控制块的绑定工作。

netconn_connect()

netconn_connect()函数是用于连接服务器的函数,它一般在客户端中调用,将服务器端的 IP 地址和端口号与本地的 netconn 连接结构绑定,当 TCP 协议使用该函数的时候就是进行握手的过程,调用的应用线程将阻塞至握手完成;而对于 UDP 协议来说,调用该函数只是设置 UDP 控制块的目标 IP 地址与目标端口号,其实这个函数也是通过调用netconn_apimsg()函数构造一个 API 消息,并且请求内核执行 lwip_netconn_do_connect()函数, 然后通过 netconn 连接结构的信号量进行同步,在 lwip_netconn_do_connect()函数中,根据 netconn 的类型不同, 调用对应的 xxx_connect()函数进行对应的处理,如果是 TCP 连接,将调用 tcp_connect();如果是 UDP 协议,将调用 udp_connect();如果是RAW,将调用 raw_connect()函数处理。

netconn_recv()

它可以接收一个 UDP 或者 TCP的数据包,从 recvmbox 邮箱中获取pbuf数据包,如果该邮箱中没有数据包,那么线程调用这个函数将会进入阻塞状态以等待消息的到来, 如果在等待 TCP 连接上的数据时,远端主机终止连接,将返回一个终止连接的错误代码(ERR_CLSD),应用程序可以根据错误的类型进行不一样的处理。对应 TCP 连接, netconn_recv()函数将调用 netconn_recv_data_tcp()函数去获取 TCP 连接上的数据,在获取数据的过程中,调用 netconn_recv_data()函数从 recvmbox 邮箱获取pbuf, 然后通过 netconn_tcp_recvd_msg()->netconn_apimsg()函数构造一个 API 消息投递给系统邮箱, 请求内核执行 lwip_netconn_do_recv()函数, 该函数将调用 tcp_recved()函数去更新 TCP 接收窗口,同时netconn_recv()函数将完成 pbuf 数据包封装在 netbuf 中,返回个应用程序; 而对于 UDP 协议、 RAW 连接,将简单多了,将直接调用netconn_recv_data()函数获取数据,完成 pbuf 封装在 netbuf 中,返回给应用程序。
在这里插入图片描述

netconn_send()

该函数会调用 netconn_apimsg()函数构造一个 API 消息,并且请求内核执行 lwip_netconn_do_send()函数, 这个函数会通过消息得到目标 IP 地址与端口号以及 pbuf 数据报等信息, 然后调用 raw_send()/udp_send()等函数发送数据,最后通过 netconn 连接结构的信号量进行同步。

netconn_write()

用于处于稳定连接状态的 TCP 协议发送数据,这个函数的功能是把 dataptr 指针指向的数据放在属于 conn 连接的 TCP 连接的发送队列中, size 参数指定了数据的长度, apiflags 参数有以下几种:

/* 没有标志位(默认标志位) */
 #define NETCONN_NOFLAG 0x00

 /* 不拷贝数据到内核线程 */
 #define NETCONN_NOCOPY 0x00
 /* 拷贝数据到内核线程 */
 #define NETCONN_COPY 0x01

 /* 尽快递交给上层应用 */
 #define NETCONN_MORE 0x02

 /* 当内核缓冲区满时,不会被阻塞,而是直接返回 */
 #define NETCONN_DONTBLOCK 0x04

 /* 不自动更新接收窗口,需要调用 netconn_tcp_recvd()函数完成 */
 #define NETCONN_NOAUTORCVD 0x08

 /* 上层已经收到数据,将 FIN 保留在队列中直到再次调用 */

9 使用 Socket 接口编程

在 LwIP 中, Socket API 是基于 NETCONN API 之上来实现的,系统最多提供MEMP_NUM_NETCONN 个 netconn 连接结构,因此 Socket 套接字的个数也是那么多个,为了更好对 netconn 进行封装, LwIP 还定义了一个套接字结构体——lwip_sock(我称之为Socket 连接结构) , 每个 lwip_sock 内部都有一个 netconn 的指针, 实现了对 netconn 的再次封装。
LwIP 定义了一个 lwip_sock 类型的 sockets数组, 通过套接字就可以直接索引并且访问这个结构体了, 这也是为什么套接字是一个整数的原因。

9.1 Socket API

9.1.1 socket()

9.1.1 bind()

用于服务器端绑定套接字与网卡信息

9.1.1 connect()

它用于客户端中,将 Socket 与远端 IP 地址、端口号进行绑定,在
TCP 客户端连接中,调用这个函数将发生握手过程(会发送一个 TCP 连接请求),并最终建立新的 TCP 连接,而对于 UDP 协议来说,调用这个函数只是在 UDP 控制块中记录远端IP 地址与端口号,而不发送任何数据。

9.1.1 listen()

只能在 TCP 服务器中使用,让服务器进入监听状态,等待远端的连接请求, LwIP 中可以接收多个客户端的连接。

9.1.1 accept()

用于 TCP 服务端中,等待着远端主机的连接请求,并且建立一个新的 TCP 连接,在调用这个函数之前需要通过调用 listen()函数让服务器进入监听状态。 accept()函数的调用会阻塞应用线程直至与远程主机建立 TCP 连接。

9.1.1 read()、 recv()、 recvfrom()

9.1.1 sendto()

9.1.1 send()

end()函数可以用于 UDP 协议和 TCP 连接发送数据。在调用 send()函数之前,必须使用 connect()函数将远端主机的 IP 地址、端口号与 Socket 连接结构进行绑定。对于 UDP 会话, send()函数将调用 lwip_sendto()函数发送数据, 而对于 TCP 连接, 将调用netconn_write_partly()函数发送数据。

9.1.1 write()

这个函数一般用于处于稳定的 TCP 连接中传输数据,当然也能用于 UDP 协议中,它也是基于 lwip_send 上实现的,但是无需我们设置 flag 参数。

9.1.1close()

close()函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符索引到连接结构,如果连接是 TCP 协议,将产生一个请求终止连接的报文发送到对端主机中,如果是 UDP 协议,将直接释放 UDP 控制块的内容。

提高 LwIP 网络传输的速度

以太网发送和接收的缓冲区大小,默认是 4,可以稍微改大一点,

1 #define ETH_RXBUFNB ((uint32_t)8U) /* 接收缓冲区 */
2 #define ETH_TXBUFNB ((uint32_t)8U) /* 发送缓冲区 */

对 LwIP 管理的内存肯定要分配的大一些, 而对于发送数据是存储在 ROM 或者静态存储区的时候,还要将 MEMP_NUM_PBUF 宏定义改的大一点, 当然发送缓冲区大小和发送缓冲区队列长度决定了发送速度的大小,根据不同需求进行配置,并且需要不断调试,而对于接收数据的配置,应该配置 TCP 缓冲队列中的报文段数量与 TCP 接收窗口大小,特别是接收窗口的大小,这直接可以影响数据的接收速度。

//内存堆 heap 大小
2 #define MEM_SIZE (25*1024)
3
4 /* memp 结构的 pbuf 数量,如果应用从 ROM 或者静态存储区发送大量数据时
5 这个值应该设置大一点 */
6 #define MEMP_NUM_PBUF 25
7
8 /* 最多同时在 TCP 缓冲队列中的报文段数量 */
9 #define MEMP_NUM_TCP_SEG 150
10
11 /* 内存池大小 */
12 #define PBUF_POOL_SIZE 65
13
14 /* 每个 pbuf 内存池大小 */
15 #define PBUF_POOL_BUFSIZE \
16
LWIP_MEM_ALIGN_SIZE(TCP_MSS+40+PBUF_LINK_ENCAPSULATION_HLEN+PBUF_LINK_HLEN)
17
18
19 /* 最大 TCP 报文段, TCP_MSS = (MTU - IP 报头大小 - TCP 报头大小 */
20 #define TCP_MSS (1500 - 40)
21
22 /* TCP 发送缓冲区大小(字节) */
23 #define TCP_SND_BUF (11*TCP_MSS)
24
25 /* TCP 发送缓冲区队列的最大长度 */
26 #define TCP_SND_QUEUELEN (8* TCP_SND_BUF/TCP_MSS)
27
28 /* TCP 接收窗口大小 */
29 #define TCP_WND (11*TCP_MSS)

当然,除此之外,想要整个 LwIP 能高速平稳运行,只配置这些是不够的,比如我们应该使用中断的方式接收数据,这就省去了 CPU 查询数据,而且,我们应该将内核邮箱的容量增大,这样子在接收到数据之后,投递给内核就不会因为无法投递而阻塞,同时内核线程的优先级应该设置得更高一点,这样子就能及时去处理这些数据,当然,我们也可以独立使用一个新的发送线程,这样子内核就无需调用底层网卡函数,它可以专心处理数据,发送数据的事情就交由发送线程去处理,同时,在处理数据的时候,不用使用串口打印信息,因为串口是一个很慢的外设,当然啦, 关于提高 LwIP 网络传输的速度,还有很多东西要优化的,这也跟使用环境有关系,不能一概而论,只是给出一些方向,具体怎么实现,还需要大家亲身实践去调试。

发布了23 篇原创文章 · 获赞 27 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/sinat_31039061/article/details/104667647