WiFi事件处理——基于TCP的认证通信机制


这部分的函数的调度跨度大、调用深度大,想要详细的解读可以参考前面发布的几篇再谈WiFiEventTrigger,这里对其中关键的代码和方法进行凝练和概括,从整体出发,从细节把握整个WiFi触发的事件处理函数,由于自身所掌握的知识尚浅,所以有错误和不足的地方,还请读者批评指正!

1. TCP流程(select的用法)

在这里插入图片描述
首先我们必须熟悉TCP的典型流程,这里简单对其过程进行一个简单概述:

socket():用于建立socket连接创建套接字,建立连接后可对sockaddr等结构进行初始化
bind():将socket套接字与本地IP地址和端口号绑定,一般用于服务端较多
listen():调用listen函数来创建一个等待队列——在鸿蒙中就是创建waitProcess线程,在其中处理连接请求和数据处理请求
accept():在创建等待队列后,调用accept()函数等待并接收客户端的连接请求,通常从由listen()所创建的等待队列中取出第一个未处理的连接请求进行处理
connect():客户端与服务端进行连接的函数
send()recv():数据的收发函数

而在后面要讲述的鸿蒙源码中我们看到在listen之后创建的线程处理函数waitProcess中先调用了select再在其数据处理函数ProcessAuthData中调用了accept

select的引入是为了解决CS模型中accept的阻塞问题——CS架构中accept会持续阻塞直到有客户端socket来进行连接,而引入select能够解决其一直等待的问题。

其工作原理:
1. 所有的socket存储在一个集合中(fd_set)中
2. 通过select遍历所有的socket
3. 监测到变化相应的socket,将其取出放入另外的集合中(readfds,writefds,exceptfds)中
4. 对相应的集合中的socket连接进行集中处理
5. 对于服务端就是调用accept、对于客户端就是调用recv或send

优点:防止accept阻塞,使用一种有序的方式,对多个套接字进行统一的管理与调度
缺点:原理是遍历监测,当socket较多时,效率较低,开销较大,适用于小型网络中

这里稍微梳理了一下TCP模型,是为了更好的讲解后面的总线开启——会话开启和监听开启——分别是三次握手的可信通道建立和实际鸿蒙系统中TCP通信的两大过程,在两个不同的线程中进行处理

正篇开启!

2. wifi事件处理(WifiEventTrigger)

我们可以在discovery_service.c中的函数InitService()中可以找到它的调用:

可以看到在RegisterWifiCallback的参数列表中调用了WiFiEventTrigger,我们可以看到
在这里插入图片描述
将wifiEventTrigger绑定在g_wifiCallback中,然后在CoapWriteMsgQueue函数中定义了一个handler,里面的handler函数就是绑定了wifiEventTrigger的g_wifiCallback,最后调用WriteMsgQue将handler载入queueId相应的队列中处理相应任务
在这里插入图片描述
WiFiEventTrigger函数在从WiFi消息队列中读取信息入线程中处理时通过handler调用,也就是说具体的WiFi接入触发事件的实际处理就是由它实现

函数主要封装了四个功能函数实现了三大功能:得到本地设备的IP并保存——开启总线——初始化信息的本地保存
在这里插入图片描述
然后我们从关键核心BusManager入手:
在这里插入图片描述
通过传入的startFlag不同分别调用开启总线和停止总线工作。而开启总线最重要的就是两个开启函数:startSession和startListener创建两个线程进行后续的处理,其共用的监听标识符g_listenFd和g_dataFd在不同的场景下分别在不同的线程中使用。

  • startSession:会话开启的处理。主要处理TCP可信通道连接的三次握手信息的解析和发送。作为服务端,需要进行两次不同的消息处理:一次需要回复、一次只需要接受解析即可
  • startListener:会话监听的处理。当select到可读的通道时,与客户端进行应答。这里主要时处理WiFi相关的信息和数据。

3. 会话开启(startSession)

其主函数为CreateTcpSessionMgr

CreateTcpSessionMgr——套接字的创建

首先贴上注释方便读者阅读

