服务器之家:专注于服务器技术及软件下载分享
分类导航

云服务器|WEB服务器|FTP服务器|邮件服务器|虚拟服务器|服务器安全|DNS服务器|服务器知识|

服务器之家 - 服务器技术 - 服务器知识 - 教你从头写游戏服务器框架

教你从头写游戏服务器框架

2019-09-16 21:01今日头条Java的小本家 服务器知识

由于“越通用的代码,就是越没用的代码”,所以在设计之初,我就认为应该使用分层的模式来构建整个系统。

教你从头写游戏服务器框架

由于“越通用的代码,就是越没用的代码”,所以在设计之初,我就认为应该使用分层的模式来构建整个系统。按照游戏服务器的一般需求划分,最基本的可以分为两层:

底层基础功能:包括通信、持久化等非常通用的部分,关注的是性能、易用性、扩展性等指标。

高层逻辑功能:包括具体的游戏逻辑,针对不同的游戏会有不同的设计。

我希望能有一个基本完整的“底层基础功能”的框架,可以被复用于多个不同的游戏。由于目标是开发一个 适合独立游戏开发 的游戏服务器框架。所以最基本的需求分析为:

功能性需求

并发:所有的服务器程序,都会碰到这个基本的问题:如何处理并发处理。一般来说,会有多线程、异步两种技术。多线程编程在编码上比较符合人类的思维习惯,但带来了“锁”这个问题。而异步非阻塞的模型,其程序执行的情况是比较简单的,而且也能比较充分的利用硬件性能,但是问题是很多代码需要以“回调”的形式编写,对于复杂的业务逻辑来说,显得非常繁琐,可读性非常差。虽然这两种方案各有利弊,也有人结合这两种技术希望能各取所长,但是我更倾向于基础是使用异步、单线程、非阻塞的调度方式,因为这个方案是最清晰简单的。为了解决“回调”的问题,我们可以在其上再添加其他的抽象层,比如协程或者添加线程池之类的技术予以改善。

通信:支持 请求响应 模式以及 通知 模式的通信(广播视为一种多目标的通知)。游戏有很多登录、买卖、打开背包之类的功能,都是明确的有请求和响应的。而大量的联机游戏中,多个客户端的位置、HP 等东西都需要经过网络同步,其实就是一种“主动通知”的通信方式。

持久化:可以存取 对象 。游戏存档的格式非常复杂,但其索引的需求往往都是根据玩家 ID 来读写就可以。在很多游戏主机如 PlayStation 上,以前的存档都是可以以类似“文件”的方式存放在记忆卡里的。所以游戏持久化最基本的需求,就是一个 key-value 存取模型。当然,游戏中还会有更复杂的持久化需求,比如排行榜、拍卖行等,这些需求应该额外对待,不适合包含在一个最基本的通用底层中。

缓存:支持远程、分布式的对象缓存。游戏服务基本上都是“带状态”的服务,因为游戏要求响应延迟非常苛刻,基本上都需要利用服务器进程的内存来存放过程数据。但是游戏的数据,往往是变化越快的,价值越低,比如经验值、金币、HP,而等级、装备等变化比较慢的,价值则越高,这种特征,非常适合用一个缓存模型来处理。

协程:可以用 C++ 来编写协程代码,避免大量回调函数分割代码。这个是对于异步代码非常有用的特性,能大大提高代码的可读性和开发效率。特别是把很多底层涉及IO的功能,都提供了协程化 API,使用起来就会像同步的 API 一样轻松惬意。

脚本:初步设想是支持可以用 Lua 来编写业务逻辑。游戏需求变化是出了名快的,用脚本语言编写业务逻辑正好能提供这方面的支持。实际上脚本在游戏行业里的使用非常广泛。所以支持脚本,也是一个游戏服务器框架很重要的能力。

其他功能:包括定时器、服务器端的对象管理等等。这些功能很常用,所以也需要包含在框架中,但已经有很多成熟方案,所以只要选取常见易懂的模型即可。比如对象管理,我会采用类似 Unity 的组件模型来实现。

非功能性需求

