Engine
C++服务器编程底层库
特点
Windows,Linux双平台(Windows下为静态库,主要方便开发者调试;Linux下为动态库,用于生产环境部署)
基本包含集成服务器常用模块(数学、文件系统、配置、日志、网络、脚本、时间、多线程等)
二次开发无平台配置,无其他依赖
基于C++11开发
使用
项目使用xmake管理,使用方法详见xmake手册
注:Linux下建议使用GCC的-Wl,-rpath,.连接选项指定运行期动态连接库的优先查找目录,以方便分发部署
集成第三方说明
Zip使用miniz v2.1.0.
Lua v5.3.5.
集成Jsoncpp v1.8.4.
hiredis v0.13.3.
模块
程序模型
#include
class GameApp : public Application {
public:
GameApp() {}
virtual bool OnInit(const Commandline & cmd) override {
if (cmd.Has("--debug")) {
Logger::Instance().Initialize("game", "logs", Logger::Debug);
} else {
Logger::Instance().Initialize("game", "logs", Logger::Info);
}
/// 锁帧
LockFPS(20);
/// 填写其他初始化逻辑,当返回false时程序直接退出
return true;
}
virtual void OnBreath() override {
/// 这里填写需要每帧更新的逻辑
}
};
RUN_APP(GameApp)
网络模型
头文件socket.h,一个简单的客户端实现:
class Client : public IOListener {
public:
Client() : _socket(new Socket()) {
}
~Client() {
Close();
delete _socket;
}
bool Connect(const std::string & ip, int port) {
bool succ = _socket->Connect(ip, port);
if (succ) BindIO(_socket->ctx, IO_READ); //! 我们只需要网络有数据来时通知
return succ;
}
/// 实现数据可读时消息处理
virtual void OnReadable() override {
char buf[1024] = {0};
int readed = 0;
int recv = 0;
while (true) {
recv = _socket->Recv(buf, 1024);
if (recv > 0) {
//! TODO: process received data.
} else if (recv == 0) {
break;
} else {
Close(recv);
break;
}
}
}
void Close(int reason = 0) {
UnbindIO(); //! 关闭之前请取消IO事件监听
_socket->Close();
}
bool Send(const char * p, size_t s) {
return _socket->Send(p, s);
}
private:
Socket * _socket;
}
脚本
设计原则:Lua只负责逻辑,对象生存管理交由C++(可以注册管理到Lua)
涉及到Get操作,需要try...catch以捕获类型异常(C++注册到Lua的接口内Get不需要,调用函数不需要)
Property可以为地址方式,也可以为Getter(TG (void))、Setter(void (TS))方式注册
Method必须为int (*f)(LuaState &)
Lua不可用于多线程,只能在主线程中使用,但可以使用协程。
#include
/// 注册公共变量或函数到Lua
GLua.Register("GameSetting")//! 所有下面注册的属性或函数放在GameSetting中
.Property("nPlayerCounter", &GPlayerCount, false)//! 以地址方式注册属性,同时设置不可写
.Property("nTime", &GetTime)//! 以Getter方式注册属性,同时不注册属性的写方法(不可写)
.Method("GetAById", &GetAById);//! 注册全局Lua方法
/// 注册C++类到Lua
GLua.Register("LuaA")
.Property("nId", &A::id, false)
.Property("sName", &A::GetName, &A::SetName)
.Method("Msg", &A::SendMessage);
try {
A * p = GLua.Get("me");// 需要使用try,因为可能类型不匹配
} catch (...) {}
if (GLua.Is("me")) {} // 不需要try
GLua.Set("me", new A) // 不需要try
GLua.Call("GameSetting", "GetAById", false, 100); // 不需要try
int GetAById(LuaState & r) {
int n = r.Get(1);// 不需要try
...
}
LUA中扩展C++注册的类或名空间(注只能扩展方法,不可扩展属性)
-- 扩展名空间的方法
function XXX.yyy()
end
-- 扩展类静态方法
function LuaA.Test()
print("hehe")
end
-- 扩展类成员方法,注意:这里用的是':',因为需要使用self
function LuaA.apis:YYY()
end
内置的其他基本函数
函数
功能
print(...)
使用Logger重载的print接口[Logger::Level::Info]
print_err(...)
使用Logger重载的print接口[Logger::Level::Error]
loadbits(n, start, end) -> integer
读取一个int32中[start, end]字节表示的值
setbits(n, start, end, v) -> integer
设置一个int32中[start, end]字节表示的值
json.encode(v) -> string
将lua变量序列化成json字串
json.decode(s) -> var
将json字串反序列化成lua值
scheduler.timer(delay, func[, is_loop]) -> integer(id)
注册一个定时器
scheduler.task(hour, min, sec, func) -> integer(id)
注册一个每天hour:min:sec执行的操作
scheduler.is_valid(id) -> bool
测试一个定时器或计划是否存在
scheduler.remain(id) -> double
返回一个定时器或计划还需要多少毫秒运行
scheduler.cancel(id)
取消一个定时器或计划任务
内存池
由于本人能力有限,经实际效率测试,目前仅保留非线程安全的对象Pool(Pool.h)
Pool加锁后可用于多线程,但经测试效率还不及系统的new,但Linux下相差不大,如果考虑到无内存碎片的优点,可以自行添加。
如果采用Application的模型,Pool基本上是够用的。因为逻辑主要在主线程的Tick中触发
线程池模型
First. 编写线程内的具体工作类,继承IThreadJob.
#include
class DemoTask : public IThreadJob {
public:
DemoTask(...) { ... }//! 这里为该工作参数初始化
virtual ~DemoTask() { ... }//! 这里为工作结束时清理操作
virtual void OnRun() { ... }//! 工作的具体内容
private:
...//! 参数声明
};
Second. 创建线程池及工作对象容器
int main() {
Threads workers(4);//! 创建含有一个4个工作线程的容器
/// 增加100个并发任务(多余的会暂时等待空闲线程)
for (int i = 0; i < 100; ++i) {
workers.AddJob(...);// 传入工作需要的参数,这里自动调用 new DemoTask(...);
}
/// 等待所有的工作结束,如果不执行该操作,mgr超出生存期时会放弃未执行的任务。
workers.Wait();
return 0;
}
Redis客户端
比较简单,请自行阅读redis.h,redis.cc
定时任务
C++接口
/// 添加一个500毫秒后执行的定时器
GScheduler.Add(500, [](uint64_t id) {
printf("Timer's id : %llu", id);
});
/// 添加一个每500毫秒执行一次的定时器
GScheduler.Add(500, [](uint64_t id) {
printf("Timer's id : %llu", id);
}, true);
/// 注册每天05:00:00时执行的计划任务
GScheduler.Add(5, 0, 0, [](uint64_t id) {
printf("Task's id : %llu", id);
});
/// 是否存在定时器或计划任务
bool valid = GScheduler.IsValid(timer_id);
/// 取得一个定时器或计划多少毫秒后执行
double left = GScheduler.GetRemainTime(timer_id);
/// 取消一个定时器或计划任务
GScheduler.Cancel(timer_id);
Lua接口
scheduler.timer(500, function(id) end);
scheduler.timer(500, function(id) end, true);
scheduler.task(5, 0, 0, function(id) end);
scheduler.is_valid(timer_id);
scheduler.remain(timer_id);
scheduler.cancel(timer_id);
日志
多线程安全
日志使用之前可以初始化(不是必要的,但建议初始化—)
日志生成的结构说明
RootOfLogs指定的日志根目录
|-- 20160803首先日志会根据“年月日”分文件夹
||-- main_01_00_00.000.log其次日志会按指定大小分文件记录,文件名为指定的"Name_时_分_秒.毫秒.log"
||-- main_01_27_18.193.log
/// 初始化日志。日志名为main, 放在logs目录下,输出等级为DEBUG,每个文件最大为4M
Logger::Instance().Initialize("main", "logs", Logger::Debug, 4 * 1024 * 1024);
/// 写日志
LOG_INFO("Hello");
LOG_DEBUG("Hello %d", 2);
LOG_ERR("Error : %s", "Test");
LOG_WARN("You have a warning");
HASH
头文件 Crypto.h
CRC32算法:uint32_t CalcCRC(const char * mem, size_t size, uint32_t pre_crc = 0)
BKDRHash算法:uint32_t CalcHash(const char * mem, size_t size)
MD5算法:class MD5
SHA-1算法:class SHA1
HMAC-SHA1算法:class HMAC_SHA1
工具库
单例模型:Singleton (singleton.h)
生存域模型:ScopeGuard (scope_gurad.h)
ZIP压缩/解压缩算法:Zip(zip.h)
字符串操作:string_tools.h
配置库
CSV文件的读取见:CsvFile(csv.h)
INI文件的读取见:IniFile(ini.h)
JSON文件,使用jsoncpp:Json::Value, Json::Reader (json.h)
命令行解析: Flags (flags.h)
系统相关
头文件os.h。包含:
高精度时间:
OS::Tick()
OS::Now()
OS::GetTimeZone()
OS::ParseDataTime(year, month, day, hour, min, sec)
文件系统:
OS::Exists(path)
OS::CreateDir(dir)
OS::GetCWD()
OS::SetCWD(path)
OS::GetFullPath(path)
OS::GetDirName(path)
OS::GetFileName(path)
OS::GetFiles(path, recursive)
创建GUID/uuid
OS::CreateID();