/*
函数功能:创建TCP会话管理器并开启监听和处理
函数参数:asServer:是否为服务端;localIp:本地IP(其实是startListen中得到的用户设备IP)
函数返回:int sessionIp会话IP
*/
int CreateTcpSessionMgr(bool asServer, const char* localIp)
{
    if (localIp == NULL) {//检查传入的ip参数是否为空
        return TRANS_FAILED;
    }
    if (InitTcpMgrLock() != 0 || GetTcpMgrLock() != 0) {
        return TRANS_FAILED;
        //初始化mutex相关锁和互斥体。申请互斥锁
    }
    //初始化g_sessionMgr(如果不存在的话)
    int ret = InitGSessionMgr();
    if (ReleaseTcpMgrLock() != 0 || ret != 0) {
        FreeSessionMgr();
        return TRANS_FAILED;
    }
    g_sessionMgr->asServer = asServer;//true/false表明是否作为服务端
    int listenFd = OpenTcpServer(localIp, DEFAULT_TRANS_PORT);//根据端口和ip打开服务器,并将套接字写进listenfd
    if (listenFd < 0) {
        SOFTBUS_PRINT("[TRANS] CreateTcpSessionMgr OpenTcpServer fail\n");
        //检查服务器是否打开成功
        FreeSessionMgr();
        return TRANS_FAILED;
    }
    int rc = listen(listenFd, LISTEN_BACKLOG);//根据打开的tcp服务器进行监听
    if (rc != 0) {
        SOFTBUS_PRINT("[TRANS] CreateTcpSessionMgr listen fail\n");
        CloseSession(listenFd);
        FreeSessionMgr();
        return TRANS_FAILED;
    }
    g_sessionMgr->listenFd = listenFd;//将服务器的套接字写进g_sessionMgr->listenFd
    signal(SIGPIPE, SIG_IGN);//原语操作唤醒 SIGPIPE,SIG_IGN为忽视信号函数
    if (StartSelectLoop(g_sessionMgr) != 0) {//创建线程开启循环处理消息
        SOFTBUS_PRINT("[TRANS] CreateTcpSessionMgr StartSelectLoop fail\n");
        CloseSession(listenFd);
        FreeSessionMgr();
        return TRANS_FAILED;
    }
    return GetSockPort(listenFd);
}

可以将其拆分为三个部分来理解:
开启TCP套接字的准备工作——建立socket并bind——调用StartSelectLoop创建线程进行消息处理

第一部分:准备工作就是InitGSessionMgr——g_sessionMgr并将传入的参数asServer赋给它告知是否作为服务端

第二部分:非常重要的socket创建函数OpenTcpServer,由它来创建套接字开启服务——传入的参数IP实际上是上面startListen中得到的用户IP,服务开启后调用listen对端口进行监听

第三部分:StartSelectLoop——与StartListener类似,同样创建了一个线程SelectSessionLoop用于会话中消息的循环处理

限于篇幅的原因,这里仅贴出重要的源码,其余的源码注释请移步之前的文章参考

下面我们重点看两个函数OpenTcpServer和StartSelectLoop

3.1 OpenTcpServer——创建TCP服务端

这里完成了TCP服务开启的前两个过程:socket创建和bind绑定
在这里插入图片描述
函数流程:
1. 首先将IP使用inet_pton进行一次转换并与AF_INET一并存入sockaddr_in中
2. 调用socket创建一个套接字,有三个参数分别代表了地址族,传输类型,传输协议
3. 调用bind将用户IP绑定在创建的socket上并得到fd返回


再调用StartSelectLoop,首先调用了listen对用户端进行监听,接收到客户端的连接请求则继续往下执行

3.2 StartSelectLoop——创建线程

与StartListener类似,但是这里需要区分的是线程的创建函数略有不同在StartListener中使用的是AuthCreate,而这里使用的是TcpCreate,具体的细节这里略去不表。重点是后面的线程running函数SelectSessionLoop:

3.3 SelectSessionLoop——线程处理函数

在这里插入图片描述
形式上和waitProcess(startListener中的线程创建函数)几乎一致:都是先通过select机制从fds集合中寻找做好请求准备的fds再调用ProcessData函数进行进一步处理。不过这里多了一个初始化函数:InitSelectList——每次请求前会在g_sessionMgr将所有符合可读或异常的fds添加到对应的fd_SET中