灵活性:支持可替换的通信协议;可替换的持久化设备(如数据库);可替换的缓存设备(如 memcached/redis);以静态库和头文件的方式发布,不对使用者代码做过多的要求。游戏的运营环境比较复杂,特别是在不同的项目之间,可能会使用不同的数据库、不同的通信协议。但是游戏本身业务逻辑很多都是基于对象模型去设计的,所以应该有一层能够基于“对象”来抽象所有这些底层功能的模型。这样才能让多个不同的游戏,都基于一套底层进行开发。

部署便利性:支持灵活的配置文件、命令行参数、环境变量的引用;支持单独进程启动,而无须依赖数据库、消息队列中间件等设施。一般游戏都会有至少三套运行环境,包括一个开发环境、一个内测环境、一个外测或运营环境。一个游戏的版本更新,往往需要更新多个环境。所以如何能尽量简化部署就成为一个很重要的问题。我认为一个好的服务器端框架,应该能让这个服务器端程序,在无配置、无依赖的情况下独立启动,以符合在开发、测试、演示环境下快速部署。并且能很简单的通过配置文件、或者命令行参数的不同,在集群化下的外部测试或者运营环境下启动。

性能:很多游戏服务器,都会使用异步非阻塞的方式来编程。因为异步非阻塞可以很好的提高服务器的吞吐量,而且可以很明确的控制多个用户任务并发下的代码执行顺序,从而避免多线程锁之类的复杂问题。所以这个框架我也希望是以异步非阻塞作为基本的并发模型。这样做还有另外一个好处,就是可以手工的控制具体的进程,充分利用多核 CPU 服务器的性能。当然异步代码可读性因为大量的回调函数,会变得很难阅读,幸好我们还可以用“协程”来改善这个问题。

扩展性:支持服务器之间的通信,进程状态管理,类似 SOA 的集群管理。自动容灾和自动扩容,其实关键点是服务进程的状态同步和管理。我希望一个通用的底层,可以把所有的服务器间调用,都通过一个统一的集权管理模型管理起来,这样就可以不再每个项目去关心集群间通信、寻址等问题。

一旦需求明确下来,基本的层级结构也可以设计了:

教你从头写游戏服务器框架

最后,整体的架构模块类似:

教你从头写游戏服务器框架

通信模块

对于通信模块来说,需要有灵活的可替换协议的能力,就必须按一定的层次进行进一步的划分。对于游戏来说,最底层的通信协议,一般会使用 TCP 和 UDP 这两种,在服务器之间,也会使用消息队列中间件一类通信软件。框架必须要有能同事支持这几通信协议的能力。故此设计了一个层次为: Transport

在协议层面,最基本的需求有“分包”“分发”“对象序列化”等几种需求。如果要支持“请求-响应”模式,还需要在协议中带上“序列号”的数据,以便对应“请求”和“响应”。另外,游戏通常都是一种“会话”式的应用,也就是一系列的请求,会被视为一次“会话”,这就需要协众需要有类似 Session ID 这种数据。为了满足这些需求,设计一个层次为: Protocol

拥有了以上两个层次,是可以完成最基本的协议层能力了。但是,我们往往希望业务数据的协议包,能自动化的成为编程中的 对象,所以在处理消息体这里,需要一个可选的额外层次,用来把字节数组,转换成对象。所以我设计了一个特别的处理器:ObjectProcessor ,去规范通信模块中对象序列化、反序列化的接口。

教你从头写游戏服务器框架

Transport

此层次是为了统一各种不同的底层传输协议而设置的,最基本应该支持 TCP 和 UDP 这两种协议。对于通信协议的抽象,其实在很多底层库也做的非常好了,比如 Linux 的 socket 库,其读写 API 甚至可以和文件的读写通用。C# 的 Socket 库在 TCP 和 UDP 之间,其 api 也几乎是完全一样的。但是由于作用游戏服务器,很多适合还会接入一些特别的“接入层”,比如一些代理服务器,或者一些消息中间件,这些 API 可是五花八门的。另外,在 html5 游戏(比如微信小游戏)和一些页游领域,还有用 HTTP 服务器作为游戏服务器的传统(如使用 WebSocket 协议),这样就需要一个完全不同的传输层了。

