# 4 ClientUser实现

我们为客户端定义了一个代理类ClientUser，该类继承于基类库中的LinkUser，头文件实现如下

```cpp
#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，函数中可以处理一些清理操作。

```cpp
bool ClientUser::OnBreak()
{
    CLinkUser::OnBreak();
    LogInfo("Client User break %d", m_sesID);
    g_pService->OnClientBreak(m_sesID);
    return true;
}
```

最后，定义了一个成员变量nickName，用于记录聊天用户的昵称。

下面看看OnMsg函数的具体实现：

```cpp
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头文件中

```cpp
#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在会话创建时已经自动分配，此处可以直接回复客户端

```cpp
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方法，将消息回复给客户端。

下面再讲一个略微复杂的请求，客户端请求发送消息的处理

```cpp
 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的请求处理，与上面基本相同，不再详细描述，附上代码

```cpp
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
```