具体的数据处理函数

3.4 ProcessData——数据处理

数据的具体处理函数同样分为了连接和数据处理:
1.当监测到请求的用户为当前监听的端口则可判断是要将TCP连接建立的报文则调用ProcessConnection进行连接处理;
2. 当监测到是其他的请求用户,则可判断是套接字已经完成的客户端的请求,即为数据交换报文,则调用数据处理函数

这里可以和startListener进行一个差异比较:
相同:都是通过检查是否为当前监听的端口来判断是否调用连接处理函数
差异:而对于数据的处理,startListener是通过g_dataFds进行全局的可读数据fd进行管理;而startSession通过tsm和readfds来管理这样的可读数据fd

3.4.1 ProcessConnection
代码段较长这里先贴出注释

/*
函数功能:在服务端保存一个已建立连接的会话信息(在tsm中添加一个新的session)
函数参数:tsm:管理所有TCP连接的管理实例
函数返回:void
*/
static void ProcessConnection(TcpSessionMgr *tsm)
{
    struct sockaddr_in addr = { 0 };
    socklen_t addrLen = sizeof(addr);
    //等待客户端的connect请求
    int cfd = accept(tsm->listenFd, (struct sockaddr *)&addr, &addrLen);
    //accept函数指定服务端去接受客户端的连接,接收后,返回了客户端套接字的标识,且获得了客户端套接字的“地方”
    if (cfd < 0) {
        SOFTBUS_PRINT("[TRANS] ProcessConnection accept fail\n");
        return;
    }
    //初始化TcpSession结构体
    TcpSession *session = CreateTcpSession();
    if (session == NULL) {
        SOFTBUS_PRINT("[TRANS] ProcessConnection CreateTcpSession fail, fd = %d\n", cfd);
        CloseSession(cfd);
        return;
    }
    //获得用户连接实例
    AuthConn* authConn = GetOnLineAuthConnByIp(inet_ntoa(addr.sin_addr));
    if (authConn != NULL && strncpy_s(session->deviceId, MAX_DEV_ID_LEN, authConn->deviceId,
        strlen(authConn->deviceId)) != 0) {
        //用来将用户连接的设备id复制到会话的设备id
        SOFTBUS_PRINT("[TRANS] Error on copy deviceId of session.");
        free(session);
        CloseSession(cfd);
        return;
    }
    //得到的cfd存入session中
    session->fd = cfd;
    int result = AddSession(tsm, session);
    //将session添加进tsm会话
    if (result == false) {
        SOFTBUS_PRINT("[TRANS] AddSession fail\n");
        free(session);
        CloseSession(cfd);
        return;
    }//检查是否添加成功
    return;
}

函数流程:
1. 按照一般的TCP流程,在bind端口后,就应该accept客户端的connect了,所以函数的一开始就是accept函数等待客户端的连接请求并得到客户端给出的cfd

2. 然后就是在服务端这边注册这个连接——也就是调用CreateTcpSession初始化tcpsession结构体
在这里插入图片描述
这边是初始化的默认值:

sessionName:softbus_Lite_unknown
deviceId:0
groupId:0
sessionKey:0
seqNum:0
fd:-1
busVersion:0
routeType:0
isAccepted:false
seqNumList:malloc(sizeof(List))

3. 从g_fdMap中取出IP对应的authConn得到其中的deviceId存入刚刚创建的session->deviceId中、将accept得到的cfd存入session->fd中

4. 调用AddSession将session添加入tsm中

完成上述的过程则一个新的连接在服务端成功建立,当监测到该客户端可读时,就能够调用ProcessSesssionData进行数据通信了


3.4.2 ProcessSesssionData
在这里插入图片描述
这里使用了一个for循环来比对tsm->sessionMap_中存在的fd和是否在可读rfds中,存在则说明该客户端与服务端有连接且有数据需要处理则调用OnProcessDataAvailable进行数据处理

解决了一对多的TCP通信问题


3.4.3 OnProcessDataAvailable

