Client Session for Chat Server

The first component is the binary reader.

BinaryReader

class has two member vairables:

string m_buffer;
uint32_t m_index;

The first, m_buffer, stores a string buffer, and the second is a pointer index. Based on business requirements, data is assigned to m_buffer, then data is read and the pointer m_index is moved.

The first two functions are used for outputting dumps:

static void dump(const string& buf) {
    dump(buf.c_str(), buf.size());
}

static void dump(const char* buf, size_t size) {
    for (size_t i = 0; i < size; i++) 
    {
        if (i != 0 && ((i % 16) == 0))printf("\r\n");
        printf("%02X ", (unsigned)(buf[i]) & 0xFF);
    }
    printf("\r\n");
}

Constructor and copy constructor, destructor:

BinaryReader() { m_index = 0; }
BinaryReader(const BinaryReader& reader) {
    m_buffer = reader.m_buffer;
    m_index = reader.m_index;
}
BinaryReader(const string& buffer) {
    m_index = 0;
    m_buffer = buffer;
}
~BinaryReader() = default;

Updating buffer:

void UpdateBuffer(const string& buffer) {
    m_buffer = buffer;
    m_index = 0;
}

The following is the key part, reading data from m_buffer based on the data type.

Reading int32:

bool ReadInt32(int32_t& data) {
    // If reading int32 exceeds m_buffer's limit
    if (m_index + sizeof(int32_t) > m_buffer.size()) return false;
    // Transfer the read int32 to data
    memcpy(&data, m_buffer.c_str() + m_index, sizeof(int32_t));
    // Move the pointer by the size of int32
    m_index += sizeof(int32_t);
    return true;
}

Reading other types of data:

Within the class, a template function is designed where T represents the data type, and T is converted to char* type:

template<class T>
bool ReadData(T& data) {
    if (m_index + sizeof(T) > m_buffer.size()) return false;
    char* pData = (char*)&data;
    for (size_t i = 0; i < sizeof(T); i++)
    {
        pData[i] = *(m_buffer.c_str() + m_index + sizeof(T) - (i + 1));
    }
    m_index += sizeof(T);
    return true;
}

BinaryWriter

Member variables:

string m_buffer;
uint32_t m_index;

Writing data operations:

template<class T>
bool WriteData(const T& data) {
    m_buffer.resize(m_index + sizeof(T));
    memcpy((char*)m_buffer.c_str() + m_index, &data, sizeof(data));
    m_index += sizeof(T);
    return true;
}
uint32_t Size() { return m_index; }
string toString()const { return m_buffer; }
void Clear() {
    m_index = 0;
    m_buffer.clear();
}

Compression operation:

A char occupies one byte.

Unsigned integers: One byte can store unsigned integers between 0 and 255.

Signed integers: One byte can store signed integers between -128 and 127.

0x7f is 0111 1111

size_t is an unsigned integer type, meaning it only represents non-negative integers, including 0. It does not store negative values.

In general, on 32-bit systems, size_t is 4 bytes, while on 64-bit systems, size_t is 8 bytes, making this type enhance program portability.

void Compress(size_t len, string& out) {
    char c = 0;
    if (len < 128) {
        c = (char)len & 0x7F;
        out += c;
        return;
    }
    // Assuming this length is not too long, within hundreds of megabytes
    // When outputting, it will not exceed five bytes, as 32 bits can express it
    for (int i = 4; i >= 0; i--) {
        c = (len >> (7 * i)) & 0x7F;
        // No valid data found yet, all zeros
        if (c == 0 && out.size() == 0)
            continue;
        if (i > 0)// Not the last 7 bits
            c |= 0x80;
        out += c;
    }
}
template<>
bool BinaryReader::ReadData(string& data) {
    const char* pcur = m_buffer.c_str() + m_index;
    int length = 0;
    size_t i = 0;
    for (; i < m_buffer.size() - m_index; i++) {
        length <<= 7;
        length |= pcur[i] & 0x7F;
        if ((pcur[i] & 0x80) == 0) break;
    }
    m_index += i + 1;
    data.assign(m_buffer.c_str() + m_index, length);
    m_index += length;
    return true;
}
template<>
bool BinaryWriter::WriteData(const string& data) {
    string out;
    Compress(data.size(), out);
    m_buffer.append(out.c_str(), out.size());
    m_index += out.size();
    if (data.size() > 0) {
        m_buffer.append(data.c_str(), data.size());
        m_index += data.size();
    }
    return true;
}

TcpSession

class TcpSession
{
public:
    TcpSession() = default;
    ~TcpSession() = default;
    // Send the package
    void Send(const TcpConnectionPtr& conn, BinaryWriter& writer) {
        string out = writer.toString();
        writer.Clear();
        int len = (int)out.size();// Get package length
        writer.WriteData(len + 6);
        writer.WriteData(htonl(len));
        writer.WriteData(htons(0));
        out = writer.toString() + out;
        if (conn != NULL) {
            BinaryReader::dump(out);
            conn->send(out.c_str(), out.size());
        }
    }
};

