本文作者:江苏润和软件股份有限公司 郎建中
1.总体描述
1.1.总体介绍
设备通信方式多种多样(USB/WIFI/BT等),不同通信方式使用差异很大且繁琐,同时通信链路的融合共享和冲突无法处理,通信安全问题也不好保证。分布式软总线致力于实现近场设备间统一的分布式通信能力管理,提供不区分链路的设备发现和传输接口。目前实现能力包含:
l 服务发布:服务发布后周边的设备可以发现并使用服务。
l 数据传输:根据服务的名称和设备ID建立一个会话,就可以实现服务间的传输功能。
l 安全:提供通信数据的加密能力。
分布式软总线是多种终端设备的统一基座,为设备之间的互联互通提供了统一的分布式通信能力,能够快速发现并连接设备,高效地分发任务和传输数据。分布式软总线架构示意图如下:
1.2. 设备发现
在分布式软总线子系统中,设备分为发现端和被发现端。
发现端:请求使用服务的设备。一般指智慧屏设备。
被发现端:发布服务的设备。一般指轻量设备。
约束:目前必须保证发现端和被发现端处于同一个局域网内。
(1) 发现端设备,发起discover请求后,使用coap协议在局域网内发送广播。报文如下:
(2) 被发现端设备使用****PublishService****接口发布服务,接收端收到广播后,发送coap协议单播给发现端。报文格式如下:
(3) 发现端设备收到报文会更新设备信息。
被发现端发布服务的例子代码如下:
发现的流程图如下:
1.3. 数据传输
软总线提供统一的基于Session的传输功能,业务可以通过sessionId收发数据或获取其相关基本属性。当前本项目只实现被动接收Session连接的功能,业务可根据自身需要及Session自身属性判断是否接受此Session,如不接受,可以主动拒绝此连接。本项目暂未提供打开Session的相关能力。
下面是被发现端(服务提供端)向软总线申请创建Session Server的代码:
// 定义业务自身的业务名称,会话名称及相关回调
const char *g_moduleName = "BUSINESS_NAME";
const char *g_sessionName = "SESSION_NAME";
struct ISessionListener * g_sessionCallback= NULL;
// 回调实现:接收对方通过SendBytes发送的数据,此示例实现是接收到对端发送的数据后回复固定消息
void OnBytesReceivedTest(int sessionId, const void* data, unsigned int dataLen)
{
printf("OnBytesReceivedTest\n");
printf("Recv Data: %s\n", (char *)data);
printf("Recv Data dataLen: %d\n", dataLen);
char *testSendData = "Hello World, Hello!";
***\*SendBytes\****(sessionId, testSendData, strlen(testSendData));
return;
}
// 回调实现:用于处理会话关闭后的相关业务操作,如释放当前会话相关的业务资源,会话无需业务主动释放
void OnSessionClosedEventTest(int sessionId)
{
printf("Close session successfully, sessionId=%d\n", sessionId);
}
// 回调实现:用于处理会话打开后的相关业务操作。返回值为0,表示接收;反之,非0表示拒绝。此示例表示只接受其他设备的同名会话连接
int OnSessionOpenedEventTest(int sessionId)
{
if (strcmp(***\*GetPeerSessionName\****(sessionId), SESSION_NAME) != 0) {
printf("Reject the session which name is different from mine, sessionId=%d\n", sessionId);
return -1;
}
printf("Open session successfully, sessionId=%d\n", sessionId);
return 0;
}
// 向SoftBus注册业务会话服务及其回调
int StartSessionServer()
{
if (g_sessionCallback == NULL) {
g_sessionCallback = (struct ISessionListener*)malloc(sizeof(struct ISessionListener));
}
if (g_sessionCallback == NULL) {
printf("Failed to malloc g_sessionCallback!\n");
return -1;
}
g_sessionCallback->onBytesReceived = OnBytesReceivedTest;
g_sessionCallback->onSessionOpened = OnSessionOpenedEventTest;
g_sessionCallback->onSessionClosed = OnSessionClosedEventTest;
int ret = ***\*CreateSessionServer\****(g_moduleName, g_sessionName, g_sessionCallback);
if (ret < 0) {
printf("Failed to create session server!\n");
free(g_sessionCallback);
g_sessionCallback = NULL;
}
return ret;
}
// 从SoftBus中删除业务会话服务及其回调
void StopSessionServer()
{
int ret = ***\*RemoveSessionServer\****(g_moduleName, g_sessionName);
if (ret < 0) {
printf("Failed to remove session server!\n");
return;
}
if (g_sessionCallback != NULL) {
free(g_sessionCallback);
g_sessionCallback = NULL;
}
}
注意:上面的代码中的StartSessionServer()函数调用应该是在被发现端使用PublishService()函数发布服务后,在发布成功的回调中调用。可以参考《分布式调度子系统》研究中的时序图,大致的如下:
上面图中Module是指使用软总线的任意模块(比如:分布式调度子系统模块),onPublishServiceDone()回调函数是在PushService()调用中设置的回调函数。在成功发布后,软总线会调用这个回调。然后Module可以在这个回调中调用上面代码中的StartSessionServer()函数,在StartSessionServer()函数中,调用软总线的接口CreateSessionServer()创建会话服务,等待其他设备的会话连接。当其他设备会话连接成功后,软总线会首先调用OnSessionOpenedEventTest()函数,然后在数据传输完成后调用OnBytesReceivedTest()回调函数。Module可以在OnBytesReceivedTest()中处理数据协议格式的解析以及对于的Command命令字的功能调用。
2. 代码目录结构
分布式软总线的代码在foundation/communication/services/softbus_lite目录下面,目录结构如下:
authmanager:提供设备认证机制和设备知识库管理
discovery:提供基于coap协议的设备发现机制
os_adapter:操作系统适配层
trans_service:提供认证和数据传输通道
通过BUILD.gn的分析,我们知道整个softbus_lite目录下的所有源码文件将被编译到一个动态库中。其他依赖软总线的模块在编译的时候加上这个动态库的依赖就可以了。例如:分布式调度子系统所在的foundation这个bin文件的编译就依赖这个动态库。
3. 代码分析
3.1. 设备发现
设备发现的代码位于foundation/communication/services/softbus_lite/discovery目录中。这个目录下的文件如下:
coap:目录主要是负责COAP协议的部分
discovery_service:实现了轻量设备端的服务发布的能力
整个目录结构如下:
前面我们介绍过,轻量设备主要承担服务发布者,也就是被发现端的功能。被发现端主要是通过PublishService()这个函数发布服务,然后在服务发布成功后的回调中使用CreateSessionServer()函数来创建会话服务器等待发现端的连接。这个章节我们主要从代码分析PublishService()这个API的实现过程。
PublishService()函数的实现在discovery_service.c文件中,基本上其他所有的c源代码的实现都是为这个函数提供支撑的。
下面我们从PublishService()代码分析开始,分析下整个discovery目录下的源码:
这个函数大致可以分成7个部分,下面我们分别分析这个7个部分的代码。
3.1.1. 权限检查
SoftBusCheckPermission()函数实现在os_adapter目录中。这个目录结构如下:
os_adapter目录是为了适配OS操作系统的。我们知道HarmonyOS最底层的操作系统可以是Linux或者是LiteOS。因此,为了适配不同的底层操作系统,os_adapter.c中封装了一些函数,例如:SemCreate()、SemDelete()、SemWait()等,这些封装的函数在不同的操作系统上用不同的方法进行了实现。例如:SemCreate()在LiteOS中使用了LOS_SemCreate()创建信号量,在Linux上用sem_init()这个Posix标准接口创建信号量。这里不再多说,有兴趣的同学可以看一下os_adapter.c的源码。
source/L0/os_adapter.c:这个文件是适配LiteOS的
source/L1/os_adapter.c:这个文件是适配Linux的
我们看下SoftBusCheckPermission()函数的实现。在L0中的实现如下:
在LiteOS中基本没做权限的判断,只是检查参数是否有效。
在L1中的实现如下:
我们看到CheckPermission()函数,记得在Android中有类似的API来判断权限的。应该是HarmonyOS在Linux上有实现这个功能,而参数SOFTBUS_PERMISSION “ohos.permission.DISTRIBUTED_DATASYNC” 这个跟Android下的权限子串也很相似。
第二部分是输出参数有效性检查。输入参数总共是3个,分别是:
moduleName:调用者的模块名称子串
info:PublishInfo结构体,发布的信息
cb:发布成功或者失败的回调函数
上面的代码可以看到分别对moduleName是否为空,子串的长度,info里面的publishId、dataLen等进行了有效性检查。如果有问题,那么会调用 ****PublishCallback****()来回调到cb里面的失败回调函数,并且给出了出错码。
需要注意的是我们在代码中看到 info->medium必须为COAP,也就是目前只支持这个方案。其他的方案如下:
USB,BLE的方案估计以后会支持吧。
3.1.3. 创建信号量
SemCreate()函数前面已经介绍过,实现是在os_adapter.c中,分为L0和L1两个实现分别对应LiteOS和Linux。功能是创建一个信号量。这个信号量的作用是在一个进程中的不同模块之间保持互斥。也就是下面的SemWait()函数会等待信号量为一个非0的值,然后将信号量减1,如果有其他模块抢先执行了SemWait()也就是信号量为0,那么当前模块会一直等到其他模块执行SemPost()将信号量+1变成1后才能执行。这个互斥的意义是在SemWait()调用到SemPost()调用之间的所有操作保证不会有其他模块的干扰。
3.1.4. 初始化服务
我们看一下InitService()的代码:
A、是否已经初始化过了
g_isServiceInit全局变量显示是否已经初始化过了,如果已经被别的模块调用PublishService(),那么这个变量的值将为1,那么不需要第二次初始化了,直接返回。
B、初始化Common Manager(初始化g_deviceInfo结构体)
InitCommonManager()实现如下:
InitLocalDeviceInfo()函数主要是把g_deviceInfo结构体初始化好。g_deviceInfo中几个主要的成员如下:
deviceName:对于L0设备为DEV_L0
version:固定为 “1.0.0”
deviceId:通过函数GetDeviceIdFromFile()调用取得。这个函数会从"/storage/data/softbus/deviceid"文件中读取,如果读取不到,那么使用随机数字符串组成deviceId,然后再写入到上面的文件中。有兴趣的同学可以看下这个函数的实现。
C、为内部使用的数据结构分配内存
g_publishModule 这个全局变量保存所有发布服务的模块的信息数组。这个数组的元素的数据结构如下:
上面的数据结构中的成员内容基本都是从PublishInfo结构体来的:
D、注册wifi Callback
RegisterWifiCallback()函数的实现如下:
这个函数很简单,就是将callback函数赋值给全局变量g_wifiCallback。这里的callback就是WifiEventTrigger()函数。下面会具体分析 g_wifiCallback在哪里使用。
E、COAP初始化,注册TCP/IP协议栈的处理,注册session的底层socket的处理【重点】
在InitService()函数中,调用了CoapInit()函数,代码如下:
我们再看NSTACKX_Init()的代码:
继续看CoapInitDiscovery()的代码:
这里大致分三个部分:
⑴ CoapInitSocket()
CoapInitSocket()函数调用了CoapCreateUdpServer()函数,创建了一个UDP的socket,并绑定到COAP_DEFAULT_PORT(5684)端口上面上面。这个socket赋值给****g_serverFd****,在下面还会使用。
CoapCreateUdpServer()函数的实现基本上都是用socket族的标准接口实现,首先是socket()创建一个socket,然后用bind()绑定到指定的IP+PORT。
⑵ CoapInitWifiEvent()
CoapInitWifiEvent()函数大致分为两个部分:
1、CreateMsgQue()创建一个消息队列,使用RegisterWifiEvent()向wifi_lite注册事件回调函数。g_coapEventHandler.OnWifiConnectionChanged 就是当wifi连接状态发生改变(例如:设备连接到wifi后)被回调。
我们看一下CoapConnectionChangedHandler()这个回调函数:
再看下CoapWriteMsgQueue()函数代码:
所以这个函数的功能就是向消息队列中放置了一个消息。这个消息包含了wifi的状态和一个回调函数。
这个回调函数是CoapHandleWifiEvent(),代码如下:
所以这个回调函数就是调用g_wifiCallback。前面我们介绍过,g_wifiCallback被设置成了WifiEventTrigger()函数。最终当wifi状态改变后,会调用WifiEventTrigger()函数。
备注:RegisterWifiEvent()的实现在./vendor/hisi/hi3861/hi3861_adapter/hals/communication/wifi_lite/wifiservice/source/wifi_device.c中。
2、创建wifi消息队列的处理线程(任务)CoapWifiEventThread()
CoapWifiEventThread()函数的代码如下:
这个函数主要就是从wifi的消息队列中获取消息,然后执行消息中的回调函数,也就是WifiEventTrigger()函数。
⑶ CreateCoapListenThread()
这个函数的代码如下:
这个函数主要就是创建了一个线程CoapReadHandle,用于处理COAP_DEFAULT_PORT端口上的UDP socket的数据(也就是基于COAP协议的discover广播消息),代码如下:
这个函数的核心是调用了HandleReadEvent()。从UDP socket中读取数据,并解析COAP协议。代码如下:
上面的代码中 CoapSocketRecv()就是调用recvfrom()从socket中读取数据。然后调用COAP_SoftBusDecode()函数对COAP协议进行解析,解析的内容放在decodePacket结构中。最后调用PostServiceDiscover()对智慧屏发送的DISCOVER消息进行回应。这里代码不再展开,有兴趣同学自己读一下。
F、调用CoapWriteMsgQueue()触发获取wifi的IP地址,并启动总线
CoapWriteMsgQueue()函数的代码如下:
这个函数就是向wifi的消息队列写入一个消息,强制触发消息回调函数的执行。前面我们介绍过,消息回调函数就是WifiEventTrigger()。
流程图如下:
WifiEventTrigger()函数的代码如下:
这个函数的主要工作是:
1、通过CoapGetIp()获取本地设备wifi连接后的IP地址,并放入到deviceInfo->deviceIp中。后续会使用。
CoapGetIp()函数会循环执行,每次sleep 10ms,一直到能拿到IP地址。CoapGetWifiIp()函数有两个版本,分别对应LiteOS和Linux,有兴趣的同学可以自己看下代码。
2、BusManager()函数启动软总线。
StartBus()函数主要完成两个任务:
a、创建认证服务
b、创建会话服务
SelectSessionLoop()就是任务线程,会根据模块注册的sessionServer的数组监听所有的session的数据通信情况,并处理数据。这里不再展开,有兴趣同学可以自己看下代码。
注意:上层模块注册的session将保持在g_sessionMgr->sessionMap_[]数组中。
G、向COAP中注册设备信息
这个函数主要完成g_localDeviceInfo数据结构的初始化,有兴趣的同学可以自己看一下。
整个InitService的流程图如下:
小结:
初始化服务调用InitService()函数实现了如下的功能:
1、初始化了g_deviceInfo结构体,包括:deviceName,deviceId,deviceIp。
2、注册wifi_lite的event监控事件,当wifi链路发生变化的情况下(例如:设备wifi连接成功),获取当前设备wifi的IP地址,并放入到g_deviceInfo->deviceIp中,为下面TCP/IP协议栈的初始化做准备。
3、初始化UDP协议socket绑定在COAP_DEFAULT_PORT端口上,监听COAP的discover消息,并进行处理。
4、根据deviceIp初始化TCP协议socket,并且启动一个监听任务,处理连接请求,并进行auth的校验。
5、根据deviceIp初始化TCP协议socket,并且启动一个监听任务,处理session的会话请求。
3.1.5. 将Publish信息加入到Module列表
AddPublishModule()函数,将把moduleName和info(PublishInfo结构)中的内容加入到g_publishModule全局数组中。前面有过介绍,有兴趣同学可以看下这个函数的具体实现。
3.1.6. 注册COAP服务
这部分代码我们主要看下CoapRegisterDefualtService()函数实现:
代码中的 info->devicePort 就是基于TCP的认证服务的socket绑定的端口号(在StartBus()函数中赋值的)。而serviceData就是 “port:%d”的子串。
上面的代码主要是把g_localDeviceInfo.serverData赋值成 “port:auth_port”这样的子串。
3.1.7. 回调发布成功
这里很简单,就是调用PublishCallback()执行cb中的发布成功的回调函数。例如在分布式调度子系统中,使用这个回调函数可以继续进行session server的创建。
总结
1、设备发现部分代码(主要是轻量设备侧)主入口函数为PublishService()函数。
2、PublishService()函数会检查软总线的服务是否已经初始化过,如果没有则会初始化软总线的所有服务,包括:a、基于UDP的COAP协议discover发现服务。b、wifi设备状态监听服务。c、基于TCP的认证服务。d、基于TCP的session会话管理服务。
3、PublishService()然后会把模块的信息加入g_publishModule全局数组中。
4、回调模块的发布成功回调函数。
整体流程图如下: