主界面获取个人信息客户端方
前言
上一集我们完成了websocket身份验证的内容,那么这一集开始我们将要配合MockServer来完成主界面获取个人信息的内容。
需求分析
我们这边是完成客户端那方的内容,当客户端登录成功之后,我们就要从服务器获取到当前登录用户的基本信息。包括头像、昵称、id、电话、个性签名等。其中,我们的头像是要直接显示在我们的主界面上的。如下图:
对应proto文件
我们之前规定了这些前后端交互的接口的URL,所以我们就要去翻一下我们的proto文件。
我们先来看到我们的路径!!!
这就是我们的前后端交互接口的URL。
我们再来看到我们网络传输的请求和响应的proto文件。
这两个分别对应的是我们请求和响应的body。请求和响应我们都需要使用protobuf进行序列化
这里我们也是需要规定一下以下内容。
客户端和网关服务器进行通信的时候,需要填充sessionId属性,而不需要填充userId的属性,我们这里的这个规定是为了确认用户的身份信息,用于鉴权。
网关服务器和其他服务器进行通信的时候,我们就不需要填充sessionId属性,而一定要有userId属性。
getMyselfAsync
我们可以看到上面proto定义的内容,我们构造的请求里面需要有请求id,用户id以及会话id,这里我们需要传入DataCenter里保存的loginSessionId这个内容,请求id我们需要到NetClient中获取,这里的用户id我们是不需要传入的!
所以我们要在DataCenter当中添加以下方法
//通过网络获取用户个人信息(异步)void getMyselfAsync();
看到这个函数的名称的末尾,我们就可以补充一个知识点:
同步与异步
async我们称之为异步,Sync称之为同步。
就举个例子解释一下。我们发送请求的时候,请求到返回一个响应的中间都是需要时间的。
我们的异步就是发送请求,就可以做其他的事情了,不会一直阻塞等待这个响应回来。
我们的同步就是与异步相反,我们发送请求后,我们需要阻塞去等这个响应回来接收到。
那么我们在代码中就不期望使用同步的方式来完成我们的功能,我们需要获取我们的个人信息的功能只要触发了,就能做其他事情去了,这个个人信息等他自己程序自己在另一个载体进行处理。
那么这个载体或者说接力棒,我们就可以很容易想到我们的信号槽。我们的程序的流程就是发送一个请求,把接力棒给到我们的信号槽,当响应回来了就触发我们的信号槽的信号,之后就针对响应的处理放到我们的槽函数。那样我们就不会在程序上卡住停滞不前了。
由于我们还需要进一步进行构造请求id,那么我们就要将loginSessionId这个参数带入到我们的NetClient,那么我们就继续创建一个方法在NetClient当中,接收我们的loginSessionId。那么我们先完成getMyselfAsync方法。
void DataCenter::getMyselfAsync()
{//这里注意!DataCenter只是处理数据,进行网络通信的是NetClientnetClient.getMyself(loginSessionId);
}
其实就是给NetClient传入loginSessionId这个信息。
NetClient::getMyself
那么进一步我们就要去完成网络通信部分的getMyself的方法。
我们的这个方法一共三个步骤
构造http的body部分、构造http请求,信号槽处理响应。
构造http的body
这一个部分是不同的请求需要构造的成员都不一样,所以我们就不能封装成一个函数来构造http的body,那么就只能按照proto文件一步一步来构造。
//构造http的body部分bite_im::GetUserInfoReq req;req.setRequestId(makeRequestId()); req.setSessionId(loginSessionId);QByteArray body = req.serialize(&serializer);LOG() << "[获取个人信息] 发送请求 requestId=" << req.requestId() << ", loginSessionId=" << loginSessionId;
这里我们需要body弄成二进制文件才能在网络上正常的传输。这个序列化器我们的NetClient里面也有,可以直接用上。
构造http请求
这一个部分就基本都是一个通用的操作了,我们就封装一个方法来实现吧。
我们先来看一下我们请求的一个格式
这个看上去还是十分的简单的。我们需要设置的就是我们是需要使用post方法,以及这个请求的路径,之后是content-type。当然我们最后要用一个QNetworkReply进行接收响应,并且返回这个响应的变量。
我们这里还是看到那个URL
由于我们的发送的不同的请求的内容只有URL和body是不同的,那么我们就要从getMyself那里传入我们需要发送的请求路径以及构造好的请求body即可。
QNetworkReply *NetClient::sendHttpRequest(const QString &apiPath, const QByteArray &body)
{QNetworkRequest httpReq;httpReq.setUrl(QUrl(HTTP_URL + apiPath));httpReq.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-protobuf");QNetworkReply* httpResp = httpClient.post(httpReq,body);return httpResp;
}
信号槽处理响应
那么到这里,我们就已经把请求发送出去了,那么我们是要把接力棒交给我们的信号槽,我们就能让这个程序异步处理我们的请求了。
处理响应之前,我们要等到响应回来我们才能正式处理响应,所以我们的信号就是这个响应是finished的状态就可以开始我们的处理。
那么我们处理响应的时候是需要先判断我们的响应的业务上是否出错,HTTP上是否有出错。
如果我们的响应没有错误,我们就可以根据不同的响应做出不同的处理了。我们这里的需要把获取到的个人信息给我们保存到DataCenter当中,毕竟我们一开始在DataCenter里面我们的这个内容是nullptr的!
那么处理我们的业务上的逻辑是否出错我们也可以封装成一个方法。
但是这个函数需要能够处理不同的返回结果的对象,所以我们就要引入我们的模板,我们要去使用泛型编程。
返回值方面,我们虽然可以使用直接使用T来返回,但是!拷贝的开销是很大的。那么我们可以引入智能指针,在函数的内部,通过new的方式创建出对象,并返回指针,为了让这个对象能够在合适的时机进行释放,我们选用智能指针就是最好的办法!
那么判断这个业务逻辑是否有问题,我们就传入这个http的响应以及传入两个指针,一个ok用于判定是否有问题,一个reason用于接收错误原因。
connect(httpResp, &QNetworkReply::finished, this, [=](){//返回值能获取一个反序列好的对象,并且判定业务上正确的bool ok = true;QString reason;auto resp = handleHttpResponse<bite_im::GetUserInfoRsp>(httpResp, &ok, &reason);if(!ok){LOG() << "[获取个人信息] 出错!requestId=" << req.requestId() <<"reason=" << reason;return;}
}
那么我们就要进去完成我们的handleHttpResponse的内容。
handleHttpResponse
我们这个函数要先判断http是否出错,我们就要去看是否NoError
之后返回一个空的智能指针即可。要记得delete以下我们的响应对象。
之后就要获取我们响应的body,之后进行反序列化
template <typename T>std::shared_ptr<T> handleHttpResponse(QNetworkReply* httpResp, bool* ok, QString* reason){//判定http是否出错if(httpResp->error() != QNetworkReply::NoError){*ok = false;*reason = httpResp->errorString();httpResp->deleteLater();return std::shared_ptr<T>();}//获取响应bodyQByteArray respBody = httpResp->readAll();//body反序列化std::shared_ptr<T> respObj = std::make_shared<T>();respObj->deserialize(&serializer, respBody);//判定业务上的逻辑if(!respObj->success()){*ok = false;*reason = respObj->errmsg();httpResp->deleteLater();return std::shared_ptr<T>();}//释放对象httpResp->deleteLater();return respObj;}
我们反序列化后能够得到以下的内容
我们就可以知道我们的业务逻辑是否是成功的,如果成功了我们就可以直接返回我们的respObj了,我们接收就可以在外面接收我们的respObj了。里面就包含了我们的userInfo!当然一定要记得我们的响应是已经使用完了,不需要他了,我们就要手动给他延迟delete!
C++模板
我们先不回到信号槽的代码!我们来谈一谈C++模板的问题
在C++中,模板的使用确实有一些特殊的考虑,特别是关于模板的声明和定义。你提到的“分离编译模型必须把声明和定义写在一起,不能分开写”的问题,实际上是由于C++模板的编译机制导致的。
模板的编译机制
C++模板是一种在编译时进行处理的泛型编程工具。编译器需要在编译时看到模板的完整定义,以便对模板进行实例化。这意味着,如果你只在头文件中声明模板而不提供定义,那么在其他.cpp文件中使用这个模板时,编译器将无法找到模板的定义,从而导致编译错误。
所以我们上面的那个函数就需要放到我们的头文件当中!
我们回到我们的信号槽部分,接下来我们就剩下把respObj里面的userInfo给放到我们的DataCenter当中。
也是十分的简单,请看下面的代码
我们一开始给myself就是一个nullptr,我们需要new一个新对象出来,再把userInfo给到我们的myself即可,这里也用到我们之前使用的load方法。
void DataCenter::resetMyself(std::shared_ptr<bite_im::GetUserInfoRsp> resp)
{if(myself == nullptr){myself = new UserInfo();}const bite_im::UserInfo& userInfo = resp->userInfo();myself->load(userInfo);
}
这里我们调用了这个函数就能把userInfo的内容放到我们的DataCenter里的myself了。
主窗口信号槽
我们只要进入主窗口就必须让这个头像和我们userInfo的内容就能够加载出来了,所以我们就要在整个触发连接信号槽的地方直接让他调用我们的getMyselfAsync的方法。只要一登录我们就能够从客户端发送请求到服务端获取我们的个人信息。
但是!展示在我们主页面的内容是也要显示正确才行啊,我们的头像还得渲染到我们的主页面的个人头像上,所以我们还得弄一个信号槽函数,用于接收一下信号,信号我们可以自定义一个,我们当把DataCenter的内容给弄上去之后就可以触发这个信号,信号被触发之后我们就会把头像的内容放到我们的主界面的userAvatar上。
//获取个人信息connect(dataCenter, &DataCenter::getMyselfDone, this,[=](){//从DataCenter中拿到响应结果的myself,把里面的头像拿出来,放置在界面上auto myself = dataCenter->getMyself();userAvatar->setIcon(myself->avatar);});dataCenter->getMyselfAsync();
signals://自定义信号void getMyselfDone();
//通过网络获取用户个人信息
void NetClient::getMyself(const QString &loginSessionId)
{//构造http的body部分bite_im::GetUserInfoReq req;req.setRequestId(makeRequestId()); req.setSessionId(loginSessionId);QByteArray body = req.serialize(&serializer);LOG() << "[获取个人信息] 发送请求 requestId=" << req.requestId() << ", loginSessionId=" << loginSessionId;//构造http请求QNetworkReply* httpResp = sendHttpRequest("/service/user/get_user_info", body);//通过信号槽,获取到当前的响应connect(httpResp, &QNetworkReply::finished, this, [=](){//返回值能获取一个反序列好的对象,并且判定业务上正确的bool ok = true;QString reason;auto resp = handleHttpResponse<bite_im::GetUserInfoRsp>(httpResp, &ok, &reason);if(!ok){LOG() << "[获取个人信息] 出错!requestId=" << req.requestId() <<"reason=" << reason;return;}//响应数据保存到DataCenter中,一开始个人信息是nullptr的dataCenter->resetMyself(resp);//通知调用逻辑,响应处理完毕emit dataCenter->getMyselfDone();//打印日志LOG() << "[获取个人信息] 响应处理完毕!requestId=" << req.requestId();});}
我们程序的链路就是:
- 主窗口调用DataCenter的getMyselfAsync
- 把DataCenter的loginSessionId传给NetClient,调用getMyself
- 进入getMyself,发送请求,返回并处理请求,保存myself,触发信号
- 触发信号,主窗口渲染头像即可。
这样我们客户端这边就暂时告一段落了,可以准备去完成测试服务器那边的内容。