从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)

从零实现 LWIP 一(配置过程)

从零实现 LWIP 二(UDP、无DHCP)

从零实现 LWIP 三(TCP客户端服务端、静态IP)

一、前言:

我回来继续搞lwip了,DHCP这块之前在F429上调通了,但是并没有实战应用。此次在F407上实现,底层驱动差不太多。

代码部分尽量不给大家密密麻麻的粘贴复制,主要介绍整体实现的思维。

二、成果:

1.实现了设备DHCP功能

2.在DHCP失败的时候使用静态IP建立通讯,而且考虑到了大量设备DHCP都失败时,用一种算法得到不同的静态IP建立通讯。

3.实现了网线的热插拔,网线断开关闭TCP,网线接上重新尝试DHCP和建立TCP连接

4.server方断开连接后能够检测到,并且当server重新打开后,能主动重新建立连接

5.UDP就更简单了,不需要保持连接

6.封装了用户层,使得用户可以选择UDP 还是 TCP client 还是 TCP server来选择配置,并且发送和接收数据都是相同方法,极大便捷了用户使用LWIP

平台是STM32F407ZG + 外部PHY DP83848 + UCOSIII

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第1张图片

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第2张图片

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第3张图片

三、实现最基本功能的过程

从下图可以看出来,用二级路由器当作交换机将我的电脑与设备建立通讯。我电脑作为tcp server 设备作为tcp client。

都开启DHCP,路由器也要确认开启了DHCP,可以理解为我的电脑和设备是作为DHCP从机去找路由器的DHCP主机申请动态IP的。

1.连接方式如下:

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第4张图片

2.然后需要确定电脑是否开启了DHCP,其实这个步骤的目的是看路由器在哪个网段内,通过配置路由器也可以得知,所以得知网段后,设置计算机为静态IP也是可以的,但是一定要在同一个网段内。我是为了方便而使用计算机的DHCP的。

打开计算机控制面板的网络适配器管理,找到以太网,右键进入属性

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第5张图片

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第6张图片

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第7张图片

3.确定申请到的IP地址

打开cmd,输入ipconfig,找到申请到的IP

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第8张图片

4.打开网络调试助手,建立tcp server(一定要关闭计算机的防火墙)

选择协议类型为tcp server

本地IP地址就设置为我获取到的192.168.0.158

 端口号可以自行设置 我设置为5014

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第9张图片

 

5.设备端初始化过程

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第10张图片

DHCP成功的要素:

  1. 网线得有效接通(是废话但是很重要)
  2. 路由器或交换机打开了DHCP功能
  3. 设备端LWIP底层驱动使能了DHCP服务

如何解决DHCP失败:

  1. 我就遇到一个情况,一个路由器打开了DHCP也恢复默认配置了很多遍,电脑可以DHCP成功但是设备就是不行。换一个牌子的路由器,就可以。我至今也没弄明白,肯定是我配置路由器的问题,但我找不到。
  2. 此外,我是在初始化的时候DHCP的,但是LWIP处理函数的任务还没启动,那就需要DHCP的循环中调用LWIP处理函数去处理DHCP。那么这块会引出一个问题,DHCP如果不顺利 最多需要4秒的话,我在任务调度前开始的话无疑会影响屏幕的启动。就会出现开机后隔了四秒屏幕才能正常使用。所以我将这部分初始化放在了任务中while(1)前去执行,并且延时函数采用操作系统延时,这样就能有空去处理屏幕的任务了,就不影响屏幕的使用了。

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第11张图片

3.建立一个任务,循环执行LWIP处理函数

这个LWIP处理函数是核心的LWIP部分,这里是真正执行DHCP、建立连接、收发数据等功能。

到目前为止,一个简单的具有DHCP服务的TCP client就创建好了,TCP client的实现可以看我的上一篇文章LWIP三

四、MAC地址和DHCP的关系

DHCP是很重要的,而且要知道为什么一个设备每次DHCP下来的IP地址都一样呢?这跟MAC地址有关系,路由器或者交换机会建立一种链表去记录MAC地址和对应的之前分配过的IP地址。所以大家可以试试,修改下设备的MAC地址,再重新申请DHCP,IP就变了。

所以,都是同样的程序,我怎么让每台设备的MAC地址不一样?

1.给随机值行不行?在每台设备一上电,随机生成MAC地址,然后存储下来。以后就用这个MAC地址了。

用这样的方式,两台设备连在同一个路由器上,运气好,MAC地址不一样,能行得通。运气不好,两台随机生成的MAC地址有可能一样,

根据墨菲定律,有可能发生的事就一定会发生。那么两台设备,先接上路由器的设备DHCP成功,后接上的会失败。