服务器传输层在异步模型下的基本使用序列,就是:

在主循环中,不断尝试读取有什么数据可读

如果上一步返回有数据到达了,则读取数据

读取数据处理后,需要发送数据,则向网络写入数据

根据上面三个特点,可以归纳出一个基本的接口:

classTransport{

public:

/**

*初始化Transport对象,输入Config对象配置最大连接数等参数,可以是一个新建的Config对象。

*/

virtualintInit(Config*config)=0;

/**

*检查是否有数据可以读取,返回可读的事件数。后续代码应该根据此返回值循环调用Read()提取数据。

*参数fds用于返回出现事件的所有fd列表,len表示这个列表的最大长度。如果可用事件大于这个数字,并不影响后续可以Read()的次数。

*fds的内容,如果出现负数,表示有一个新的终端等待接入。

*/

virtualintPeek(int*fds,intlen)=0;

/**

*读取网络管道中的数据。数据放在输出参数peer的缓冲区中。

*@parampeer参数是产生事件的通信对端对象。

*@return返回值为可读数据的长度,如果是0表示没有数据可以读,返回-1表示连接需要被关闭。

*/

virtualintRead(Peer*peer)=0;

/**

*写入数据,output_buf,buf_len为想要写入的数据缓冲区,output_peer为目标队端,

*返回值表示成功写入了的数据长度。-1表示写入出错。

*/

virtualintWrite(constchar*output_buf,intbuf_len,constPeer&output_peer)=0;

/**

*关闭一个对端的连接

*/

virtualvoidClosePeer(constPeer&peer)=0;

/**

*关闭Transport对象。

*/

virtualvoidClose()=0;

}

在上面的定义中,可以看到需要有一个 Peer 类型。这个类型是为了代表通信的客户端(对端)对象。在一般的 Linux 系统中,一般我们用 fd (File Description)来代表。但是因为在框架中,我们还需要为每个客户端建立接收数据的缓存区,以及记录通信地址等功能,所以在 fd 的基础上封装了一个这样的类型。这样也有利于把 UDP 通信以不同客户端的模型,进行封装。

///@brief此类型负责存放连接过来的客户端信息和数据缓冲区

classPeer{

public:

intbuf_size_;///<缓冲区长度

char*constbuffer_;///<缓冲区起始地址

intproduced_pos_;///<填入了数据的长度

intconsumed_pos_;///<消耗了数据的长度

intGetFd()const;

voidSetFd(intfd);///获得本地地址

conststructsockaddr_in&GetLocalAddr()const;

voidSetLocalAddr(conststructsockaddr_in&localAddr);///获得远程地址

conststructsockaddr_in&GetRemoteAddr()const;

voidSetRemoteAddr(conststructsockaddr_in&remoteAddr);

private:

intfd_;///<收发数据用的fd

structsockaddr_inremote_addr_;///<对端地址

structsockaddr_inlocal_addr_;///<本端地址

};

游戏使用 UDP 协议的特点:一般来说 UDP 是无连接的,但是对于游戏来说,是肯定需要有明确的客户端的,所以就不能简单用一个 UDP socket 的fd 来代表客户端,这就造成了上层的代码无法简单在 UDP 和 TCP 之间保持一致。因此这里使用 Peer 这个抽象层,正好可以接近这个问题。这也可以用于那些使用某种消息队列中间件的情况,因为可能这些中间件,也是多路复用一个 fd 的,甚至可能就不是通过使用 fd 的 API 来开发的。

对于上面的 Transport 定义,对于 TCP 的实现者来说,是非常容易能完成的。但是对于 UDP 的实现者来说,则需要考虑如何宠妃利用 Peer ,特别是 Peer.fd_ 这个数据。我在实现的时候,使用了一套虚拟的 fd 机制,通过一个客户端的 IPv4 地址到 int 的对应 Map ,来对上层提供区分客户端的功能。在 Linux 上,这些 IO 都可以使用 epoll 库来实现,在 Peek() 函数中读取 IO 事件,在 Read()/Write() 填上 socket 的调用就可以了。

