再谈WiFiEventTrigger——会话开始
上几篇文章讲解了wifiEventTrigger函数中关于开启监听的相关函数流程,本篇文章从总线开启的第二个重要流程startSession来剖析分布式软总线的通信原理
StartBus
从源码中我们可以看到在开启监听得到authPort后紧接着就是开启会话得到了sessionPort:
接下来就深入startSession内部来看看里面是如何组织流程的
StartSession
主要是调用CreateTcpSessionMgr来完成TCP会话的创建和管理
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
1. OpenTcpServer
这里完成了TCP服务开启的前两个过程:socket创建和bind绑定
函数流程:
1. 首先将IP使用inet_pton进行一次转换并与AF_INET一并存入sockaddr_in中
2. 调用socket创建一个套接字,有三个参数分别代表了地址族,传输类型,传输协议
3. 调用bind将用户IP绑定在创建的socket上并得到fd返回
在调用StartSelectLoop,首先调用了listen对用户端进行监听,接收到客户端的连接请求则继续往下执行
2. StartSelectLoop
与StartListener类似,但是这里需要区分的是线程的创建函数略有不同在StartListener中使用的是AuthCreate,而这里使用的是TcpCreate,具体的细节这里略去不表,重点是后面的线程running函数SelectSessionLoop:
3. SelectSessionLoop
形式上和waitProcess几乎一致:都是先通过select机制从fds集合中寻找做好请求准备的fds再调用ProcessData函数进行进一步处理。不过这里多了一个初始化函数:InitSelectList——每次请求前会在g_sessionMgr将所有符合可读或异常的fds添加到对应的fd_SET中
关于select机制的详细介绍可以参考这篇博客:
鸿蒙分布式软总线——TCP中的select机制
具体的数据处理函数
4. ProcessData
数据的具体处理函数同样分为了连接和数据处理:
1.当监测到请求的用户为当前监听的端口则可判断是要将TCP连接建立的报文则调用ProcessConnection进行连接处理;
2. 当监测到是其他的请求用户,则可判断是套接字已经完成的客户端的请求,即为数据交换报文,则调用数据处理函数
这里可以和startListener进行一个差异比较:
相同:都是通过检查是否为当前监听的端口来判断是否调用连接处理函数
差异:而对于数据的处理,startListener是通过g_dataFds进行全局的可读数据fd进行管理;而startSession通过tsm和readfds来管理这样的可读数据fd
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进行数据通信了
4.2 ProcessSesssionData
这里使用了一个for循环来比对tsm->sessionMap_中存在的fd和是否在可读rfds中,存在则说明该客户端与服务端有连接且有数据需要处理则调用OnProcessDataAvailable进行数据处理
解决了一对多的TCP通信问题
4.3 OnProcessDataAvailable
这里又根据sessionName的不同分了两类处理
这里通过不同的sessionName来区分两类数据的处理,当sessionName为softbus_Lite_unknown——也就是初始化session时设置的sessionName是调用HandleRequestMsg;当不是时则先通过GetSessionListenerByName获取会话的监听器然后调用TcpSessionRecv进行数据的接收并通过listener->onBytesReceived来通知服务端数据已接收
这里两个处理方式最最重要的区别就是调用HandleRequestMsg时调用ResponseToClient进行send而调用第二个过程时只接收消息而不再发送reply给客户端
对于第二个过程暂时并没有搞懂它是为了应对什么情况而设置的(会话终止的最后一次接收?)这里不妄加猜测,还希望伙伴们能够为我解惑!
下面就HandleRequestMsg展开:
4.4 HandleRequestMsg
该部分代码比较长
static bool HandleRequestMsg(TcpSession *session)
//用来处理用户请求消息
{
char data[RECIVED_BUFF_SIZE] = { 0 };
//接收报文头部
int size = TcpRecvData(session->fd, data, AUTH_PACKET_HEAD_SIZE, 0);
//size为接收信息的数据大小
if (size != AUTH_PACKET_HEAD_SIZE) {
return false;
}
int identifier = GetIntFromBuf(data, 0); //用来将data中内容以offset=0的偏置进行复制信息
if ((unsigned int)identifier != PKG_HEADER_IDENTIFIER) {
return false;
//检查是否复制成功
}
//得到主体数据大小
int dataLen = GetIntFromBuf(data, AUTH_PACKET_HEAD_SIZE - sizeof(int));
if (dataLen + AUTH_PACKET_HEAD_SIZE >= RECIVED_BUFF_SIZE) {
return false;
}
int total = size;
int remain = dataLen;
//除了头部还有实体数据则继续接收主体数据
while (remain > 0) {
size = TcpRecvData(session->fd, data + total, remain, 0);
remain -= size;
total += size;
}
//构造密钥将主体数据进行解密并转换为cJson类型
cJSON *receiveObj = TransFirstPkg2Json(data, dataLen + AUTH_PACKET_HEAD_SIZE);
if (receiveObj == NULL) {
return false;
}
//将skey解码并得到BUSNAME
int ret = AssignValue2Session(session, receiveObj);
cJSON_Delete(receiveObj);
if (ret != true) {
return false;
}
SessionListenerMap *sessionListener = GetSessionListenerByName(session->sessionName, strlen(session->sessionName));
if (sessionListener == NULL) {
return false;
}
//封装reply报文并send
if (!ResponseToClient(session)) {
SOFTBUS_PRINT("[TRANS] HandleRequestMsg ResponseToClient fail\n");
return false;
//响应到客户端
}
//监测当前会话的监听器状态
if (sessionListener->listener == NULL) {
return false;
}
if (sessionListener->listener->onSessionOpened == NULL) {
return false;
}
if (sessionListener->listener->onSessionOpened(session->fd) != 0) {
return false;
}
return true;
}
可以把它拆成三部分:报文头部的接收和解析——数据主体的接收和转换存储——相应客户端回应的报文封装和发送
第一部分:报文头部的接收和解析
首先调用TcpRecvData获取头部数据——实则时调用TCP的recv函数进行报文的接收。这里可以看到规定了接收的字节数为报文头部的长度,后面也做了检查如何返回的size不等于头部的规定的大小,则返回false表示报文不可信;否则调用GetIntFromBuf取一个int长度的数据检查是否为报文头部的认证部分:PKG_HEADER_IDENTIFIER,是则确信该报文有效
第二部分:数据主体部分的接收和转换存储
- 报文得到确认后,从头部中解析整个报文的长度,检查其是否有主体数据内容。
- 如果存在主体数据则再次调用TcpRecvData将后续的数据接收入data中
- 调用TransFirstPkg2Json(这个名字意味深长),同startListener中DecryptMessage类似,都是先根据报文中的index从g_sessionKeyList中得到sessionKey再取得sKey,将数据封装入cipher.iv中,传入DecryptTransData调用mbedtls框架中的AES_GCM进行解密得到明文并调用cJSON_Parse将数据转换为cJSON类型并返回
- AssignValue2Session取得报文中的BUS_NAME存入session->sessionName并将解码的密钥存入session->sessionKey中
第三部分:相应客户端回应的报文封装和发送
三步走:根据会话的名字得到会话监听器——>调用ResponseToClient对客户端进行回复——>对sessionListener->listener进行状态检查包括listener是否存在,对应FD是否会话打开
看到这里读者可能会有疑问?——好像没有对主体数据进行任何的保存或者处理?
——是的。我们把ResponseToClient函数的代码先讲解完:
/*
函数功能:创建回复客户端的报文并发送
函数参数:session:对应的连接session信息
函数返回:false:失败;true:成功
*/
static bool ResponseToClient(TcpSession *session)//客户端响应函数
{
cJSON *jsonObj = cJSON_CreateObject();
if (jsonObj == NULL) {
return false;
}
GetReplyMsg(jsonObj, session);//创建reply
char *msg = cJSON_PrintUnformatted(jsonObj);//转换为char*msg
if (msg == NULL) {
cJSON_Delete(jsonObj);
return false;、
//检查是否转换成功
}
int bufLen = 0;
//调用AuthConnPackBytes根据模式进行数据加密
unsigned char *buf = PackBytes(msg, &bufLen);
//打包数据
if (buf == NULL) {
SOFTBUS_PRINT("[TRANS] ResponseToClient PackBytes fail\n");
free(msg);
cJSON_Delete(jsonObj);
return false;
}
//send数据
int dataLen = TcpSendData(session->fd, (char*)buf, bufLen, 0);
free(msg);
cJSON_Delete(jsonObj);
free(buf);
if (dataLen <= 0) {
SOFTBUS_PRINT("[TRANS] ResponseToClient TcpSendData fail\n");
return false;
//检查数据是否发送到tcp另一端
}
return true;
}
这里创建的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函数中有两类处理:一类是发送回复的第一次报文发送;另一类只接收不回复报文。我推测这里的两类处理正好符合服务端对于三次握手的两次处理如同所示:
5. 总结
最后小小总结一下整个startSession的内部逻辑(仅从源码解读角度,如有偏差,请一定告知修正!)
1. 首先这里存在了两种处理:连接处理ProcessConnection和数据处理ProcessSesssionData;但是这里的连接处理并不是真正意义上的三次握手的类似连接,而只是简单的将客户端的会话信息存入服务端,连接的真正建立需要等待客户端的socket套接字建立后发送的connect。但是没有这步ProcessConnection,服务端无法获取客户端的一定信息是无法进行下面的真正的三次握手的,所以两种处理存在先后,先有ProcessConnection再有ProcessSesssionData
2. 而在ProcessSesssionData中也存在两种类型的数据处理:一类是接收到connect信息后封装确认消息的HandleRequestMsg将客户端的更多信息正式保存在服务端中以便后续的通信需要;另一类就是客户端收到确认信息后发送的确认信息,而服务端只需要接收不需要再次恢复的TcpSessionRecv,所以我认为这里是客户端对应三次握手的两次处理
3. 这里使用了select机制,所以可以有序处理若干个会话的连接,统一的使用startSession来监听和管理所有的会话连接
感谢阅读!