2.STM32的芯片会有一个独一无二的32位 CPU ID。

我们可以根据这个ID 来换算MAC地址。这样每台设备的MAC地址就是独一无二的了。

五、如何封装用户层,实现UDP或TCP发送数据、收取数据、

我们首先需要了解LWIP的结构

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第12张图片

 

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第13张图片

 

从上图可以看到,UDP 和 TCP 除了transport layer不一样外,其他层都一样,所以我们可以结合之前文章介绍的UDP和TCP的用法在用户层对他们进行统一的封装,这样,在用户使用的过程中比如要发送或者处理接收的数据感受不到UDP还是TCP,使用方法都一样,就很方便。

在用户层定义一个结构体,我们通过这个结构体作为媒介,串联协议层和用户层。

#define RX_LEN 200
typedef err_t (*App_Lwip_send)(char *pData, uint16_t u16Len);

typedef struct
{
	uint8_t u8TransformType;			//传输类型是何种
	struct udp_pcb *upcb;				//UDP控制块
	struct tcp_pcb *tpcb;				//TCP控制块
	ip_addr_t RemoteIp;				//目标IP
	uint16_t u16RemotePort;				//目标端口号
	uint16_t u16LocalPort;				//本地端口号
	uint8_t u8Connected;				//已连接标志
	uint8_t Received;				//接收到数据包标志
	uint8_t Sending;				//要发送数据标志
	uint8_t RxBuff[RX_LEN]; 			//接收缓冲区
	void    *pTxBuff;				//发送缓冲区地址
	uint16_t RxLen;					//接收长度
	uint16_t TxLen;					//发送长度
	App_Lwip_send  SendData;			//发送函数指针
}App_Lwip_type;
extern App_Lwip_type hAppLwip;

举例子来说明这个数组的用法

1.标记使用何种传输协议

#define USE_UDP_CLIENT  0
#define USE_UDP_SERVER  1
#define USE_TCP_CLIENT  2
#define USE_TCP_SERVER  3

hAppLwip.u8TransformType 用来记录使用何种传输协议

这就是用户层对协议层的选择,我们在使用时会根据不同的传输协议使用方法也不同,比如UDP不需要保持连接,而TCP是需要保持连接的。

2.控制块

我们需要知道,在传输层的数据流控制是通过控制块来实现的。

UDP的控制块会记录下一个控制块地址、目标地址IP、端口号、本地IP、端口号、接收回调函数指针、入口参数等。

TCP控制块类似UDP,但更复杂,因为TCP还要保持连接。

所以我们可以在初始化UDP或TCP控制块后,记录下这些控制块的地址,然后在应用层可以读取这些控制块的信息。

3.接收数据

我们首先需要知道LWIP在使用前会申请一块内存用作LWIP的内存池。无论是UDP还是TCP在接收到数据后都会进入回调函数中,最后会释放掉缓冲区数据,以备重新准备接收新的数据。

但是问题是,我们对接收的数据需要进行处理,有可能会执行的时间比较长,而我们也知道在回调函数中最好不要做时间很长的操作。另外也要考虑到架构的清晰,LWIP的协议层不要和其他功能的应用层有联系。那么就需要把接收到的数据提取出来,然后释放掉缓冲区(在协议层)。随后在任务中对刚接收到的数据进行处理(在应用层)。

所以我在结构体中定义了一个长度为200的数组,用来在接收回调函数中保存接收的数据,并且记录下接收的数据大小。并且标记有需要处理的数据。即

uint8_t Received;			//接收到数据包标志
uint8_t RxBuff[RX_LEN];			//接收缓冲区
uint16_t RxLen;				//接收长度

然后在LWIP应用层,我是建立了一个app_lwip.c,其中一个void AppLwip_process(void)函数会被任务不断调用。

那么在这个函数中不断判断,是否有需要处理的数据,若有则进行相应处理。那这部分就是应用层的事儿了,大家想怎么处理就怎么处理。

4.发送数据

typedef err_t (*App_Lwip_send)(char *pData, uint16_t u16Len);

uint8_t Sending;			//要发送数据标志
void    *pTxBuff;			//发送缓冲区地址
uint16_t TxLen;				//发送长度
App_Lwip_send  SendData;		//发送函数指针

 

我们在使用UDP、TCP发送数据时,其实道理都是一样的,我给他们的服务函数一个缓冲区指针、长度,让他们去发送掉就好了啊。

因为是从应用层向协议层调用,所以不用像接收数据那样定义一个数组,把指针拿过来即可。

但是我们会有UDP client、server TCP client、server,他们的发送函数都不同,但是结构上可以相同,那么我们就定义一个函数类型和函数指针。

