包含对象名字的游戏id_教你从头写游戏服务器框架(三)关于作者:韩伟,腾讯互娱⾼级⼯程师,⽬前在Next产品中⼼研发创新类型游戏。
本⽂为系列⽂章的第 3篇
教你从头写游戏服务器框架(1)
第⼀篇:
第⼀篇:教你从头写游戏服务器框架
教你从头写游戏服务器框架(2)
第⼆篇:教你从头写游戏服务器框架
第⼆篇:
协 程
使⽤异步⾮阻塞编程,确实能获得很好的性能。但是在代码上,确⾮常不直观。因为任何⼀个可能阻塞的操作,都必须要要通过“回调”函数来链接。⽐如⼀个玩家登录,你需要先读数据库,然后读⼀个远程
smarty模板文件的后缀名为缓冲服务器(如 redis),然后返回登录结果:⽤户名、等级……在这个过程⾥,有两个可能阻塞的操作,你就必须把这个登录的程序,分成三个函数来编写:⼀个是收到客户端数据包的回调,第⼆个是读取数据库后的回调,第三个是读取缓冲服务器后的回调。
这种情况下,代码被放在三个函数⾥,对于阅读代码的⼈来说,是⼀种负担。因为我们阅读代码,⽐如通过⽇志、coredump 去查问题,往往会直接切⼊到某⼀个函数⾥。这个被切⼊阅读的函数,很可能就是⼀个回调函数,对于这个函数为什么会被调⽤,属于什么流程,单从这个函数的代码是很难理解的。
另外⼀个负担,是关于开发过程的。我们知道回调函数的代码,是需要“上下⽂”的,也就是发起回调时的数据状态的。为了让回调函数能获得发起函数的⼀个变量内容,我们就必须把这个变量内容放到某个“上下⽂”的变量中,然后传给回调函数。由于业务逻辑的变化,这种需要传递的上下⽂变量会不停的变化,反复的编写“放⼊”“取出”上下⽂的代码,也是⼀种重复的编码劳动。⽽且上下⽂本⾝的设置可能也不够安全,因为你⽆法预计,哪个回调函数会怎么样的修改这个上下⽂对象,这也是很多难以调试的 BUG 的来源。
为了解决这个问题,出现了所谓的协程技术。我们可以认为,协程技术提供给我们⼀种特殊的 return 语句:yield。这个语句会类似
return ⼀样从函数中返回,但你可以⽤另外⼀个特殊的语句 resume(id) 来从新从 yield 语句下⽅开始运⾏代码。更重要的是,在 resume 之后,之前整个函数中的所有临时变量,都是可以继续访问的。
当然,做 resume(id) 的时候,肯定是在进程的所谓“主循环”中,⽽这个 id 参数,则代表了被中断了的函数。这种可以被中断的函数调⽤过程,就叫协程。⽽这个 id ,则是代表了协程的⼀个数字。异步调⽤的上下⽂变量,就被⾃动的以这个协程函数的“栈”所取代,也就是说,协程函数中的所有局部变量,都⾃动的成为了上下⽂的内容。这样就再也不⽤反复的编写“放⼊”“取出”上下⽂内容的代码了。
游戏开发中,协程确实能⼤⼤的提⾼开发效率。因此我认为协程也应该是 Game Server 所应该具备的能⼒。特别是在处理业务逻辑的Handler 的 Process() 函数,本⾝就应该是⼀个协程函数。所以我设计了⼀个 CoroutineProcessor 的类,为普通的 Processor 添加上协程的能⼒。——基于装饰器模式。这样任何的 Processor::Process() 函数,就⾃然的在⼀个协程之中。
因为有了协程的⽀持,那些可能产⽣阻塞⽽要求编写回调的功能,就可以统⼀的变成以协程使⽤的 API 了:
1. DataStore -> CoroutineDataStore
2. Cache -> CoroutineCache
3. Client -> CoroutineClient
使⽤协程的 API,就完全不需要各种 Callback 类型的参数了,完全提供⼀个返回结果⽤的输出参数即可。
/**
* @brief DataStore 的具备协程能⼒的装饰器类型。
* @attention 除了定义变量语句和 Update() 以外,其他的操作都需要在协程中调⽤。
*/
class CoroutineDataStore : public Updateable{
bootstrapping中介分析public:
CoroutineDataStore(DataStore* data_store, CoroutineSchedule* schedule);
virtual ~CoroutineDataStore();
int Init(Config* cfg, std::string* err_msg);
/**
* 读取⼀个数据对象,通过 key ,把数据放⼊到输出参数 value。
* 此函数会在调⽤过程中使⽤协程的 yield 出去。
*/
int Get(const std::string&key, Serializable* value);
/**
* 写⼊⼀个数据对象,写⼊ key ,value
* 写⼊结果从返回值获得,返回 0 表⽰成功,其他值表⽰失败。
* 此函数会在调⽤过程中使⽤协程的 yield 出去。
*/
int Put(const std::string&key, const Serializable& value);
/**
德甲复赛首球创造历史
* 删除⼀个数据对象,通过 key
* 写⼊结果从返回值获得,返回 0 表⽰成功,其他值表⽰失败。
* 此函数会在调⽤过程中使⽤协程的 yield 出去
*/
int Remove(const std::string& key);
int Update();
private:
DataStore* data_store_;
CoroutineSchedule* schedule_;
};
服务器对象管理
01
组件模型
⼀般来说服务器上,主要是运⾏各种各样处理请求的代码为主(通常叫 Handler)。然⽽,我们也会有⼀些需要持续运⾏的逻辑代码,⽐如处理匹配玩家战⽃的逻辑,检查玩家是否超时发呆的逻辑,循环处理⽀付订单等等。这些代码的很多功能,同时还需要被各种 Handler 所调⽤。所以我们必须要有⼀种能让所有的这些⾃定义代码,以⼀种标准的⽅式在进程中互相引⽤,以及管理⽣命周期的⽅法。
借鉴于 Unity, 我觉得使⽤所谓的组件模型是很好的。它的特点包括:
1. 组件之间通过 Application::GetComponet(name) 的⽅式互相调⽤。以⼀个字符串作为索引,就可以⽅便的获得对于的对象。组件
⾃⼰通过 Application::Register(com_obj) 注册到系统中去,注册的名字⾃⼰实现 string GetName() 的接⼝去提供。
2. 每个组件有预定的⼏个回调函数,提供进程⽣命周期的调⽤机会。包括:
初始化:Init()
主循环更新:Update()
关闭:Close()
/**
* 代表⼀个应⽤程序组件,所有的应⽤程序组件应该继承此对象
*/
class Component : public Updateable {
public:
Component();
virtual ~Component();
/**
* 返回此组件的名字
* @return 名字
*/
virtual std::string GetName() = 0;
/**
* 初始化过程会调⽤此⽅法
* @param app 进程对象
* @param cfg 配置
* @return 返回 0 表⽰成功,其他表⽰失败
*/
virtual int Init(Application* app, Config* cfg);
/**
* 更新过程会调⽤此⽅法
* @return 返回更新处理的负载,0 表⽰没有负载,负数表⽰出错或需要停⽌对此组件进⾏更新
*/
virtual int Update();
/**
* 应⽤停⽌时会调⽤此⽅法
* @return 返回 0 表⽰可以退出,返回 >0 表⽰需要等待返回值的秒数,返回 < 0 表⽰出错退出
*/
virtual int Stop() {
return 0;
post怎么读音发音二叉树前序中序后序非递归};
/
**
* 设置组件被加⼊的应⽤程序,⽤于让组件继承者能简单的获取 Application 对象
* @note 如果⼀个组件被加⼊多个不同的 Application,必须使⽤ @see Init() ⽅法来具体保存 Application 对象,* 因为此处修改的成员对象 app_ 将是最后⼀次被添加进的 Application 对象。
* @param app 要设置的 Application 对象。
*/
void set_app(Application* app){
app_ = app;
}
protected:
Application* app_;
};
02
Server 对象
由于⼀个游戏服务器,所集成的功能实在是太多了,⽐如配置不同的协议、不同的处理器、提供数据库功能等等。要让这样⼀个服务器对象启动起来,需要⼤量的“组装代码”。为了节省这种代码,我设计了⼀个 LocalServer 的类型,作为⼀个 Server 模板,简化⽹络层的组装。使⽤者可以继承这个类,⽤来实现各种不同的 Server。
class LocalServerApp : public Application {
public:
LocalServerApp();
virtual ~LocalServerApp();
virtual int Init(Config* cfg = NULL);
virtual int Exit();
centos和ubuntu
void set_transport(Transport* transport);
void set_protocol(Protocol* protocol);
void set_processor(Processor* processor);
private:
Transport* transport_;
Protocol* protocol_;
Processor* processor_;
Server* server_;
};
这个简单的类,可以通过 setter ⽅法来⾃定义⽹络层的组件,否则就是最常⽤的 TCP, TLV, Echo 这种服务器。⽽且这个类还是继承于Application 的,这样可以让数据库或者其他的组件,也很⽅便的利⽤组件系统安装到服务器上。
集功能
01
需求分析
游戏常常是⼀个带状态的服务。所以集功能⾮常困难。
有⼀些框架,试图把状态从逻辑进程中搬迁出来,放在缓冲服务器中,但是往往满⾜不了性能需求。另外⼀些框架,则把集定义成⼀个固定的层次架构,通过复杂的消息转发规则,来试图“把请求发到装载状态的进程上”,但这导致了运维部署的巨⼤复杂性。
为了解决这些问题,我觉得有⼏个设计决策是必须要订⽴的:
1. 使⽤ SOA 的模式:集中⼼的地址作为集的地址,通过服务名来分割逻辑
2. 提供给⽤户⾃定义路由的接⼝:由于集中的进程都带有状态,要把请求发给哪个进程,并不能完全⾃动选择,所以必须要⽤户提供
代码来选择
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论