这里又根据sessionName的不同分了两类处理:

这里通过不同的sessionName来区分两类数据的处理,当sessionName为softbus_Lite_unknown——也就是初始化session时设置的sessionName是调用HandleRequestMsg;当不是时则先通过GetSessionListenerByName获取会话的监听器然后调用TcpSessionRecv进行数据的接收并通过listener->onBytesReceived来通知服务端数据已接收

这里两个处理方式最最重要的区别就是调用HandleRequestMsg时调用ResponseToClient进行send而调用第二个过程时只接收消息而不再发送reply给客户端

下面就HandleRequestMsg展开:


3.4.4 HandleRequestMsg

该部分代码比较长
可以把它拆成三部分:报文头部的接收和解析——数据主体的接收和转换存储——相应客户端回应的报文封装和发送

第一部分:报文头部的接收和解析
在这里插入图片描述
首先调用TcpRecvData获取头部数据——实则时调用TCP的recv函数进行报文的接收。这里可以看到规定了接收的字节数为报文头部的长度,后面也做了检查如何返回的size不等于头部的规定的大小,则返回false表示报文不可信;否则调用GetIntFromBuf取一个int长度的数据检查是否为报文头部的认证部分:PKG_HEADER_IDENTIFIER,是则确信该报文有效

第二部分:数据主体部分的接收和转换存储
在这里插入图片描述

  1. 报文得到确认后,从头部中解析整个报文的长度,检查其是否有主体数据内容。
  2. 如果存在主体数据则再次调用TcpRecvData将后续的数据接收入data中
  3. 调用TransFirstPkg2Json(这个名字意味深长),同startListener中DecryptMessage类似,都是先根据报文中的index从g_sessionKeyList中得到sessionKey再取得sKey,将数据封装入cipher.iv中,传入DecryptTransData调用mbedtls框架中的AES_GCM进行解密得到明文并调用cJSON_Parse将数据转换为cJSON类型并返回
  4. AssignValue2Session取得报文中的BUS_NAME存入session->sessionName并将解码的密钥存入session->sessionKey中

第三部分:相应客户端回应的报文封装和发送
在这里插入图片描述
三步走:根据会话的名字得到会话监听器——>调用ResponseToClient对客户端进行回复——>对sessionListener->listener进行状态检查包括listener是否存在,对应FD是否会话打开

ResponseToClient函数:

这里创建的msg并没有使用解析得到的data数据
函数流程:
1. cJSON_CreateObject创建一个cJSON类型
2. 调用GetReplyMsg创建一个reply报文,里面包含的内容如下:

CODE:1
API_VERSION:DEFAULT_API_VERSION(2)
DEVICE_ID:BusGetLocalDeviceInfo().deviceId
UID:-1
PID:-1
PKG_NAME:session->sessionName(这个是从请求报文中解析出来的)
CLIENT_BUS_NAME:session->sessionName(这个是从请求报文中解析出来的)
AUTH_STATE:""
CHANNEL_TYPE:1

3. 调用cJSON_PrintUnformatted将cJSON转换为char指针类型
4. 调用PackBytes(实则调用AuthConnPackBytes与startListener中一致)针对模式MODULE_SESSION(6)进行报文加密
5. 调用TcpSendData进行send将报文发送到FD对应的客户端

这里注意到整个数据处理对报文中的实际数据并没有进行任何保存或者处理!
再结合函数名TransFirstPkg2Json可以推测这是TCP连接建立的三次握手中的服务端的第一次报文发送——并不携带实际数据

结合三次握手的过程,以及重点——上述的OnProcessDataAvailable函数中有两类处理:一类是发送回复的第一次报文发送;另一类只接收不回复报文。我推测这里的两类处理正好符合服务端对于三次握手的两次处理如同所示:

在这里插入图片描述

3.5 一点总结

最后小小总结一下整个startSession的内部逻辑(仅从源码解读角度,如有偏差,请一定告知修正!)