另外,为了实现服务器之间的通信,还需要设计和 Tansport 对应的一个类型:Connector 。这个抽象基类,用于以客户端模型对服务器发起请求。其设计和 Transport 大同小异。除了 Linux 环境下的 Connecotr ,我还实现了在 C# 下的代码,以便用 Unity 开发的客户端可以方便的使用。由于 .NET 本身就支持异步模型,所以其实现也不费太多功夫。

/**

*@brief客户端使用的连接器类,代表传输协议,如TCP或UDP

*/

classConnector{

public:virtual~Connector(){}

/**

*@brief初始化建立连接等

*@paramconfig需要的配置

*@return0为成功

*/

virtualintInit(Config*config)=0;

/**

*@brief关闭

*/

virtualvoidClose()=0;

/**

*@brief读取是否有网络数据到来

*读取有无数据到来,返回值为可读事件的数量,通常为1

*如果为0表示没有数据可以读取。

*如果返回-1表示出现网络错误,需要关闭此连接。

*如果返回-2表示此连接成功连上对端。

*@return网络数据的情况

*/

virtualintPeek()=0;

/**

*@brief读取网络数

*读取连接里面的数据,返回读取到的字节数,如果返回0表示没有数据,

*如果buffer_length是0,也会返回0,

*@return返回-1表示连接需要关闭(各种出错也返回0)

*/

virtualintRead(char*ouput_buffer,intbuffer_length)=0;

/**

*@brief把input_buffer里的数据写入网络连接,返回写入的字节数。

*@return如果返回-1表示写入出错,需要关闭此连接。

*/

virtualintWrite(constchar*input_buffer,intbuffer_length)=0;

protected:

Connector(){}

};

Protocol

对于通信“协议”来说,其实包含了许许多多的含义。在众多的需求中,我所定义的这个协议层,只希望完成四个最基本的能力:

分包:从流式传输层切分出一个个单独的数据单元,或者把多个“碎片”数据拼合成一个完整的数据单元的能力。一般解决这个问题,需要在协议头部添加一个“长度”字段。

请求响应对应:这对于异步非阻塞的通信模式下,是非常重要的功能。因为可能在一瞬间发出了很多个请求,而回应则会不分先后的到达。协议头部如果有一个不重复的“序列号”字段,就可以对应起哪个回应是属于哪个请求的。

会话保持:由于游戏的底层网络,可能会使用 UDP 或者 HTTP 这种非长连接的传输方式,所以要在逻辑上保持一个会话,就不能单纯的依靠传输层。加上我们都希望程序有抗网络抖动、断线重连的能力,所以保持会话成为一个常见的需求。我参考在 Web 服务领域的会话功能,设计了一个 Session 功能,在协议中加上 Session ID 这样的数据,就能比较简单的保持会话。

分发:游戏服务器必定会包含多个不同的业务逻辑,因此需要多种不同数据格式的协议包,为了把对应格式的数据转发。

除了以上三个功能,实际上希望在协议层处理的能力,还有很多,最典型的就是对象序列化的功能,还有压缩、加密功能等等。我之所以没有把对象序列化的能力放在 Protocol 中,原因是对象序列化中的“对象”本身是一个业务逻辑关联性非常强的概念。在 C++ 中,并没有完整的“对象”模型,也缺乏原生的反射支持,所以无法很简单的把代码层次通过“对象”这个抽象概念划分开来。但是我也设计了一个 ObjectProcessor ,把对象序列化的支持,以更上层的形式结合到框架中。这个 Processor 是可以自定义对象序列化的方法,这样开发者就可以自己选择任何“编码、解码”的能力,而不需要依靠底层的支持。

至于压缩和加密这一类功能,确实是可以放在 Protocol 层中实现,甚至可以作为一个抽象层次加入 Protocol ,可能只有一个 Protocol 层不足以支持这么丰富的功能,需要好像 Apache Mina 这样,设计一个“调用链”的模型。但是为了简单起见,我觉得在具体需要用到的地方,再额外添加 Protocol 的实现类就好,比如添加一个“带压缩功能的 TLV Protocol 类型”之类的。