举个例子,比如我们打算用UDP client,那么

hAppLwip.SendData = udp_client_send;

这样在发送时,调用hAppLwip.SendData,在使用TCP client也是同样的道理,用户使用就会很方便了。

5.连接标志

uint8_t u8Connected;			//已连接标志

我们知道TCP需要时刻保持连接,那么当连接中断后,我们需要知道,并且尝试重新连接。不然会发生什么呢?

设备一旦断开就连不上,那就糟糕了,因为这是和外界的通讯手段,一个设备在现场断开连接,跟关机没什么分别。所以一定要保证设备能够得知连接断开,并且可以重新连接上。

这就引出下一个重要内容

六、网线热插拔和自动重新连接

1.那都有哪些情况会导致连接断开呢?(UDP不考虑)

  1. 网线断开
  2. TCP服务端方面断开连接
  3. TCP客户端方面断开连接

首先我们需要知道,网线断开一定会导致TCP连接断开,而且网线断开也就没必要再去尝试TCP连接了。

但是TCP连接断开不一定是由网线断开导致的,所以当网线在连接状态,而TCP断开时我们应该尝试重新建立TCP连接。

2.网线断开和连接如何检测到呢?

网上找了很多答案,大部分是检测外部PHY的寄存器的某个值,但是代码都是标准库的,而我们现在用的HAL库。而且在cubemx生成的以太网驱动中,已经做好了检测网线状态的函数。

void ethernetif_set_link(struct netif *netif)

这个函数我们只需要在任务中循环调用查看即可。

3.如何得知TCP连接和断开呢?

回顾下TCP client的初始化

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第14张图片

那么TCP断开或连接都会进入连接回调函数(这个函数需要按照格式自己定义)中。

若连接上了,就标记hAppLwip.u8Connected为1,并且注册接收函数(当有数据接收到会进入该接收回调函数中,我们前面说过,在这个接收回调函数中搬运数据到hAppLwip.RxBuff[RX_LEN]中)

否则就为0并且还要删除TCP控制块,并且释放空间

4.如何得知TCP连接状态

TCP连接上,不代表连接正常。事实上,TCP连接状态有很多种

enum tcp_state {
  CLOSED      = 0,
  LISTEN      = 1,
  SYN_SENT    = 2,
  SYN_RCVD    = 3,
  ESTABLISHED = 4,
  FIN_WAIT_1  = 5,
  FIN_WAIT_2  = 6,
  CLOSE_WAIT  = 7,
 CLOSING     = 8,
 LAST_ACK    = 9,
 TIME_WAIT   = 10
};

我们可以不全面了解这些状态,但是要知道正常情况hAppLwip.tpcb->state应该为ESTABLISHED,因为我们使用的时候没有需要关闭TCP的时候,所以其他状态都是异常的。

5.如何实现自动连接呢

重新连接的核心就是TCP初始化,但是为什么还需要DHCP呢?

大家想一下,断开连接有可能是网线断了换了路由器,或者路由器重新设置了网段,网段就有更换了的可能性。

因此DHCP重新申请动态IP就极有必要了。

从零实现 LWIP 四(一对一 UDP、TCP客户端 DHCP 网线热插拔和自动重新连接)_第15张图片

七、总结:

1.这篇文章的重点我觉得是对UDP、TCP控制块的理解,他的应用是一种思想,我把这个思想应用到了应用层,其实就是通过一个结构体来综合控制以及信息获取。这样虽然没有做到尽量高内聚低耦合,但是他极大的减少了文件之间的接口函数,也减少了很多缓冲区到缓冲区间的移动。也节约了大量的CPU资源。

2.在应用层封装一层,尽量让用户不去考虑UDP、TCP的区别,最好是让用户能够像使用串口一样方便去使用UDP、TCP。目的就是让不懂以太网的用户能使用起来。

3.如何能随意的插拔网线,被断开连接,还能自动尝试建立连接。像计算机一样,

标题上就说到,目前做到的仅仅是一对一的情况,比如设备只和一个server通讯,可以参考我的这篇文章。

但若我的设备数据要发向多个server呢?就要增加一些逻辑了。

其实就是对控制块链表的管理,新创建的连接如何增加进链表中,断开连接的控制块又如何从链表中删除,而且前后串联在一起。

如何对一个控制块链表中有效的控制块进行管理,比如说我就要往某一个server发送数据,怎么办?

我从某一个server接收到了数据,如何进行对应的处理?

 

如果有错误的地方,请您指正~

如果有好的想法,欢迎补充~

你可能感兴趣的:(嵌入式编程,网络)