从零实现 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
从下图可以看出来,用二级路由器当作交换机将我的电脑与设备建立通讯。我电脑作为tcp server 设备作为tcp client。
都开启DHCP,路由器也要确认开启了DHCP,可以理解为我的电脑和设备是作为DHCP从机去找路由器的DHCP主机申请动态IP的。
1.连接方式如下:
2.然后需要确定电脑是否开启了DHCP,其实这个步骤的目的是看路由器在哪个网段内,通过配置路由器也可以得知,所以得知网段后,设置计算机为静态IP也是可以的,但是一定要在同一个网段内。我是为了方便而使用计算机的DHCP的。
打开计算机控制面板的网络适配器管理,找到以太网,右键进入属性
3.确定申请到的IP地址
打开cmd,输入ipconfig,找到申请到的IP
4.打开网络调试助手,建立tcp server(一定要关闭计算机的防火墙)
选择协议类型为tcp server
本地IP地址就设置为我获取到的192.168.0.158
端口号可以自行设置 我设置为5014
5.设备端初始化过程
3.建立一个任务,循环执行LWIP处理函数
这个LWIP处理函数是核心的LWIP部分,这里是真正执行DHCP、建立连接、收发数据等功能。
到目前为止,一个简单的具有DHCP服务的TCP client就创建好了,TCP client的实现可以看我的上一篇文章LWIP三
DHCP是很重要的,而且要知道为什么一个设备每次DHCP下来的IP地址都一样呢?这跟MAC地址有关系,路由器或者交换机会建立一种链表去记录MAC地址和对应的之前分配过的IP地址。所以大家可以试试,修改下设备的MAC地址,再重新申请DHCP,IP就变了。
1.给随机值行不行?在每台设备一上电,随机生成MAC地址,然后存储下来。以后就用这个MAC地址了。
用这样的方式,两台设备连在同一个路由器上,运气好,MAC地址不一样,能行得通。运气不好,两台随机生成的MAC地址有可能一样,
根据墨菲定律,有可能发生的事就一定会发生。那么两台设备,先接上路由器的设备DHCP成功,后接上的会失败。
2.STM32的芯片会有一个独一无二的32位 CPU ID。
我们可以根据这个ID 来换算MAC地址。这样每台设备的MAC地址就是独一无二的了。
我们首先需要了解LWIP的结构
从上图可以看到,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;
#define USE_UDP_CLIENT 0
#define USE_UDP_SERVER 1
#define USE_TCP_CLIENT 2
#define USE_TCP_SERVER 3
hAppLwip.u8TransformType 用来记录使用何种传输协议
这就是用户层对协议层的选择,我们在使用时会根据不同的传输协议使用方法也不同,比如UDP不需要保持连接,而TCP是需要保持连接的。
我们需要知道,在传输层的数据流控制是通过控制块来实现的。
UDP的控制块会记录下一个控制块地址、目标地址IP、端口号、本地IP、端口号、接收回调函数指针、入口参数等。
TCP控制块类似UDP,但更复杂,因为TCP还要保持连接。
所以我们可以在初始化UDP或TCP控制块后,记录下这些控制块的地址,然后在应用层可以读取这些控制块的信息。
我们首先需要知道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)函数会被任务不断调用。
那么在这个函数中不断判断,是否有需要处理的数据,若有则进行相应处理。那这部分就是应用层的事儿了,大家想怎么处理就怎么处理。
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也是同样的道理,用户使用就会很方便了。
uint8_t u8Connected; //已连接标志
我们知道TCP需要时刻保持连接,那么当连接中断后,我们需要知道,并且尝试重新连接。不然会发生什么呢?
设备一旦断开就连不上,那就糟糕了,因为这是和外界的通讯手段,一个设备在现场断开连接,跟关机没什么分别。所以一定要保证设备能够得知连接断开,并且可以重新连接上。
这就引出下一个重要内容
首先我们需要知道,网线断开一定会导致TCP连接断开,而且网线断开也就没必要再去尝试TCP连接了。
但是TCP连接断开不一定是由网线断开导致的,所以当网线在连接状态,而TCP断开时我们应该尝试重新建立TCP连接。
网上找了很多答案,大部分是检测外部PHY的寄存器的某个值,但是代码都是标准库的,而我们现在用的HAL库。而且在cubemx生成的以太网驱动中,已经做好了检测网线状态的函数。
void ethernetif_set_link(struct netif *netif)
这个函数我们只需要在任务中循环调用查看即可。
回顾下TCP client的初始化
那么TCP断开或连接都会进入连接回调函数(这个函数需要按照格式自己定义)中。
若连接上了,就标记hAppLwip.u8Connected为1,并且注册接收函数(当有数据接收到会进入该接收回调函数中,我们前面说过,在这个接收回调函数中搬运数据到hAppLwip.RxBuff[RX_LEN]中)
否则就为0并且还要删除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的时候,所以其他状态都是异常的。
重新连接的核心就是TCP初始化,但是为什么还需要DHCP呢?
大家想一下,断开连接有可能是网线断了换了路由器,或者路由器重新设置了网段,网段就有更换了的可能性。
因此DHCP重新申请动态IP就极有必要了。
1.这篇文章的重点我觉得是对UDP、TCP控制块的理解,他的应用是一种思想,我把这个思想应用到了应用层,其实就是通过一个结构体来综合控制以及信息获取。这样虽然没有做到尽量高内聚低耦合,但是他极大的减少了文件之间的接口函数,也减少了很多缓冲区到缓冲区间的移动。也节约了大量的CPU资源。
2.在应用层封装一层,尽量让用户不去考虑UDP、TCP的区别,最好是让用户能够像使用串口一样方便去使用UDP、TCP。目的就是让不懂以太网的用户能使用起来。
3.如何能随意的插拔网线,被断开连接,还能自动尝试建立连接。像计算机一样,
标题上就说到,目前做到的仅仅是一对一的情况,比如设备只和一个server通讯,可以参考我的这篇文章。
但若我的设备数据要发向多个server呢?就要增加一些逻辑了。
其实就是对控制块链表的管理,新创建的连接如何增加进链表中,断开连接的控制块又如何从链表中删除,而且前后串联在一起。
如何对一个控制块链表中有效的控制块进行管理,比如说我就要往某一个server发送数据,怎么办?
我从某一个server接收到了数据,如何进行对应的处理?
如果有错误的地方,请您指正~
如果有好的想法,欢迎补充~