消息本身被抽象成一个叫 Message 的类型,它拥有“服务名字”“会话ID”两个消息头字段,用以完成“分发”和“会话保持”功能。而消息体则被放在一个字节数组中,并记录下字节数组的长度。

enumMessageType{

TypeError,///<错误的协议

TypeRequest,///<请求类型,从客户端发往服务器

TypeResponse,///<响应类型,服务器收到请求后返回

TypeNotice///<通知类型,服务器主动通知客户端

};

///@brief通信消息体的基类

///基本上是一个char[]缓冲区

structMessage{

public:

staticintMAX_MAESSAGE_LENGTH;

staticintMAX_HEADER_LENGTH;

MessageTypetype;///<此消息体的类型(MessageType)信息

virtual~Message();virtualMessage&operator=(constMessage&right);

/**

*@brief把数据拷贝进此包体缓冲区

*/

voidSetData(constchar*input_ptr,intinput_length);

///@brief获得数据指针

inlinechar*GetData()const{

returndata_;

}

///@brief获得数据长度

inlineintGetDataLen()const{

returndata_len_;

}

char*GetHeader()const;

intGetHeaderLen()const;

protected:

Message();

Message(constMessage&message);

private:

char*data_;//包体内容缓冲区

intdata_len_;//包体长度

};

根据之前设计的“请求响应”和“通知”两种通信模式,需要设计出三种消息类型继承于 Message,他们是:

Request 请求包

Response 响应包

Notice 通知包

Request 和 Response 两个类,都有记录序列号的 seq_id 字段,但 Notice 没有。Protocol 类就是负责把一段 buffer 字节数组,转换成 Message 的子类对象。所以需要针对三种 Message 的子类型都实现对应的 Encode() / Decode() 方法。

classProtocol{

public:

virtual~Protocol(){

}

/**

*@brief把请求消息编码成二进制数据

*编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了len,则返回-1表示错误。

*如果返回0,表示不需要编码,框架会直接从msg的缓冲区读取数据发送。

*@parambuf目标数据缓冲区

*@paramoffset目标偏移量

*@paramlen目标数据长度

*@parammsg输入消息对象

*@return编码完成所用的字节数,如果<0表示出错

*/

virtualintEncode(char*buf,intoffset,intlen,constRequest&msg)=0;

/**

*编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了len,则返回-1表示错误。

*如果返回0,表示不需要编码,框架会直接从msg的缓冲区读取数据发送。

*@parambuf目标数据缓冲区

*@paramoffset目标偏移量

*@paramlen目标数据长度

*@parammsg输入消息对象

*@return编码完成所用的字节数,如果<0表示出错

*/

virtualintEncode(char*buf,intoffset,intlen,constResponse&msg)=0;

/**

*编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了len,则返回-1表示错误。

*如果返回0,表示不需要编码,框架会直接从msg的缓冲区读取数据发送。

*@parambuf目标数据缓冲区

*@paramoffset目标偏移量

*@paramlen目标数据长度

*@parammsg输入消息对象

*@return编码完成所用的字节数,如果<0表示出错

*/

virtualintEncode(char*buf,intoffset,intlen,constNotice&msg)=0;

/**

*开始编码,会返回即将解码出来的消息类型,以便使用者构造合适的对象。

*实际操作是在进行“分包”操作。

*@parambuf输入缓冲区

*@paramoffset输入偏移量

*@paramlen缓冲区长度

*@parammsg_type输出参数,表示下一个消息的类型,只在返回值>0的情况下有效,否则都是TypeError

*@return如果返回0表示分包未完成,需要继续分包。如果返回-1表示协议包头解析出错。其他返回值表示这个消息包占用的长度。

*/

virtualintDecodeBegin(constchar*buf,intoffset,intlen,

MessageType*msg_type)=0;

/**

*解码,把之前DecodeBegin()的buf数据解码成具体消息对象。

*@paramrequest输出参数,解码对象会写入此指针

*@return返回0表示成功,-1表示失败。

*/

virtualintDecode(Request*request)=0;

/**

*解码,把之前DecodeBegin()的buf数据解码成具体消息对象。

*@paramrequest输出参数,解码对象会写入此指针

*@return返回0表示成功,-1表示失败。

*/

virtualintDecode(Response*response)=0;

/**

*解码,把之前DecodeBegin()的buf数据解码成具体消息对象。

*@paramrequest输出参数,解码对象会写入此指针

*@return返回0表示成功,-1表示失败。

*/

virtualintDecode(Notice*notice)=0;protected:

Protocol(){

}

};