ClientSession

class ClientSession :public TcpSession
{
public:
    ClientSession(const TcpConnectionPtr& conn);
    // To control the lifecycle, prevent premature destruction or repeated destruction
    ClientSession(const ClientSession&) = delete;
    ClientSession& operator=(const ClientSession&) = delete;
    ~ClientSession();

    operator std::string() {
        return m_sessionid;
    }

    void OnRead(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time);

    // Business function
    bool Process(const TcpConnectionPtr& conn, string msgbuff);
protected:
    void OnHeartbeatResponse(const TcpConnectionPtr& conn, const string& data);
    void OnRegisterResponse(const TcpConnectionPtr& conn, const string& data);
    void OnLoginResponse(const TcpConnectionPtr& conn, const string& data);
private:
    std::string m_sessionid;
    int m_seq;// Session sequence number
};

typedef std::shared_ptr<ClientSession> ClientSessionPtr;
ClientSession::ClientSession(const TcpConnectionPtr& conn)
{
    m_seq = 0;
    // uuid_generate(m_sessionid);
    // Here, the first set of parentheses is for constructing the object
    // The second is for the () operator overload
    stringstream ss;
    ss << (void*)conn.get();
    m_sessionid = ss.str();// to_string(random_generator()());
    TcpConnectionPtr* client = const_cast<TcpConnectionPtr*>(&conn);
    (*client)->setMessageCallback(std::bind(&ClientSession::OnRead, this, _1, _2, _3));
}

ClientSession::~ClientSession()
{

}
void ClientSession::OnRead(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time)
{
    cout << __FILE__ << "(" << __LINE__ << ")\r\n";
    // Each time, take out int32 into BufferReader, which is the package size. Skip the package size, create a string msgbuff, and put the data packet contents into msgbuff, which is the real data. Why subtract 6 from the package size needs further research. The video says that this subtraction is because the format adds 6 bytes.
    // Then pass the real data to Process for processing.
    while (buf->readableBytes() >= sizeof(int32_t)) {
        int32_t packagesize = 0;
        BinaryReader::dump(buf->peek(), buf->readableBytes());
        packagesize = *(int32_t*)buf->peek();
        if (buf->readableBytes() < sizeof(int32_t) + packagesize)
            return;
        buf->retrieve(sizeof(int32_t));
        string msgbuff;
        cout << __FILE__ << "(" << __LINE__ << ")" << packagesize << "\r\n";
        msgbuff.assign(buf->peek() + 6, packagesize - 6);
        BinaryReader::dump(msgbuff);
        buf->retrieve(packagesize);
        if (Process(conn, msgbuff) != true) {
            cout << "process error,close connection!\r\n";
            conn->forceClose();
        }
    }
}

Processing function process. First, the incoming string msgbuff is placed into BinaryReader reader to parse the command. Parsing is done in three steps: first, parsing the command cmd, second, parsing the sequence number m_seq, third, parsing the data data.

bool ClientSession::Process(const TcpConnectionPtr& conn, string msgbuff)
{
    BinaryReader reader(msgbuff);
    int cmd = -1;
    if (reader.ReadData<decltype(cmd)>(cmd) == false) return false;
    if (reader.ReadData<int>(m_seq) == false) return false;
    string data;
    if (reader.ReadData(data) == false) return false;
    cout << __FILE__ << "(" << __LINE__ << ")" << cmd << "\r\n";
    cout << __FILE__ << "(" << __LINE__ << ")" << m_seq << "\r\n";
    cout << __FILE__ << "(" << __LINE__ << ")" << data.size() << "\r\n";

    switch (cmd) {
    case msg_type_heartbeart:// Heartbeat package
        OnHeartbeatResponse(conn, data);
        break;
    case msg_type_register:// Registration message
        OnRegisterResponse(conn, data);
        break;
    case msg_type_login:// Login message
        OnLoginResponse(conn, data);
        break;
    case msg_type_getofriendlist:// Get friend list
        break;
    case msg_type_finduser:// Find user
        break;
    case msg_type_operatefriend:// Operate friend
        break;
    case msg_type_updateuserinfo:// Update user information
        break;
    case msg_type_modifypassword:// Modify password
        break;
    case msg_type_creategroup:// Create group
        break;
    case msg_type_getgroupmembers:// Get group members
        break;
    case msg_type_chat:// Chat message
        break;
    case msg_type_multichat:// Broadcast message
        break;
    default:
        break;
    }
    return true;
}

OnHeartbeatResponse(conn, data)

void ClientSession::OnHeartbeatResponse(const TcpConnectionPtr& conn, const string&)
{
    // Package length 4 bytes, cannot be compressed, fixed format
    // Command type 4 bytes, cannot be compressed, fixed format
    // Package sequence number 4 bytes, cannot be compressed, fixed format
    // Package data, package length (4 bytes, can be compressed) + package content (length determined by the previous item)
    BinaryWriter writer;
    int cmd = msg_type_heartbeart;
    writer.WriteData(htonl(cmd));
    writer.WriteData(htonl(m_seq));
    string empty;
    writer.WriteData(empty);
    Send(conn, writer);
}

