这篇文章将会记录我从0开始学习网络编程后如何实现一个帧同步的游戏服务器
第一版使用了epoll和socket基于protobuf实现了一个简单帧同步服务器1,代码比较多,就没有特地的去加注释,主要的问题简单说一下:
1、用户根据一个.h文件里面写死的账号密码登录,没有实现注册登录功能
2、单线程,在IO的时候会造成cpu的浪费,导致服务器效率变低
3、没有动态的分配内存,虽然会快一点,但是会造成大量的空间被浪费
针对上一版的问题简单的修改了一下新版本服务器2。先说改进的地方
1.实现了用户的注册与登录,并将数据存在MySQL数据库中。
2.加入了Redis,提高效率。
3.实现了多线程,主线程读取完网络消息后塞到消息队列中,交给其他线程处理。
4.除了客户端读取的缓冲区(因为稍微有点麻烦,这个等后面写正式的服务器的时候再改进),其他地方都实现了动态内存分配。
但是这个地方又引出了新的问题,如果仔细看过上面新版本服务器的代码会发现我的SQL和Redis被我封装成了单例,这就导致几个线程调Sql语句的时候会有多线程的问题(其实MySQL是线程安全的),但是因为我的骚操作,导致代码跑起来会出现很多问题。改进方法就是删掉单例代码,在每个线程中都实例化一个SQL对象,分别建立连接,MySQL内部会处理多线程的问题,同时要注意及时的清空结果集。
改进后能顺利跑起来的服务器代码,SQLTool.h的代码放到最后。
下方代码虽然能跑起来但依旧存在一个问题:虽然消息是按顺序发来的,多线程也是在队列里面依次去取的,但是多个线程处理完之后放到待发送区却不是依次发过去的,所以还需要保证消息的有序性,解决办法可以在消息结构体最后加上一个序号用来标记,再每帧数据发送前sort一下,然后再依次发送,同时记得去掉最后的序号标记,那玩意客户端用不着,发过去就没必要了。
2022.3.17更新:离大谱,最初的单线程版本处理2500条登录请求算上网络延迟只需要2ms(设置TCP_NODELAY后),多线程的要900毫秒(虽然没设置TCP_NODELA,但这差距也太大了),后来发现可能是我哪辅助线程的原因,最后只开两个线程,主线程收发消息,副线程处理,最终在设置TCP_NODELAY的情况下,算上网络延迟总算是提高到了20ms,这里得出结论:消息处理很简单的话没必要开多线程
mark一下,怕忘记Linux下编译的指令,要加上protobuf还有sql还有多线程的模块所以编译指令很长: g++ FileName.cpp GameMessage.pb.cc -o FileName -std=c++11 -lprotobuf -I/usr/include/mysql/ -L/usr/lib64/mysql -L/usr/local/lib/ -lmysqlclient_r -lhiredis -lpthread
SQLTool.h的代码如下
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class SQLTool
{
public:
SQLTool();
bool init();
//暂时不提供修改地址、数据库账户密码的接口
void setPort(int32_t iPort) { m_iPort = iPort; }
int32_t getPort() { return m_iPort; }
void setHost(string strHost) { m_strHost = strHost; }
string getHost() { return m_strHost; }
void setTable(string strTableName) { m_strTableName = strTableName; }
string getTable() { return m_strTableName; }
//数据库操作接口(根据表的类型来增加接口)
bool getPassWord(string strUsername,string &strPassWord);
bool setPassWord(string strUsername, string strNewPassWord);
bool addUser(string strUserName, string strPassWord);
bool deleteUser(string strUserName);
//关闭数据库连接
void closeSql(){
mysql_close(&m_sqlHandler);
}
private:
pthread_mutex_t mutex_SQL;
int32_t m_iPort;
string m_strUsername; //库的name
string m_strPassword;
string m_strHost;
string m_strDBName;
string m_strTableName;
MYSQL m_sqlHandler;
};
SQLTool::SQLTool()
{
m_iPort = 3306;
m_strUsername = "game";
m_strPassword = "game123u";
m_strHost = "10.0.128.199";
m_strDBName = "DB1";
m_strTableName = "UserInfo";
init();
}
bool SQLTool::init()
{
pthread_mutex_init(&mutex_SQL, NULL);
uint32_t client_flag = 0;
//初始化连接
mysql_init(&m_sqlHandler);
mysql_options(&m_sqlHandler, MYSQL_OPT_RECONNECT, "1");
mysql_options(&m_sqlHandler, MYSQL_SET_CHARSET_NAME, "utf8");
MYSQL* connect_result = mysql_real_connect(&m_sqlHandler, m_strHost.c_str(), m_strUsername.c_str(), m_strPassword.c_str(), NULL, m_iPort, NULL, client_flag);
if (NULL == connect_result)
{
printf("mysql_real_connect failed with code [%d] msg[%s]\n", mysql_errno(&m_sqlHandler), mysql_error(&m_sqlHandler));
return false;
}
if (mysql_ping(&m_sqlHandler))
{
printf("mysql ping failed\n");
return false;
}
if (mysql_select_db(&m_sqlHandler, m_strDBName.c_str()))
{
printf("select db failed\n");
return false;
}
if (mysql_ping(&m_sqlHandler))
{
printf("mysql_ping failed\n");
return false;
}
return true;
}
bool SQLTool::getPassWord(string strUsername,string &strPassWord)
{
string sql = "SELECT PassWord FROM " + m_strTableName + " WHERE UserName=\"" + strUsername +"\"";
pthread_mutex_lock(&mutex_SQL);
if(mysql_real_query(&m_sqlHandler, sql.c_str(), sql.size()))
{
printf("mysql_real_query failed with code [%d] msg[%s]\n", mysql_errno(&m_sqlHandler), mysql_error(&m_sqlHandler));
return false;
}
MYSQL_RES* mysql_result = mysql_use_result(&m_sqlHandler);
if(mysql_result == nullptr && (mysql_field_count(&m_sqlHandler) > 0))
{
printf("mysql_use_result failed with code [%d] msg[%s]\n", mysql_errno(&m_sqlHandler), mysql_error(&m_sqlHandler));
return false;
}
MYSQL_ROW m_current_row = mysql_fetch_row(mysql_result);
if(!m_current_row)
{
return false;
}
uint64_t *m_fields_length = mysql_fetch_lengths(mysql_result); // 每一列得数据长度
strPassWord = std::string(m_current_row[0], (size_t)m_fields_length[0]);
mysql_free_result(mysql_result);
pthread_mutex_unlock(&mutex_SQL);
return true;
}
bool SQLTool::setPassWord(string strUsername, string strNewPassWord)
{
string sql = "UPDATE " + m_strTableName + " SET PassWord=\"" + strNewPassWord +"\" WHERE UserName=\"" + strUsername + "\"";
if(mysql_real_query(&m_sqlHandler, sql.c_str(), sql.size()))
{
printf("mysql_real_query failed with code [%d] msg[%s]\n", mysql_errno(&m_sqlHandler), mysql_error(&m_sqlHandler));
return false;
}
return true;
}
bool SQLTool::addUser(string strUserName, string strPassWord)
{
if(getPassWord(strUserName,strPassWord))
{
return false;
}
string sql = "INSERT INTO " + m_strTableName + " (UserName,PassWord) VALUES (\"" + strUserName + "\",\"" + strPassWord + "\")";
if(mysql_real_query(&m_sqlHandler, sql.c_str(), sql.size()))
{
printf("mysql_real_query failed with code [%d] msg[%s]\n", mysql_errno(&m_sqlHandler), mysql_error(&m_sqlHandler));
return false;
}
return true;
}
bool SQLTool::deleteUser(string strUserName)
{
return true;
}
/* TEST
string usename = "XXX";
string password = "xxx";
if(SQLTool::getInstance()->getPassWord(usename ,password))
cout<>password;
if(SQLTool::getInstance()->addUser(usename ,password)){
cout<<"注册 成功\n";
}
}
if(SQLTool::getInstance()->getPassWord(usename ,password)){
cout<<"登录成功,你的用户名和密码是:"<
class RedisTool{
public:
RedisTool();
bool init();
bool RedisToolConnect();
int handleReply();
int setValue(const string &key,const string &value);
int getValue(const string &key,string &value);
int delKey(const string &key);
private:
int m_iNum;
int m_iRoom;
redisContext* m_pm_rct;
redisReply* m_pm_rr;
};
RedisTool::RedisTool()
{
init();
}
bool RedisTool::init()
{
m_iNum=1;
m_iRoom=1;
m_pm_rct=nullptr;
m_pm_rr = nullptr;
if(RedisToolConnect())
{
return true;
}
return false;
}
bool RedisTool::RedisToolConnect()
{
m_pm_rct = redisConnect("127.0.0.1",6379);
if(m_pm_rct->err)
{
cout<<"redis connect error"<<"\n";
return false;
}
return true;
}
int RedisTool::handleReply()
{
return m_pm_rct->err;
}
int RedisTool::setValue(const string &key,const string &value)
{
string cmd="set "+key+" "+value;
m_pm_rr=(redisReply*)redisCommand(m_pm_rct,cmd.c_str());
return handleReply();
}
int RedisTool::getValue(const string &key,string &value)
{
string cmd="get "+key;
m_pm_rr=(redisReply*)redisCommand(m_pm_rct,cmd.c_str());
if(m_pm_rr->str)
{
value=string(m_pm_rr->str);
}
return handleReply();
}
int RedisTool::delKey(const string &key)
{
string cmd="del "+key;
m_pm_rr=(redisReply*)redisCommand(m_pm_rct,cmd.c_str());
return handleReply();
}