这里有一点需要注意,由于 C++ 没有内存垃圾搜集和反射的能力,在解释数据的时候,并不能一步就把一个 char[] 转换成某个子类对象,而必须分成两步处理。

先通过 DecodeBegin() 来返回,将要解码的数据是属于哪个子类型的。同时完成分包的工作,通过返回值来告知调用者,是否已经完整的收到一个包。

调用对应类型为参数的 Decode() 来具体把数据写入对应的输出变量。

对于 Protocol 的具体实现子类,我首先实现了一个 LineProtocol ,是一个非常不严谨的,基于文本ASCII编码的,用空格分隔字段,用回车分包的协议。用来测试这个框架是否可行。因为这样可以直接通过 telnet 工具,来测试协议的编解码。然后我按照 TLV (Type Length Value)的方法设计了一个二进制的协议。大概的定义如下:

协议分包: [消息类型:int:2] [消息长度:int:4] [消息内容:bytes:消息长度]

消息类型取值:

0x00 Error

0x01 Request

0x02 Response

0x03 Notice

教你从头写游戏服务器框架

一个名为 TlvProtocol 的类型完成对这个协议的实现。

Processor

处理器层是我设计用来对接具体业务逻辑的抽象层,它主要通过输入参数 Request 和 Peer 来获得客户端的输入数据,然后通过 Server 类的 Reply()/Inform() 来返回 Response 和 Notice 消息。实际上 Transport 和 Protocol 的子类们,都属于 net 模块,而各种 Processor 和 Server/Client 这些功能类型,属于另外一个 processor 模块。这样设计的原因,是希望所有 processor 模块的代码单向的依赖 net 模块的代码,但反过来不成立。

Processor 基类非常简单,就是一个处理函数回调函数入口 Process():

///@brief处理器基类,提供业务逻辑回调接口

classProcessor{

public:

Processor();

virtual~Processor();

/**

*初始化一个处理器,参数server为业务逻辑提供了基本的能力接口。

*/

virtualintInit(Server*server,Config*config=NULL);

/**

*处理请求-响应类型包实现此方法,返回值是0表示成功,否则会被记录在错误日志中。

*参数peer表示发来请求的对端情况。其中Server对象的指针,可以用来调用Reply(),

*Inform()等方法。如果是监听多个服务器,server参数则会是不同的对象。

*/

virtualintProcess(constRequest&request,constPeer&peer,

Server*server);

/**

*关闭清理处理器所占用的资源

*/

virtualintClose();

};

设计完 Transport/Protocol/Processor 三个通信处理层次后,就需要一个组合这三个层次的代码,那就是 Server 类。这个类在 Init() 的时候,需要上面三个类型的子类作为参数,以组合成不同功能的服务器,如:

TlvProtocoltlv_protocol;//TypeLengthValue格式分包协议,需要和客户端一致

TcpTransporttcp_transport;//使用TCP的通信协议,默认监听0.0.0.0:6666

EchoProcessorecho_processor;//业务逻辑处理器

Serverserver;//DenOS的网络服务器主对象

server.Init(&tcp_transport,&tlv_protocol,&echo_processor);//组装一个游戏服务器对象:TLV编码、TCP通信和回音服务

Server 类型还需要一个 Update() 函数,让用户进程的“主循环”不停的调用,用来驱动整个程序的运行。这个 Update() 函数的内容非常明确:

检查网络是否有数据需要处理(通过 Transport 对象)

有数据的话就进行解码处理(通过 Protocol 对象)

解码成功后进行业务逻辑的分发调用(通过 Processor 对象)

另外,Server 还需要处理一些额外的功能,比如维护一个会话缓存池(Session),提供发送 Response 和 Notice 消息的接口。当这些工作都完成后,整套系统已经可以用来作为一个比较“通用”的网络消息服务器框架存在了。剩下的就是添加各种 Transport/Protocol/Processor 子类的工作。