1. 首先这里存在了两种处理:连接处理ProcessConnection和数据处理ProcessSesssionData;但是这里的连接处理并不是真正意义上的三次握手的类似连接,而只是简单的将客户端的会话信息存入服务端,连接的真正建立需要等待客户端的socket套接字建立后发送的connect。但是没有这步ProcessConnection,服务端无法获取客户端的一定信息是无法进行下面的真正的三次握手的,所以两种处理存在先后,先有ProcessConnection再有ProcessSesssionData

2. 而在ProcessSesssionData中也存在两种类型的数据处理:一类是接收到connect信息后封装确认消息的HandleRequestMsg将客户端的更多信息正式保存在服务端中以便后续的通信需要;另一类就是客户端收到确认信息后发送的确认信息,而服务端只需要接收不需要再次恢复的TcpSessionRecv,所以我认为这里是客户端对应三次握手的两次处理

3. 这里使用了select机制,所以可以有序处理若干个会话的连接,统一的使用startSession来监听和管理所有的会话连接

上述观点仅是从源码出发的猜测,具体的验证需要从整体的代码实践入手
4. 最后给出整个过程的流转图供读者参考学习(红线为TCP过程)
在这里插入图片描述

4. 监听开启(startListener)

在完成了TCP可信通道的建立后会保存该session的信息和sKey密钥再次监听时,可以直接进行真正的数据交换

同样由于代码量较大所以这里做了化简,详尽的请见前几篇再谈wifiEventTrigger的博文

首先放一张简化的函数调用图看看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信号量的具体作用,可以看到操作系统中的基本原理在不断迭代更新的操作系统中仍然适用。

4.1 waitProcess——线程处理

写了一个死循环while(1)来循环处理与监听
具体流程:
1. 每次给readSet和exceptfds赋当前的g_dataFd
2. 调用select监测标识符的变化,当返回值发生变化时根据变化调用ProcessAuthData进行用户数据处理,或者异常情况和退出时break该循环并stopListener——即重置g_listenFd和g_dataFd

具体的select机制在最上面已经介绍过,所以整个线程的工作就是监视文件标识符的变动——对于可读的连接进行数据或连接的处理。

而具体的处理函数是下面要接着深入的ProcessAuthData

4.2 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进行相关数据处理

这里我倾向于认为是需要先调用onConnectEvent为监听到的连接用户创建一个AuthConn然后上链后才能调用onDataEvent进行处理——相当于onConnectEvent是CS进行通信的一个准备步骤,准备就绪后才能调用onDataEvent进行真正的通信

4.3 g_callback——回调处理函数

4.3.1 onConnectEvent——通信准备

函数参数:int fd:标识符;const char *ip:目标的IP地址
调用ProcessConnectEvent执行具体的处理

ProcessConnectEvent——新建与上链
比较简单,两个功能:结点的新建和上链
流程:
1. 通过fd调用FindAuthConnByFd从g_fdMap中找到对应的AuthConn——注意这里由于是新连接所以理论上来说在g_fdMap中应该不存在,存在则调用closeAuth将其从链上删除释放,函数return
可以从代码中看到AuthConn结构体的构成:
在这里插入图片描述
2. 不存在该连接则新建立一个AuthConn结构体将传入的fd和IP作为其属性
3. 调用AddAuthConnToList将该新建结点上链g_fdMap——在链表的尾部插入该结点

4.3.2 onDataEvent——TCP通信

