lua和C交互的核心就是lua栈,lua和C的所有数据交互都是通过lua栈来完成的。

一. C调用lua

C调用lua很简单,通常C以lua作为配置脚本,在运行时读取脚本数据,主要步骤:

  1. 加载脚本 luaL_loadfile
  2. 运行脚本 lua_pcall
  3. 获取数据 lua_getglobal ….
  4. 使用数据 lua_tostring lua_pcall …

二. 在lua脚本中调用C:

在C程序中,使用lua作为脚本,但是要在运行脚本时,访问C中定义的一些变量或函数。

  1. 将C变量或函数(遵从指定函数原型,见下面三 Step 1)push到lua栈中
  2. 通过lua_setglobal为当前lua栈顶的函数或变量命名,这样在lua中可通过该名字完成对变量或函数的使用
  3. 之后可在加载的lua脚本中使用C变量或函数
阅读全文 »

PlayerSession类

在之前的网络底层设计中,Player和Session之间通过组合实现弱关联,但仍然有个诟病:Player类和Session类在网络连接到来时一并创建了。这样后面在做断线重连的时候,会有两个Player。而事实上LoginService只管登录认证,登录认证的时候并不需要创建Player类,因此可以延迟Player的创建,将其放在MapService中。而这之前LoginService的登录认证也需要用户的一些基本信息。基于这些,实现了PlayerSession类:

阅读全文 »

总结下几个使用shared_ptr需要注意的问题:

一. 相互引用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class C;
class B : public std::enable_shared_from_this<B>
{
public:
~B(){ cout << "~B" << endl; }
void SetPC(std::shared_ptr<C>& pc){ _pc = pc; }

private:
std::shared_ptr<C> _pc;
};

class C : public std::enable_shared_from_this<C>
{
public:
~C(){ cout << "~C" << endl; }
void SetPB(std::shared_ptr<B>& pb){ _pb = pb; }

private:
std::shared_ptr<B> _pb;
};

int main()
{
std::shared_ptr<C> pc = std::make_shared<C>();
std::shared_ptr<B> pb = std::make_shared<B>();
pc->SetPB(pb);
pb->SetPC(pc);
return 0;
}

上面的代码中,B和C均不能正确析构,正确的做法是,在B和C的释放函数,如Close中,将其包含的shared_ptr置空。这样才能解开引用链。

阅读全文 »

本文是《深度探索C++对象模型》的读书笔记,主要根据自己的理解整理一下C++对象构造的实现细节以及其在实际编程中所带来的影响。

一. 构造函数

C++95标准对构造函数如下描述:

对于 class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式声明出来…..一个被隐式声明出来的 default constructor 将是一个trivial(浅薄无能的,没啥用的) constructor ……

上面的话摘自《深度探索C++对象模型》P40,由于其省略了其中c++标准的部分内容,因此很容易造成误解:

编译器隐式生成的构造函数都是 trivial constructor …..

事实上,描述中提到 default constructor 被隐式声明出来(满足语法需要),而该构造函数是否被编译器合成(实现或定义),取决于编译器是否需要在构造函数中做些额外工作,一个没有被合成的 default constructor 被视为 trivial constructor(这也是c++标准原话的意思),而当编译器在需要时合成了构造函数,那么该类构造函数将被视为 nontrivial。

另外,一个定义了 user-decalred constructor(用户定义的任何构造函数) 的类被视为具有 nontrivial constructor。

下面将着重讨论编译器隐式声明的构造函数在哪种情况下需要被合成(nontrivial),哪种情况下无需被合成(trivial):

阅读全文 »

消息编解码(或序列化)主要是将消息体由一些标准库容器或自定义的类型,转化成二进制流,方便网络传输。为了减少网络IO,编解码中也可能在存在数据”压缩和解压”,但这种压缩是针对于特定的数据类型,并不是针对于二进制流的。在NGServer的消息编解码中,并不涉及数据压缩。

一. 消息编码格式