classServer{

public:

Server();

virtual~Server();

/**

*初始化服务器,需要选择组装你的通信协议链

*/

intInit(Transport*transport,Protocol*protocol,Processor*processor,Config*config=NULL);

/**

*阻塞方法,进入主循环。

*/

voidStart();

/**

*需要循环调用驱动的方法。如果返回值是0表示空闲。其他返回值表示处理过的任务数。

*/

virtualintUpdate();

voidClosePeer(Peer*peer,boolis_clear=false);//关闭当个连接,is_clear表示是否最终整体清理

/**

*关闭服务器

*/

voidClose();

/**

*对某个客户端发送通知消息,

*参数peer代表要通知的对端。

*/

intInform(constNotice¬ice,constPeer&peer);

/**

*对某个SessionID对应的客户端发送通知消息,返回0表示可以发送,其他值为发送失败。

*此接口能支持断线重连,只要客户端已经成功连接,并使用旧的SessionID,同样有效。

*/

intInform(constNotice¬ice,conststd::string&session_id);

/**

*对某个客户端发来的Request发回回应消息。

*参数response的成员seqid必须正确填写,才能正确回应。

*返回0成功,其它值(-1)表示失败。

*/

intReply(Response*response,constPeer&peer);

/**

*对某个SessionID对应的客户端发送回应消息。

*参数response的seqid成员系统会自动填写会话中记录的数值。

*此接口能支持断线重连,只要客户端已经成功连接,并使用旧的SessionID,同样有效。

*返回0成功,其它值(-1)表示失败。

*/

intReply(Response*response,conststd::string&session_id);

/**

*会话功能

*/

Session*GetSession(conststd::string&session_id="",booluse_this_id=false);

Session*GetSessionByNumId(intsession_id=0);

boolIsExist(conststd::string&session_id);

};

有了 Server 类型,肯定也需要有 Client 类型。而 Client 类型的设计和 Server 类似,但就不是使用 Transport 接口作为传输层,而是 Connector 接口。不过 Protocol 的抽象层是完全重用的。Client 并不需要 Processor 这种形式的回调,而是直接传入接受数据消息就发起回调的接口对象 ClientCallback。

classClientCallback{

public:

ClientCallback(){

}

virtual~ClientCallback(){

//Donothing

}

/**

*当连接建立成功时回调此方法。

*@return返回-1表示不接受这个连接,需要关闭掉此连接。

*/

virtualintOnConnected(){

return0;

}

/**

*当网络连接被关闭的时候,调用此方法

*/

virtualvoidOnDisconnected(){//Donothing

}

/**

*收到响应,或者请求超时,此方法会被调用。

*@paramresponse从服务器发来的回应

*@return如果返回非0值,服务器会打印一行错误日志。

*/

virtualintCallback(constResponse&response){

return0;

}

/**

*当请求发生错误,比如超时的时候,返回这个错误

*@paramerr_code错误码

*/

virtualvoidOnError(interr_code){

WARN_LOG("Therequestistimeout,err_code:%d",err_code);

}

/**

*收到通知消息时,此方法会被调用

*/

virtualintCallback(constNotice¬ice){

return0;

}

/**

*返回此对象是否应该被删除。此方法会被在Callback()调用前调用。

*@return如果返回true,则会调用delete此对象的指针。

*/

virtualboolShouldBeRemoved(){

returnfalse;

}

};