/*
函数功能:通过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;
    }

    //数据处理并进行应答
    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");
}

1. 调用FindAuthConnByFd从g_fdMap中找到fd对应的AuthConn结点——此时应该是可以找到的,原本没有在刚刚的if判断中已经通过onConnectEvent事件新建一个结点
2. 当其中的数据部分也就是上面展示的结构体DataBuffer为NULL时为其初始化分配空间
3. 若已分配好空间则提取相关属性参数供接收使用

4. 调用函数AuthConnRecv
实际上是封装一层参数检查后调用tcp_socket.c中的TcpRecvData函数,调用其recv原语操作来完成对数据的接收

5. 最后的关键的数据处理和应答函数ProcessPackets。从外部结构来看,ProcessPackets函数返回了其处理的数据字段大小processed,如果其小与零则内部处理失败调用CloseConn删库跑路,如果大于0则于used做差,不等于0说明并没有处理完所有的数据,则调用memmove_s进行数据的覆盖return;最后保存使用了的空间大小内部打印并优雅return

所以能够接收的情况智能是处理的数据和接收的数据量一致的情况

接下来我们深入函数ProcessPackets看看它做了些啥

ProcessPackets——实际的数据处理函数
里面又分为两个重要的处理函数,一个用于解析接收的TCP信息,解析出头部信息和消息主题;另一个用于封装另一个packet根据通讯的模式发送报文回应该消息
在这里插入图片描述
我们重点关注其中的两个函数:ParsePacketHead和OnDataReceived

4.3.2.1 ParsePacketHead——报文解析

跟我们在计算机网络中所学的TCP报文一样,一个packet会分为头部——装载每个报文的类型和固有属性和主题——真正的数据内容,所以我们需要正确解析它们

该函数主要是结合函数GetIntFromBuf和GetLongFromBuf以及offset偏移量在buf中按在COAP协议中规定好的顺序,解析报文,从代码的最后几行我们可以看到头部主要包含以下信息:
在这里插入图片描述
module:在下面的数据处理中可以看到module的具体类型,用于不同情况下的数据处理
seq:序列号,类似TCP协议中的序列号和确认号,用于对于报文的顺序和是否送达进行排序和确认的标志
flags:类似TCP中的flags标识报文的状态
dataLen:主体的数据大小

最终返回packet结构体

4.3.2.2 OnDataReceived ——报文处理和发送

限于篇幅的原因这里不再将大段的源码放上,通过图解的方式来具体看OnDataReceived的工作流程
在这里插入图片描述
1. 首先是模式不同导致的处理方式不同,从ParsePacketHead可以解析得到module,当module为MODULE_AUTH_SDK时,调用AuthInterfaceOnDataReceived进行处理,其余的模式则调用DecryptMessage先将数据进行解密再调用OnModuleMessageReceived进行回应报文的构造和发送

2. 在auth_conn.h中我们可以看到全部的module:

#define MODULE_NONE 0
#define MODULE_TRUST_ENGINE 1
#define MODULE_HICHAIN 2
#define MODULE_AUTH_SDK 3
#define MODULE_HICHAIN_SYNC 4
#define MODULE_CONNECTION 5
#define MODULE_SESSION 6
#define MODULE_SMART_COMM 7
#define MODULE_AUTH_CHANNEL 8
#define MODULE_AUTH_MSG 9

3. 对于模式MODULE_AUTH_SDK调用AuthInterfaceOnDataReceived进行处理的具体步骤:

  • 当g_hcHandle不存在时调用AuthInitHiChain对g_hcHandle进行初始化;初始化失败调用AuthDelAuthSessionBySessionId删除g_authSessionMap中SessionId对应的内容
  • 封装request——包含数据和数据大小两项内容
  • 调用receive_data——这里没有该函数的具体内容,但是可以推测出该函数调用g_hcHandle中封装的处理函数对request进行处理并发送了回复消息
  • 在AuthInitHiChain中初始化了g_hcHandle并为其注册了hiChainCallback的五个回调函数——其中的AuthConfirmReceiveRequest从函数名可以推测这就是回复请求确认的函数,由于代码的补全这里无法给出g_hcHandle的详细解读,留待读者阅读完善

4. DecryptMessage:主要是三个功能部分:解密前的cipherKey的构造——调用DecryptTransData在mbedtls框架下用AES_GCM模式进行解密——调用cJSON_Parse将输出转换为cJSON模式进行返回

这里需要注意两点:代码的开始通过ModuleUseCipherText来判断模式是否需要加密,我们可以得出需要加密的模式和不需要加密的模式如下:

不需要加密的模式:
#define MODULE_TRUST_ENGINE 1
#define MODULE_HICHAIN 2
#define MODULE_HICHAIN_SYNC 4
#define MODULE_AUTH_CHANNEL 8
#define MODULE_AUTH_MSG 9

需要加密的模式:
#define MODULE_CONNECTION 5
#define MODULE_SESSION 6
#define MODULE_SMART_COMM 7

另一点就是:加密参数AesGcmCipherKey中的sKey与StartListener中需要sKey一样都存储在g_sessionKeyList中,也就是说在开启会话和监听之前就已经存在该sKey——这里大胆推测应该是在COAP协议发现服务初始化通信的过程中存储在本地的

5. OnModuleMessageReceived:又分为两种模式的MODULE_TRUST_ENGINE和MODULE_CONNECTION的不同处理,一个调用OnMsgOpenChannelReq,另一个调用OnMessageReceived,但是两者最后都通过AuthConnPostMessage进行报文的回复,module参数不同,需要注意区分

6. OnMsgOpenChannelReq(处理MODULE_TRUST_ENGINE类型报文)
其函数流程:

1. 调用MsgGetDeviceIdUnPack从msg中解析出cmd,deviceId,authId存入conn
2. 调用BusGetLocalDeviceInfo获取当前设备的全局存储变量g_deviceInfo
3. 调用MsgGetDeviceIdPack将本地设备的CMD_TAG,deviceId存入reply
4. 调用AuthConnPostMessage进行信息的封装和发送

7.OnMessageReceived(处理MODULE_CONNECTION类型报文)其根据CODE又分为OnVerifyIp和VerifyDeviceId——这里可以类比前面module的区别——封装头部不同;但是最后和上面的OnMsgOpenChannelReq一样调用AuthConnPostMessage进行报文的传输

OnVerifyIp头部封装以下信息:

reply:{
CODE:CODE_VERIFY_IP
BUS_MAX_VERSION:connInfo->maxVersion
BUS_MIN_VERSION:connInfo->minVersion
AUTH_PORT:authPort
SESSION_PORT:sessionPort
CONN_CAP:DEVICE_CONN_CAP_WIFI
DEVICE_NAME:devInfo->deviceName
DEVICE_TYPE:DEVICE_TYPE_DEFAULT
DEVICE_ID:devInfo->deviceId
VERSION_TYPE:devInfo->version
}

而VerifyDeviceId仅封装以下信息:

reply:{
CODE:CODE_VERIFY_DEVID
DEVICE_ID:info->deviceId
}

8. AuthConnPostMessage:调用cJSON_PrintUnformatted将传入的cJSON格式的msg转换为char;调用AuthConnPostBytes进行传输*

9. AuthConnPostBytes:调用AuthConnPackBytes对packet进行打包——进行加密;调用AuthConnSend进行传输

这里需要注意:
相关的头部信息添加入data中的顺序:

PKG_HEADER_IDENTIFIER
module
seqNum
flags
dataLen

如果需要加密则调用GetEncryptTransData进行加密(加密的具体步骤不详细讲解,这里需要注意的是在构造密钥结构体中的iv时,采用了randomIv+seqNum的方法),将结果存入data中,不需要加密则直接将data拷贝入data中,最后返回buf

最后调用TcpSendData——调用send操作将buf发送到fd对应的客户端

至此数据处理部分全部完结!

4.4 一点总结

  1. 监听处理的重要函数就是线程waitProcess的处理函数ProcessAuthData,其通过解析报文中的module和CODE来对不同类型的报文进行不同的处理

  2. 在g_baseListener中注册的两个处理函数,OnConnectEvent更多的是一个通信准备函数——负责用户数据的准备,而OnDataEvent中才是真正的报文处理函数

  3. 进行处理的时候面对不同的场景也有不同的处理方法,比如在模式MODULE_AUTH_SDK下,涉及到HiChain区块链则另外有一套机制进行处理;而在其他模式则使用通用的步骤处理;又比如在OnModuleMessageReceived中针对MODULE_TRUST_ENGINE来说因为是可信通道所以只需要封装非常少量的认证信息就能够进行信任和安全的通信。分布式软总线针对不同的场景提供了丰富而安全的通信手段,使得通信变得更加高效而便捷

  4. 最后给出在startListener中隐藏的TCP机制的流程图供读者参考学习
    在这里插入图片描述
    关于WiFi事件处理的讲解到此结束!有任何有疑惑的地方或者不对的地方还望批评指正!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

国家一级假勤奋研究牲

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

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

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

打赏作者

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

抵扣说明:

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

余额充值