我们为客户端定义了一个代理类ClientUser,该类继承于基类库中的LinkUser,头文件实现如下
#pragma once
#include "LinkUser.h"
#include <string>
#include "ChatProtocol.h"
class ClientUser : public CLinkUser
{
public:
ClientUser();
public:
virtual bool OnMsg(char* msg, int nLen);
virtual bool OnBreak();
public:
bool OnRequestSessionID();
bool OnCreateRoom();
bool OnJoinRoom(PHYHEADER pHeader);
bool OnChatMsg(PHYHEADER pHeader);
int64_t GetSesID(){return m_sesID;}
public:
std::string m_strNickName;
};
基类库的TCPSession在会话创立后,根据消息来源创建,调用服务的CreateUser函数,创建了User对象,并将其和Session绑定,后续ClientUser可以通过这个会话和客户端进行消息通信。
OnMsg函数用于处理客户端发来的消息,根据消息类型的不同,分别由下面的OnXXX函数处理。
给客户端发送消息,调用基类函数SendMsg(char* msg, int nLen);
和客户端连接状态异常,端开连接后,会调用函数OnBreak,函数中可以处理一些清理操作。
bool ClientUser::OnBreak()
{
CLinkUser::OnBreak();
LogInfo("Client User break %d", m_sesID);
g_pService->OnClientBreak(m_sesID);
return true;
}
最后,定义了一个成员变量nickName,用于记录聊天用户的昵称。
下面看看OnMsg函数的具体实现:
bool ClientUser::OnMsg(char* msg, int nLen)
{
//LogInfo("Client User Recv msg %d", nLen);
PHYHEADER pHeader = (PHYHEADER)msg;
switch (pHeader->wType){
case MSG_TYPE_REQUEST_SESSIONID:
OnRequestSessionID();
break;
case MSG_TYPE_CREATE_ROOM:
OnCreateRoom();
break;
case MSG_TYPE_JOIN_ROOM:
OnJoinRoom(pHeader);
break;
case MSG_TYPE_SEND_CHAT_MSG:
OnChatMsg(pHeader);
break;
}
return true;
}
ClientUser收到的消息,其消息格式为HYHEADER打头,这个是由基类保证的,业务层可以直接转换使用。
pHeader的wType参数,标识了消息类型,每个消息定义了一个类型。统一定义在ChatProtocol头文件中
#pragma once
#include "Protocol.h"
const int ORIGINE_CLIENT = 1;
const int ORIGINE_CHATSVR = 2001;
// 请求会话id
// REQ(Client->Server):RequestSessionID 消息ID 101,客户端连接到服务后发送,无需其他参数
// ACK(Server->Client):消息为json字符串{"sesID":"1000012345"}
const int MSG_TYPE_REQUEST_SESSIONID = 101;
// 创建聊天室:
// REQ:CreateRoom 消息ID 102,客户端获取会话id后发送,无需其他参数
// ACK:成功返回房间ID {"status":0, "roomID":100001},失败返回对应错误码{"status":1}
// status 状态定义:
// 0,成功,非0为创建失败;
// 1,未找到会话id,需要先申请会话id;
const int MSG_TYPE_CREATE_ROOM = 102;
// 加入聊天室:
// REQ:JoinRoom 消息ID 103,需要指定要加入的房间id {"roomID":100001}
// ACK:返回加入状态,由消息头中wParam字段携带
// 0,加入成功
// 1,加入失败,聊天室未找到
// 2,加入失败,聊天室人数已达上限
const int MSG_TYPE_JOIN_ROOM = 103;
// 获取我的聊天室:
// REQ:GetMyRoom 消息ID 104,无需其他参数
// ACK:返回聊天室列表{"Create":100001,"his":[100002,100001],"in":100001}
const int MSG_TYPE_GET_MY_ROOM = 104;
// 聊天室内发送消息:
// REQ: SendMsg 消息ID 105 ,参数为聊天室id和消息内容{"roomID":100001, "msg":"this is the first msg"}
// ACK: wParam返回发送状态,0成功,1失败,不在聊天室内
const int MSG_TYPE_SEND_CHAT_MSG = 105;
// 聊天室内获取房间内用户名单:
// REQ:GetRoomUserList 消息ID 106,无需参数,如果没在聊天室内,则返回错误
// ACK:wParam返回获取状态,0成功,消息体为用户昵称列表 {"users":["张三","李四"]}
// 1 失败,不在聊天室内
const int MSG_TYPE_GET_ROOM_USER_LIST = 106;
// 广播消息:
// ACK: BroadcastMsg 消息ID 107, {"msg":"this is the first msg","sender":"张三"}
const int MSG_TYPE_BROADCAST_MSG = 107;
先看获取会话ID的请求,因为会话ID在会话创建时已经自动分配,此处可以直接回复客户端
bool ClientUser::OnRequestSessionID()
{
CHYBuffer buf;
buf.SetHeaderLen(HYHEADERSIZE);
buf.AppendFormatString("{\"sesID\":\"%lld\"}", m_sesID);
LogInfo("ClientUser::OnRequestSessionID: %lld", m_sesID);
PHYHEADER pHeader = (PHYHEADER)buf.GetBufPtr();
pHeader->dwLength = buf.GetDataLen();
pHeader->wOrigine = ORIGINE_CHATSVR;
pHeader->wType = MSG_TYPE_REQUEST_SESSIONID | HY_ACK;
SendMsg(buf.GetBufPtr(), buf.GetBufLen());
return true;
}
通过自定义的CHYBuffer,拼接了消息头和消息内容。
首先SetHeaderLen设置消息头长度,但此时未分配空间。后面执行AppendFormatString的时候,才真正分配空间,并在数据前面预留出消息头的数据长度。
消息内容是一个json字符串,因为比较简单,直接进行字符串拼接,内容拼接后为{"sesID":"1"}。
消息头是HYHEADER格式,dwLength是消息内容的长度,此处使用了GetDataLen()函数,是指除了SetHeaderLen函数之后的数据长度,也就是我们发送的内容长度。后面再SendMsg时,传入的参数是GetBufLen(),此时获取的长度是整个buf的数据长度,包含了消息头和消息内容。这两个函数略有差异。
wOrigine设置了消息来源,这个后期可以整合到基类中,避免每次设置,有兴趣的同学可以进行优化。
wType设置了回复消息的类型,这里使用了HY_ACK或上请求类型,wType定义的类型为2个字节,因此最大表示范围到65535.
最后,直接在函数中调用基类的SendMsg方法,将消息回复给客户端。
下面再讲一个略微复杂的请求,客户端请求发送消息的处理
bool ClientUser::OnChatMsg(PHYHEADER pHeader)
{
char* pData = (char*)(pHeader + 1);
std::cout << pData << std::endl;
json j = json::parse(pData);
int nRoomID = 0;
std::string msg;
if (j["msg"].is_string())
{
LogInfo("recv client msg :%s", pData);
msg = j["msg"];
}
if (j["roomID"].is_number_integer())
{
nRoomID = j["roomID"];
}
int nRet = g_pService->OnSendRoomMsg(m_sesID, nRoomID, msg);
CHYBuffer buf;
buf.SetHeaderLen(HYHEADERSIZE);
buf.AppendFormatString("{\"status\":%d}", nRet);
PHYHEADER pHdr = (PHYHEADER)buf.GetBufPtr();
pHdr->dwLength = buf.GetDataLen();
pHdr->wOrigine = ORIGINE_CHATSVR;
pHdr->wType = MSG_TYPE_SEND_CHAT_MSG | HY_ACK;
SendMsg(buf.GetBufPtr(), buf.GetBufLen());
return true;
}
请求发送消息的请求,是携带数据内容的,数据紧跟再Header后面,为json格式。
此处引入了github上的json解析类nlohmann/json.hpp,用于解析客户端上传的json数据。
首先将指针偏移到消息头后面,取char指针作为数据起始,解析json中的msg和roomID。
这里直接使用了服务的指针,调用服务中相应的处理方法,当然这里也可以使用其他方式达到相同的目的,并且解除ClientUser和ChatService之间的耦合,不过直接调用指针也没有太坏的影响,且更为直观,如此处理即可。
同步调用了服务的方法,得到返回结果,作为status的参数,发送给客户端,消息发送方式与获取SessionID的相同,此处不再赘述。
CreateRoom和JoinRoom的请求处理,与上面基本相同,不再详细描述,附上代码
bool ClientUser::OnCreateRoom()
{
int nRoomID = 0;
int nRet = g_pService->OnCreateRoom(m_sesID, nRoomID);
LogInfo("ClientUser::OnCreateRoom: %lld, room %d", m_sesID, nRoomID);
CHYBuffer buf;
buf.SetHeaderLen(HYHEADERSIZE);
buf.AppendFormatString("{\"status\":\"%d\", \"roomid\":%d}", nRet, nRoomID);
PHYHEADER pHeader = (PHYHEADER)buf.GetBufPtr();
pHeader->dwLength = buf.GetDataLen();
pHeader->wOrigine = ORIGINE_CHATSVR;
pHeader->wType = MSG_TYPE_CREATE_ROOM | HY_ACK;
SendMsg(buf.GetBufPtr(), buf.GetBufLen());
return true;
}
bool ClientUser::OnJoinRoom(PHYHEADER pHeader)
{
char* pData = (char*)(pHeader + 1);
std::cout << pData << std::endl;
json j = json::parse(pData);
int nRoomID = 0;
if (j["roomID"].is_number_integer())
{
nRoomID = j["roomID"].get<int>();
}
LogInfo("ClientUser::OnJoinRoom %lld, %s", m_sesID, pData);
CHYBuffer buf;
buf.SetHeaderLen(HYHEADERSIZE);
if (nRoomID <= 0)
{
buf.AppendFormatString("{\"status\":1}");
}
int nRet = g_pService->JoinRoom(m_sesID, nRoomID, this);
buf.AppendFormatString("{\"status\":%d, \"roomID\":%d}", nRet, nRoomID);
PHYHEADER pHdr = (PHYHEADER)buf.GetBufPtr();
pHdr->dwLength = buf.GetDataLen();
pHdr->wOrigine = ORIGINE_CHATSVR;
pHdr->wType = MSG_TYPE_JOIN_ROOM | HY_ACK;
SendMsg(buf.GetBufPtr(), buf.GetBufLen());
return true;
C