classClient:publicUpdateable{

public:

Client();virtual~Client();

/**

*连接服务器

*@paramconnector传输协议,如TCP,UDP...

*@paramprotocol分包协议,如TLV,Line,TDR...

*@paramnotice_callback收到通知后触发的回调对象,如果传输协议有“连接概念”(如TCP/TCONND),建立、关闭连接时也会调用。

*@paramconfig配置文件对象,将读取以下配置项目:MAX_TRANSACTIONS_OF_CLIENT客户端最大并发连接数;BUFFER_LENGTH_OF_CLIENT客户端收包缓存;CLIENT_RESPONSE_TIMEOUT客户端响应等待超时时间。

*@return返回0表示成功,其他表示失败

*/

intInit(Connector*connector,Protocol*protocol,

ClientCallback*notice_callback=NULL,Config*config=NULL);

/**

*callback参数可以为NULL,表示不需要回应,只是单纯的发包即可。

*/

virtualintSendRequest(Request*request,ClientCallback*callback=NULL);

/**

*返回值表示有多少数据需要处理,返回-1为出错,需要关闭连接。返回0表示没有数据需要处理。

*/

virtualintUpdate();

virtualvoidOnExit();

voidClose();

Connector*connector();

ClientCallback*notice_callback();

Protocol*protocol();

};

至此,客户端和服务器端基本设计完成,可以直接通过编写测试代码,来检查是否运行正常。

延伸 · 阅读

精彩推荐
  • 服务器知识Cisco刀片服务器产品深度解析

    Cisco刀片服务器产品深度解析

    随着云计算的发展,新的数据中心建设和计算平台的架构发生了显著的变化。业界的厂商纷纷在推出融合架构和集成系统。过去,存储、计算与网络都是互相割裂的产品,但是,在新的虚拟化技术中,他们变成了底层的资源,可以...

    网络5002019-06-15
  • 服务器知识香港服务器租用与国内服务器租用相比都有哪些差异

    香港服务器租用与国内服务器租用相比都有哪些差异

    很多企业在搭建网站的时候,都会纠结一个问题,应该选择国内的服务器还是 香港服务器 。其实无论是香港服务器还是国内的服务器,都会有很多企业使用。但两者还是会存在备案、网络、访问等方面的差异。香港服务器有什么...

    服务器之家2152019-05-24
  • 服务器知识服务器设计方案之应用限流

    服务器设计方案之应用限流

    在一个高并发系统中对流量的把控是非常重要的,当巨大的流量直接请求到我们的服务器上没多久就可能造成接口不可用,不处理的话甚至会造成整个应用不可用。...

    优普学堂5172019-05-28
  • 服务器知识42U标准机柜尺寸是多少?42U机柜尺寸

    42U标准机柜尺寸是多少?42U机柜尺寸

    经常有客户询问我们42U标准机柜尺寸是多少?42U机柜尺寸多少?这些一直都是有机柜租用需求的用户比较关心和关注的问题,今天小编就给大家解答一下。 首先42U标准机柜尺寸就是指容量为42U的机柜尺寸,时常有客户上来就问42U机柜多...

    网络3942019-06-19
  • 服务器知识ROS软路由搭建DHCP服务器

    ROS软路由搭建DHCP服务器

    ROS软路由搭建DHCP服务器,为局域网用户自动分配ip地址,不需要一台一台手动设置ip ROS软路由搭建DHCP服务器 首先,在ip--pool创建dhcp服务器所使用的地址池 172.22.26.2-172.22.26.250,剩下的ip地址用作网关、服务器ip等。 在ip--address目录...

    百度经验4212019-06-05
  • 服务器知识电商平台为什么要使用独立服务器托管?

    电商平台为什么要使用独立服务器托管?

    随着互联网的发展,电商平台已经成为产品的重要渠道之一,电商行业也是各行各业的发展趋势了。那么搭建电商平台自然需要服务器和空间作为支撑。一般来说,电商行业的大多平台都会选择...

    服务器之家5302019-05-24
  • 服务器知识是自购服务器、还是租用虚拟机、亦或云托管?

    是自购服务器、还是租用虚拟机、亦或云托管?

    本文通过对各种调查结论的分析,向您介绍了自购服务器、租用虚拟机、以及云托管模式的优缺点,希望能够帮助您做出正确的选择。...

    51CTO962019-05-22
  • 服务器知识Linux配置ntp时间服务器(全)

    Linux配置ntp时间服务器(全)

    时间服务器 作用: 大数据产生与处理系统是各种计算设备集群的,计算设备将统一、同步的标准时间用于 记录各种事件发生时序 , 如E-MAIL信息、文件创建和访问时间、数据库处理时间等。...

    大墨垂杨3642019-05-22
北京塞车全天计划精准版