服务发布——基于COAP协议的UDP通信机制
总体概述
- 入口函数PublishService
首先检查是否有调用软总线的权限,然后申请互斥锁开始对数据的修改,调用初始化服务函数将设备信息保存,开启WiFi监听并开启软总线工作,建立服务并启动多个线程同时工作,用于数据的收发,将相关信息保存入队列并进行登记,然后将发布模式的信息进行登记保存完成服务的发布
以下是函数流转图:
- CoapInitDiscovery发现服务实际的初始化函数
首先建立本设备的套接字作为UDP服务端,创建WiFi消息队列,用于接受WiFi相关信息,然后建立一个消息线程进行监听,线程调用线程处理函数在readSet中维护一个可读套接字的集合,从其中读取信息放入消息队列中处理,最后创建响应的UDP客户端进行数据的回应(注意这里创建的客户端是临时的,用完即销毁的,不会长期维护)
以下为函数流转图:
- COAP协议规定下的数据解析和封装过程
char* 、cJSON类型的转换
在本地时使用cJSON类型方便解析出对应的信息,方便验证和封装;
在网络传输时使用char*类型方便网络传输
- 基于COAP协议的UDP通信机制
这里方便读者理解,将UDP通信的机制以及WiFi事件在队列相应被消息处理线程处理的两个过程在图中呈现。
这里需要注意两点:
- WiFi接入的具体处理函数交给了WiFiEventTrigger来开启总线的通信以及后续的用户认证和TCP通信
- 在该场景的UDP中也就是基于COAP协议的UDP传输中,服务端socket是一直存在的,而客户端socket是不断更新,不维护的,当有下一次数据需要回应时,就会更新客户端的套接字,进行数据封装和发送,数据的解析和报文的封装过程在上一张图中有详细解读
经过上述的讲解,下面的源码解读会容易许多:
源码阅读——PublishService
函数较长但是大部分都是参数检查和错误处理
/*
函数功能:通过该函数来发现外部设备
函数参数:moduleName:上层模式名;info:包含发布服务的各种信息;cb:publish的回调函数
函数返回:<0失败;=0成功
*/
int PublishService(const char *moduleName, const struct PublishInfo *info, const struct IPublishCallback *cb)
{
//权限检查
if (SoftBusCheckPermission(SOFTBUS_PERMISSION) != 0 || info == NULL || cb == NULL) {
SOFTBUS_PRINT("[DISCOVERY] PublishService invalid para(info or cb)\n");
return ERROR_INVALID;
}
if (moduleName == NULL || strlen(moduleName) >= MAX_PACKAGE_NAME || info->publishId <= 0 ||
info->dataLen > MAX_CAPABILITY_DATA_LEN) {
SOFTBUS_PRINT("[DISCOVERY] PublishService invliad para\n");
PublishCallback(info->publishId, PUBLISH_FAIL_REASON_PARAMETER_INVALID, NULL, cb);
return ERROR_INVALID;
}
//是否为COAP协议(即WiFi)
if (info->medium != COAP) {
PublishCallback(info->publishId, PUBLISH_FAIL_REASON_NOT_SUPPORT_MEDIUM, NULL, cb);
return ERROR_INVALID;
}
if (g_discoveryMutex == NULL) {
g_discoveryMutex = MutexInit();
if (g_discoveryMutex == NULL) {
PublishCallback(info->publishId, PUBLISH_FAIL_REASON_UNKNOWN, NULL, cb);
return ERROR_FAIL;
}
}
//互斥锁的检查
MutexLock(g_discoveryMutex);
if (InitService() != ERROR_SUCCESS) {
SOFTBUS_PRINT("[DISCOVERY] PublishService InitService fail\n");
PublishCallback(info->publishId, PUBLISH_FAIL_REASON_UNKNOWN, NULL, cb);
MutexUnlock(g_discoveryMutex);
return ERROR_FAIL;
}
//添加新的g_publishModule并返回对应的结构体指针
PublishModule *findModule = AddPublishModule(moduleName, info);
if (findModule == NULL) {
SOFTBUS_PRINT("[DISCOVERY] PublishService AddPublishModule fail\n");
PublishCallback(info->publishId, PUBLISH_FAIL_REASON_UNKNOWN, NULL, cb);
MutexUnlock(g_discoveryMutex);
return ERROR_FAIL;
}
int ret = ERROR_SUCCESS;
//不存在则将默认的0存入g_localDeviceInfo.serviceData[]中
if (info->capability == NULL || info->capabilityData == NULL) {
(void)CoapRegisterDefualtService();
//存在则将其携带的capabilityMap和serviceData写入g_localDeviceInfo
} else {
ret = DoRegistService(info->medium);
}
//释放互斥锁
MutexUnlock(g_discoveryMutex);
if (ret != ERROR_SUCCESS) {
PublishCallback(info->publishId, PUBLISH_FAIL_REASON_UNKNOWN, findModule, cb);
return ERROR_FAIL;
} else {
PublishCallback(info->publishId, ERROR_SUCCESS, findModule, cb);
return ERROR_SUCCESS;
}
}
函数流程:
1. 首先调用SoftBusCheckPermission进行权限检查,判断当前是否能够启动服务
2. 对于传入参数的检查。检查moduleName的大小和info中规定的介质是否为COAP
3. 获取全局互斥锁g_discoveryMutex
4. 调用InitService完成服务的初始化
5. 调用AddPublishModule添加一个新的g_publishModule并返回对应的结构体指针
6. 对于info中的capability相关信息的注册(存储)
可以知道主要的初始化功能由InitService完成,其余的步骤起检查和存储作用
InitService
初始化的主要函数
/*
函数功能:初始化服务
函数参数:void
函数返回:ERROR_SUCCESS;ERROR_FAIL;ERROR_NOMEMORY
*/
int InitService(void)
{
if (g_isServiceInit != 0) {
return ERROR_SUCCESS;
}
//初始化g_deviceInfo——将deviceId进行了加密存储
if (InitCommonManager() != 0) {
SOFTBUS_PRINT("[DISCOVERY] InitService InitCommonManager fail\n");
DeinitService();
return ERROR_FAIL;
}
g_publishModule = calloc(1, sizeof(PublishModule) * MAX_MODULE_COUNT);
if (g_publishModule == NULL) {
DeinitService();
return ERROR_NOMEMORY;
}
g_capabilityData = calloc(1, MAX_SERVICE_DATA_LEN);
if (g_capabilityData == NULL) {
DeinitService();
return ERROR_NOMEMORY;
}
//调用WifiEventTrigger函数
//并将其登记入g_wifiCallback
RegisterWifiCallback(WifiEventTrigger);
//coap协议服务初始化
int ret = CoapInit();
if (ret != ERROR_SUCCESS) {
SOFTBUS_PRINT("[DISCOVERY] InitService CoapInit fail\n");
DeinitService();
return ret;
}
//将消息写入队列中
CoapWriteMsgQueue(UPDATE_IP_EVENT);
//根据DeviceInfo更新nstackInfo并将deviceHash和localDeviceInfo更新到g_localDeviceInfo中
ret = CoapRegisterDeviceInfo();
if (ret != ERROR_SUCCESS) {
SOFTBUS_PRINT("[DISCOVERY] InitService CoapRegisterDeviceInfo fail\n");
DeinitService();
return ret;
}
g_isServiceInit = 1;
SOFTBUS_PRINT("[DISCOVERY] InitService ok\n");
return ERROR_SUCCESS;
}
可以主要从五个函数入手:InitCommonManager、RegisterWifiCallback、CoapInit、CoapWriteMsgQueue和CoapRegisterDeviceInfo
1. InitCommonManager
由该函数来完成初始化的功能
1.1 InitLocalDeviceInfo
//初始化本地设备信息(g_deviceinfo)
int InitLocalDeviceInfo(void)
{
char deviceId[DEVICEID_MAX_NUM] = {0};
if (g_deviceInfo != NULL) {
memset_s(g_deviceInfo, sizeof(DeviceInfo), 0, sizeof(DeviceInfo));
} else {
g_deviceInfo = (DeviceInfo *)calloc(1, sizeof(DeviceInfo));
if (g_deviceInfo == NULL) {
return ERROR_FAIL;
}
}
//初始化端口和是否信任状态为-1
g_deviceInfo->devicePort = -1;
g_deviceInfo->isAccountTrusted = 1;
unsigned int ret;
//获取加密后的deviceid
ret = GetDeviceIdFromFile(deviceId, MAX_VALUE_SIZE);
if (ret != ERROR_SUCCESS) {
SOFTBUS_PRINT("[DISCOVERY] Get device fail\n");
return ERROR_FAIL;
}
//不同内核存储的信息不同
#if defined(__LITEOS_M__) || defined(__LITEOS_RISCV__)
g_deviceInfo->deviceType = L0;
ret = (unsigned int)strcpy_s(g_deviceInfo->deviceName, sizeof(g_deviceInfo->deviceName), L0_DEVICE_NAME);
#else
g_deviceInfo->deviceType = L1;
ret = (unsigned int)strcpy_s(g_deviceInfo->deviceName, sizeof(g_deviceInfo->deviceName), L1_DEVICE_NAME);
#endif
ret |= (unsigned int)strcpy_s(g_deviceInfo->deviceId, sizeof(g_deviceInfo->deviceId), deviceId);
ret |= (unsigned int)strcpy_s(g_deviceInfo->version, sizeof(g_deviceInfo->version), "1.0.0");
if (ret != 0) {
return ERROR_FAIL;
}
SOFTBUS_PRINT("[DISCOVERY] InitLocalDeviceInfo ok\n");
return ERROR_SUCCESS;
}
我们可以看看DeviceInfo结构体中都包含了哪些信息:
其中deviceType有如下定义:
该初始化函数初始化了以下属性:
devicePort:-1
isAccountTrusted:1
deviceName:L1_DEVICE_NAME|L0_DEVICE_NAME
deviceId:deviceId(加密过并存储在本地文件中)
version:" 1.0.0"
函数主要是初始化工作
有个函数比较有意思:GetDeviceIdFromFile
1.2 GetDeviceIdFromFile
deviceId作为设备的唯一标识符十分重要,所有要保证其独一无二性,这里使用了伪随机生成的方法生成deviceId
函数流程:
1. 调用ReadDeviceId将存储在本地的deviceId取出,如果本地存在则直接返回该deviceId
2. 本地不存在deviceId,则需要随机初始化一个deviceId:调用hks_generate_random生成一个随机密钥——>调用GetRandomStr根据规则生成deviceId——>调用WriteDeviceId将deviceId写入本地以便下次调用
注意这里的加密算法——也就是为随机生成算法是不可逆的,所以一次生成,不可解密,也不需解密
2. RegisterWifiCallback
其调用:
RegisterWifiCallback(WifiEventTrigger);
其内部:
这里就调用了前几篇花费大力气讲解的WiFiEventTrigger并将其加入WiFi发现的回调函数中——也就是说当发现一个外部设备通过WiFi想要接入时,就会回调WiFiEventTrigger函数进行对接
具体的WiFiEventTrigger的讲解这里不再赘述,可以参考我的前几篇博客:
再谈WiFiEventTrigger——开启监听
再谈WiFiEventTrigger——数据处理与发送
再谈WiFiEventTrigger——会话开始
3. CoapInit
内部实现为NSTACKX_Init——CoapInitDiscovery
3.1 NSTACKX_Init
函数流程:
1. 判断当前g_nstackInitState的状态是否为start状态,不是则说明初始化完毕直接返回EOK
2. 是start状态则改为ONGOING状态并调用cJSON_InitHooks进行初始化并给cJSON提供空间分配和释放的能力
3. 调用CoapInitDiscovery初始化基于COAP协议的发现服务
4. 将state改为DONE状态返回EOK
所以初始化的三个状态:START——ONGOING——DONE
下面接着看CoapInitDiscovery
3.2 CoapInitDiscovery
由四个函数共同完成COAP初始化的任务:
- 首先初始化COAP的套接字——注意COAP协议使用的是UDP模式!在函数讲解中会详细讲解
- 初始化WiFi实践处理创建消息队列
- 创立两个线程一个用于处理消息、一个用于处理监听
这里当内核为A核是需要额外调用一个CreateQueryIpThread进行IP队列线程的创建
3.2.1 CoapInitSocket
函数流程:
1. 检查g_serverFd是否大于等于0,是则服务端存在直接返回、不存在则初始化sockAddr以及sin_port
2. 调用CoapCreateUdpServer进行UDP服务端的创建
3. 调用COAP_SoftBusInitMsgId初始化g_msgId为0
需要注意的是在函数CoapCreateUdpServer中创建的流程基本和TCP一致都是先socket再bind,但是在socket创建的时候有一个参数不同:
UDP:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
TCP:(来自函数OpenTcpServer)
int fd = socket(AF_INET, SOCK_STREAM, 0);
这就是区分TCP和UDP的典型参数之一了,我们看看它分别规定了啥
- SOCK_STREAM:不保留消息边界,将每次的消息作为一个完整的单元进行接收,通过若干次独立的读操作将数据取走;有序性,保证接受的数据字节和发送顺序是完全一致的——也就是说必须双方建立通信连接;无错性,也就是说当丢包超时时会进行重发确保错误消除
- SOCK_DGRAM:无序到达;报文可能丢失,不会采取补救措施;报文有尺寸大小限制;不需要建立连接也能够进行发送
上述参数规定的区别恰好就是TCP与UDP通信的差别!
3.2.2 CoapInitWifiEvent
首先贴上源代码注释:
函数的主要作用为WiFi事件创建了一个消息队列wifiQue并且当定义了(LITEOS_M)或(LITEOS_RISCV)时为其配置了WiFi状态变换是回调处理函数g_coapEventHandler
3.2.3 CreateMsgQueThread
这里的线程创建同样因为内核的差异所以使用了不同的os_adapter进行适配,同样是通过CreateThread和一个running函数CoapWifiEventThread来完成一个线程资源的分配和线程处理函数的调度
来看看CoapWifiEventThread的内部逻辑
可以看到其调用ReadMsgQue将g_wifiQueueId消息队列中的数据读入handle中并调用handler进行处理
而这里的handler就是下面CoapWriteMsgQueue中的handler——也就是前面的主角wifiEventTrigger函数
3.2.4 CreateCoapListenThread
创建过程不再赘述,这里同样重点看下线程的实际运行函数CoapReadHandle
同样是select机制监测可读有请求的fd,检查刚刚创建的g_serverFd是否在其中,是则对其调用函数HandleReadEvent进行处理
由于调用较深,这里仅放上重要的源代码片段,更注重过程和逻辑
HandleReadEvent
涉及三个重要函数(顺序向下执行)
CoapSocketRecv:调用recvfrom从g_serverFd所在的服务端接收数据入recvBuffer中
COAP_SoftBusDecode:解析报文。主要判断以下几个属性:
pkt->protocol = COAP_UDP
pkt->header.ver = COAP_VERSION
pkt->header.tokenLen <= MAX_TOK_LEN & > 0
并解析其可选配置option和payload
PostServiceDiscover:针对解析后的decodePacket进行相应
PostServiceDiscover
GetServiceDiscoverInfo:解析数据包pkt的payload,
将设备信息存于deviceInfo中,并获取remoteUrl
inet_ntop:将数字地址转换为适合显示的文本字符串
if (remoteUrl != NULL)
CoapResponseService:向解析得到的remoteUrl和IP发送消息回复
CoapResponseService
1. 首先封装了结构体CoapRequest.remoteUrl .remoteIp
调用PrepareServiceDiscover得到char* payload包含:
a. deviceInfo中的八个信息:
deviceId
devicename
type
hicomversion
mode
deviceHash
serviceData
capabilityBitmap
b. g_interfaceList中的wlanIp
wlanIp
2. 然后得到读写的缓冲区COAP_ReadWriteBuffer
3. 调用BuildSendPkt构造应答的packet包括option、remoteIp、payload等信息的封装得到sndPktBuff
4. 将得到的sndPktBuff和其长度dataLen封装入coapRequest
5. 调用CoapSendRequest将coapRequest发送
这里到关键的发送环节结合源码分析
CoapSendRequest
函数流程:
1. 首先设置了sockAddr包括remoteIp、port、family
2. 调用CoapCreateUdpClient传入sockAddr创建一个UDP客户端
3. 调用GetCoapClientSocket得到本地的客户端Fd
4. 调用CoapSocketSend将coapRequest发送到对应的目的地址
这里有趣的是,在发送回应信息的时候创建了一个客户端——也就是说在进行UDP通信的时候一个终端设备在某些特定的时候充当服务端,有的时候充当客户端——在这里回复的时候就是创建了一个客户端来进行sendTo
另外在CoapCreateUdpClient的代码中每次调用都会新生成一个socket并进行bind,然后调用CloseSocket将原来g_clientFd对应的socket关闭,然后将新的sockfd赋给g_clientFd——也就是说每次回复信息是都需要新创建一个客户端进行回复报文的发送,发送完毕后就再下一次消息回复时被销毁,从而能够创建新的客户端进行回复
也符合UDP尽力、无保障的传输特点——发送完之后当前客户端就销毁
但是UDP的服务端socket却是一直保留的用于接收remoteURL和remoteIp
4. CoapWriteMsgQueue
注意这里的handler所指的函数CoapHandleWifiEvent实际上就是刚刚在RegisterWifiCallback函数中得到wifiEventTrigger的g_wifiCallback
函数流程:
1. 将g_wifiCallback赋给handler并将其state更新为参数所带的UPDATE_IP_EVENT
2. 调用函数WriteMsgQue将handler发送到g_wifiQueueId所在的消息队列中
通过上述操作,消息队列中就存在了处理函数——也就是wifiEventTrigger每当发现设备WiFi接入时,消息队列会调用该函数进行连接或数据处理
5. CoapRegisterDeviceInfo
同样是信息在各个结构体的流转和更新,这里不再赘述
至此服务发布——基于COAP协议的UDP通信机制的全部内容到此结束!