再谈WiFiEventTrigger——开启监听
1. WiFiEventTrigger WiFi发现事件触发
为什么要再谈WiFiEventTrigger呢?——因为它太重要了,在上一篇总结中只是泛泛而谈,总结了该函数在设备发现WiFi接入时被调用,通过coap协议实现了不同设备间的信息通讯。那么它是如何实现设备互联的呢?整个分布式软总线服务又是如何发布的呢?下面几篇文章将打破砂锅问到底,通过层层抽丝剥茧,从代码层面看看鸿蒙的技术人员如何实现了这个功能。我们先从WiFiEventTrigger函数开始。其各个部分的功能如图所示:
1.1 在哪里使用
首先我们需要知道WiFiEventTrigger在那里被使用
我们可以在discovery_service.c中的函数InitService()中可以找到它的调用:
可以看到在RegisterWifiCallback的参数列表中调用了WiFiEventTrigger,我们可以看到
将wifiEventTrigger绑定在g_wifiCallback中,然后在CoapWriteMsgQueue函数中定义了一个handler,里面的handler函数就是绑定了wifiEventTrigger的g_wifiCallback,最后调用WriteMsgQue将handler载入queueId相应的队列中处理相应任务
后续的博客会提到CoapWriteMsgQueue的作用,这里先不表
1.2 内部的函数作用
从上面的函数图中我们可以知道其中主要是四个函数:
- CoapGetIp:得到WiFiIp并存入localDev->deviceIp中
- BusManager:总线管理——最最重要的开启函数后面会重点介绍
- CoapRegisterDeviceInfo:将DeviceInfo中的信息更新到NSTACKX_LocalDeviceInfo中,再将deviceHash登记在g_localDeviceInfo中
- DoRegistService:获取capability和data并将其登记在g_capabilityData中
其中三个函数都只是进行数据的更新和存储,而BusManager中的startBus才是真正开启总线的关键函数
2. StartBus 总线开启
接下来我们重点研究StartBus函数——开启总线通信
函数的流程如下:
1. 调用GetCommonDeviceInfo获取当前设备信息
2. 然后是两个非常重要的指针函数赋值g_baseListener定义了在监听到不同的消息:连接请求和数据请求时调用的不同处理函数OnConnectEvent和OnDataEvent详细的步骤在分析StartListener函数中回提到
3. 调用StartListener对IP端口进行监听处理
4. 调用StartSession建立会话进行TCP通信
5. 调用AuthMngInit将authPort和sessionPort存入相应的全局变量中
其中最重要的两个start函数:一个开启了IP端口的监听,对连接和数据事件进行处理;另一个建立了session会话,通过StartSelectLoop循环接收会话中的两类请求:连接处理和数据处理。由于内部的机制比较复杂,所以该篇文章只重点介绍StartListener,在下一篇文章中再重点介绍StartSession
下面进入重头戏!
2.1 StartListener
这里在源码中有两个StartListener区别在于线程的创建框架不同:
在宏定义 #if defined(LITEOS_M) || defined(LITEOS_RISCV)下
使用osThreadNew来创建WaitProcess线程
在不含上述定义的StartListener中使用ThreadId AuthCreate创建WaitProcess线程
首先我们可以通过下面这张图简单的了解函数流程:
1. 初始化监听的listener
2. 创建一个WaitProcess线程,利用select函数进行监听控制,返回结果>0时时,调用PorcessAuthData()函数完成对建立的连接的数据的收发和处理,两个具体的处理函数是在结构体BaseListener中定义的两个函数一个是onConnectEvent(为新建立的设备建立AuthConnNode结点并插入双向链表中)一个是onDataEvent(对AuthConnNode节点中的数据进行处理包括头信息的解析和封装等)
3. 最后调用GetSockPort根据监听的标识符g_listenFd返回socketPort
该函数中有一个非常有趣的代码——signal(SIGPIPE, SIG_IGN)
我们在操作系统中学过使用信号量的方法来控制同步和互斥,解决死锁和饥饿。里面涉及的两个源于操作:wait和signal,我猜想这里的signal应该就是其中的唤醒原语操作。由于缺乏完整的wait和signal代码,所以这里不好分析其中SIGPIPE,SIG_IGN信号量的具体作用,可以看到操作系统中的基本原理在不断迭代更新的操作系统中仍然适用。
看向创建的线程的运行函数waitProcess
2.2 waitProcess线程处理
这里写了一个死循环while(1)来循环处理与监听
具体流程:
1. 每次给readSet和exceptfds赋当前的g_dataFd
2. 调用select监测标识符的变化,当返回值发生变化时根据变化调用ProcessAuthData进行用户数据处理,或者异常情况和退出时break该循环并stopListener——即重置g_listenFd和g_dataFd
这里有意思的是select多路复用的网络机制
fd_set是一组文件描述符(fd)的集合。由于fd_set类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量:
fd_set set;
FD_ZERO(&set); /* 将set清零 */
FD_SET(fd, &set); /* 将fd加入set */
FD_CLR(fd, &set); /* 将fd从set中清除 */
FD_ISSET(fd, &set); /* 判断fd是否处于可用状态 是为true */
具体的内容会单独发一篇来详细介绍select-accept一系列知识,这里暂且不表
所以整个线程的工作就是监视文件标识符的变动——对于可读的连接进行数据或连接的处理。
而具体的处理函数是下面要接着深入的ProcessAuthData
2.3 ProcessAuthData
首先贴上源码注释:
函数流程:
1. 判断监听端口是否在可读准备的集合中
2. 上述判断成立的情况则调用accept获取客户端的IP地址并返回一个g_dataFd
3. 如果该g_dataFd<0则调用CloseAuthSessionFd关闭当前会话连接表示连接error
4. 如果g_data>=0则更新最大fd并调用全局回调函数中的连接处理函数onConnectEvent进行连接的处理,返回0则失败清除会话连接并返回false
5. 在步骤1中判断失败或执行完onConnectEvent后判断当前g_dataFd是否大于零且在可读集合中,成立则表明有数据可读,则调用onDataEvent进行相关数据处理
然后是两个处理函数的讲解
3. g_callback 回调处理函数
3.1 onConnectEvent 连接处理
函数参数:int fd:标识符;const char *ip:目标的IP地址
调用ProcessConnectEvent执行具体的处理
3.1.1 ProcessConnectEvent
首先放上代码注释
比较简单,两个功能:结点的新建和上链
流程:
1. 通过fd调用FindAuthConnByFd从g_fdMap中找到对应的AuthConn——注意这里由于是新连接所以理论上来说在g_fdMap中应该不存在,存在则调用closeAuth将其从链上删除释放,函数return
可以从代码中看到AuthConn结构体的构成:
2. 不存在该连接则新建立一个AuthConn结构体将传入的fd和IP作为其属性
3. 调用AddAuthConnToList将该新建结点上链g_fdMap——在链表的尾部插入该结点
然后是数据的处理函数
3.2 onDataEvent 数据处理
函数参数仅需要一个fd标识符即可
具体的函数处理在ProcessDataEvent中进行
/*
函数功能:通过fd进行数据的获取和存储
函数参数:fd:设备标识符
函数返回:void
*/
void ProcessDataEvent(int fd)
{
SOFTBUS_PRINT("[AUTH] ProcessDataEvent fd = %d\n", fd);
AuthConn *conn = FindAuthConnByFd(fd);
if (conn == NULL) {
SOFTBUS_PRINT("ProcessDataEvent get authConn fail\n");
return;
}
//当所指的buf为空则进行初始化
if (conn->db.buf == NULL) {
conn->db.buf = (char *)malloc(DEFAULT_BUF_SIZE);
if (conn->db.buf == NULL) {
return;
}
(void)memset_s(conn->db.buf, DEFAULT_BUF_SIZE, 0, DEFAULT_BUF_SIZE);
conn->db.size = DEFAULT_BUF_SIZE;
conn->db.used = 0;
}
//获取db中的数据属性
DataBuffer *db = &conn->db;
char *buf = db->buf;
int used = db->used;
int size = db->size;
//得到buf数据
int rc = AuthConnRecv(fd, buf, used, size - used, 0);
if (rc == 0) {
return;
} else if (rc < 0) {
CloseConn(conn);
return;
}
//将头部信息封装入buf中并进行数据的操作
used += rc;
int processed = ProcessPackets(conn, buf, size, used);
if (processed > 0) {
used -= processed;
if (used != 0) {
if (memmove_s(buf, processed, buf, used) != EOK) {
CloseConn(conn);
return;
}
}
} else if (processed < 0) {
CloseConn(conn);
return;
}
//保存使用了的空间大小
db->used = used;
SOFTBUS_PRINT("[AUTH] ProcessDataEvent ok\n");
}
数据处理的函数比较长,我们分为几个部分来讲解
3.2.1 接收准备
主要是这部分的代码
主要是数据的接收准备工作
流程:
1. 调用FindAuthConnByFd从g_fdMap中找到fd对应的AuthConn结点——此时应该是可以找到的,原本没有在刚刚的if判断中已经通过onConnectEvent事件新建一个结点
2. 当其中的数据部分也就是上面展示的结构体DataBuffer为NULL时为其初始化分配空间
3. 若已分配好空间则提取相关属性参数供接收使用
3.2.2 数据接收
主要是这块代码:
调用函数AuthConnRecv
实际上是封装一层参数检查后调用tcp_socket.c中的TcpRecvData函数
通过给其传入参数
fd:设备标识符;buf:接收数据的缓存区;len:缓存区大小;timeout:接收的时限
调用TcpRecvMessages中的recv原语操作来完成对数据的接收
3.2.3 接收数据的处理部分
这块才是数据处理函数的重头戏,也是代码的最后一部分
从外部结构来看,ProcessPackets函数返回了其处理的数据字段大小processed,如果其小与零则内部处理失败调用CloseConn删库跑路,如果大于0则于used做差,不等于0说明并没有处理完所有的数据,则调用memmove_s进行数据的覆盖return;最后保存使用了的空间大小内部打印并优雅return
所以能够接收的情况智能是处理的数据和接收的数据量一致的情况
接下来我们深入函数ProcessPackets看看它做了些啥
3.3 ProcessPackets 实际的数据处理函数
里面又分为两个重要的处理函数,一个用于解析接收的TCP信息,解析出头部信息和消息主题;另一个用于封装另一个packet根据通讯的模式发送报文回应该消息
我们重点关注其中的两个函数:ParsePacketHead和OnDataReceived
3.3.1 ParsePacketHead
跟我们在计算机网络中所学的TCP报文一样,一个packet会分为头部——装载每个报文的类型和固有属性和主题——真正的数据内容,所以我们需要正确解析它们
该函数主要是结合函数GetIntFromBuf和GetLongFromBuf以及offset偏移量在buf中按在COAP协议中规定好的顺序,解析报文,从代码的最后几行我们可以看到头部主要包含以下信息:
module:在下面的数据处理中可以看到module的具体类型,用于不同情况下的数据处理
seq:序列号,类似TCP协议中的序列号和确认号,用于对于报文的顺序和是否送达进行排序和确认的标志
flags:类似TCP中的flags标识报文的状态
dataLen:主体的数据大小
最终返回packet结构体
3.3.2 OnDataReceived
由于篇幅原因再下一篇中展开讲解!