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);
}