NGServer的消息分为首部和消息体,首部共四个字节,包括消息长度(包括首部)和消息ID,各占两个字节。消息体为消息编码后的二进制数据。

在消息体中,针对于不同的数据类型而不同编码。对于POD类型,直接进行内存拷贝,对于非POD类型,如标准库容器,则需要自定义编码格式,以下是几种最常见的数据类型编码:

std::string 先写入字符串长度,占两个字节,再写入字符串内容。
std::vector 先写入vector的元素个数(占两个字节),在对其元素逐个递归编码(如果元素类型为string,则使用string的编码方式)。
std::list 编码方式与vector类似
T arr[N] 对于这种类型,不需要写入元素个数,因为在消息结构体中指出了固定长度N,因此可以通过模板推导得到N。所以递归写入N个元素T即可。对于简单数据类型T,如T为char时,可以通过模板特例化对其优化。

阅读全文 »

在服务器中,一般都有一个代表客户端或玩家的类,用来处理一些相关逻辑并保存必要数据。也就是NGServer中的Player类,在网络模型中,一般一个Player对应一次会话(Session),因此在很多服务器模型中,客户端类直接从Session类派生,这样客户端可以直接通过父类Session的接口发送数据,并且通过实现Sessoin的虚函数对数据进行处理。这种模型的好处在于简单,客户端类能够完全控制网络IO,并且对IO事件进行及时地处理。比如连接断开,那么客户端类可以通过实现Session的OnClose()函数完成一些业务逻辑上的处理,比如保存用户数据。而这种编程模型,将客户端和网络会话的耦合性提到了最高:Client is a Session。方便的同时,很大程度上限制了模块的可拓展性,比如客户端的断线重连,由于这种继承关系,导致Session在销毁的同时必然导致Client”逻辑上”的断线,这样玩家重连的时候,数据只能重新加载,建立新的Session和Client。这种情况还会发生在客户端异处登录时,原有客户端被挤下线的同时,逻辑上的数据也丢失了,而新的客户端将重新加载数据。除了断线重连之外,该模型还会造成不必要的编译依赖。因此我们需要将逻辑上的客户端和底层的网络Session解耦。

阅读全文 »

NGServer的核心概念便是服务(Service),它对逻辑层表现为一个线程,处理各种特定的相关业务。如日志服务(LogService),数据库服务(DBService),登录服务(LoginService)。服务之间通过消息进行交互。Service实际上并不是一个独立线程,Service与线程是一种”多对多”的关系。即所有的Service通过ServiceManager来管理,后者维护一个线程池,并将线程池与”服务池”以某种调度方式关联,让线程充分被利用。

下面由下至上对Service框架和运行机制简单阐述:

阅读全文 »

在网络编程模型中,一个Session代表一次会话,主要维护网络数据的发送和接收。对外提供发送数据和处理数据的接口。一个高效的Session主要通过缓冲和异步来提高IO效率。NGServer的Session运用双缓冲和boost::asio的异步机制,很好地做到了这一点。

一. 双缓冲

在网络IO中,读写线程的互斥访问一直都是一个关乎性能的大问题。为了减少互斥锁的使用,环形缓冲和双缓冲是常见的策略。NGServer使用后者作为消息和数据缓冲。
在NGServer MessageQueue.h中,定义了两种双缓冲:基于消息的MessageQueue和基于数据的ByteBuff。下面简要介绍ByteBuff类:

ByteBuff类的基本思想是通过两个缓冲区_buff_read和_buff_write来使读写分离。通过size_t Push(const char* data, size_t len)来写入数据:

阅读全文 »

NGServer是一个迷你型C++游戏服务器框架。Github地址:https://github.com/wudaijun/NGServer。

主要特性:

  • 框架用C++(11)和boost库实现。
  • 基于单进程多线程。
  • 框架屏蔽了多线程实现,上层体现为服务(Service),服务之间通过消息进行通信。
  • 有比较完善灵活的的消息回调和序列化机制,更方便地实现RPC。
阅读全文 »