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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://eric-39.gitbook.io/c++-fu-wu-kai-fa-ru-men-zhi-nan/si-yi-ge-liao-tian-fu-wu/4-clientuser-shi-xian.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