OnRegisterResponse

First, use Json Reader to parse the data data. If parsing fails, send a parsing failure message.

Parsing success, call UserManager to add user information to the database and return a success message.

void ClientSession::OnRegisterResponse(const TcpConnectionPtr& conn, const string& data)
{//{"username":"手机号","nickname":"昵称","password":"密码"}
    Json::Reader reader;
    Json::Value root, response;
    BinaryWriter writer;
    string result;
    int cmd = msg_type_register;
    writer.WriteData(htonl(cmd));
    writer.WriteData(htonl(m_seq));
    if (reader.parse(data, root) == false) {
        cout << "error json:" << data << endl;
        response["code"] = 101;
        response["msg"] = "json parse failed!";
        result = response.toStyledString();
        writer.WriteData(result);
        Send(conn, writer);
        return;
    }
    if (!root["username"].isString() ||
        !root["nickname"].isString() ||
        !root["password"].isString())
    {
        cout << "error type:" << data << endl;
        response["code"] = 102;
        response["msg"] = "json data type error!";
        result = response.toStyledString();
        writer.WriteData(result);
        Send(conn, writer);
        return;
    }
    User user;
    user.username = root["username"].asString();
    user.nickname = root["nickname"].asString();
    user.password = root["password"].asString();

    if (!Singleton<UserManager>::instance().AddUser(user))
    {
        cout << "add user failed!\r\n";
        response["code"] = 100;
        response["msg"] = "register failed!";
        result = response.toStyledString();
        writer.WriteData(result);
        Send(conn, writer);
        return;
    }
    else {
        root["code"] = 0;
        root["msg"] = "ok";
        result = root.toStyledString();
        writer.WriteData(result);
        Send(conn, writer);
    }
}

OnLoginResponse

Login

First, use Json Reader to parse the data data. If parsing fails, send a parsing failure message.

Parsing success, get the username and password input by the user, and match them against the database. If matched correctly, return the correct information. If not matched, return the error information.

void ClientSession::OnLoginResponse(const TcpConnectionPtr& conn, const string& data)
{//{"username":"用户名","password":"密码","clienttype":1,"status":1}
    BinaryWriter writer;
    string result;
    Json::Value root, response;
    Json::Reader reader;
    int cmd = msg_type_login;
    writer.WriteData(htonl(cmd));
    writer.WriteData(htonl(m_seq));
    if (reader.parse(data, root) == false) {
        cout << __FILE__ << "(" << __LINE__ << ")\r\n";
        response["code"] = 101;
        response["msg"] = "json parse failed!";
        result = response.toStyledString();
        writer.WriteData(result);
        Send(conn, writer);
        return;
    }
    if (!root["username"].isString() ||
        !root["password"].isString() ||
        !root["cleinttype"].isInt() ||
        !root["status"].isInt())
    {
        cout << __FILE__ << "(" << __LINE__ << ")\r\n";
        response["code"] = 102;
        response["msg"] = "json data type error!";
        result = response.toStyledString();
        writer.WriteData(result);
        Send(conn, writer);
        return;
    }
    string username = root["username"].asString();
    string password = root["password"].asString();
    User user;
    if (Singleton<UserManager>::instance().GetUserInfoByUsername(username, user) == false)
    {
        cout << __FILE__ << "(" << __LINE__ << ")\r\n";
        response["code"] = 103;
        response["msg"] = "user is not exist or password is incorrect!";
        result = response.toStyledString();
        writer.WriteData(result);
        Send(conn, writer);
        return;
    }

    if (password != user.password) {
        cout << __FILE__ << "(" << __LINE__ << ")\r\n";
        response["code"] = 104;
        response["msg"] = "user is not exist or password is incorrect!";
        result = response.toStyledString();
        writer.WriteData(result);
        Send(conn, writer);
        return;
    }


    // If successful, return the response
    response["code"] = 0;
    response["msg"] = "ok";
    response["userid"] = user.userid;
    response["username"] = user.username;
    response["nickname"] = user.nickname;
    resposne["facetype"] = user.facetype;
    response["customface"] = user.customface;
    response["gender"] = user.gender;
    response["birthday"] = user.birthday;
    response["signature"] = user.signature;
    response["address"] = user.address;
    response["phonenumber"] = user.phonenumber;
    response["mail"] = user.mail;
    result = response.toStyledString();
    writer.WriteData(result);
    cout << __FILE__ << "(" << __LINE__ << ")\r\n";
    Send(conn, writer);
}

Tags: chat server client session binary reader binary writer tcp session

Posted on Sat, 03 Oct 2026 16:39:56 +0000 by oolongdavies