近期在开发一个跨平台的网络库,其目的是为了封装网络底层细节,对外提供一个简易的接口,在这里记载一下设计思路。
网络游戏服务器通常需要2种网络IO:一种是面向大量客户端连接的高连接数的网络IO,一种是服务器间的低连接数,高吞吐量的网络IO,后一种网络IO比较简单,可采取阻塞或者异步的网络IO模型。第一种通常需要采用IOCP,epoll等支持高并发的网络模型。
开发目标
1:封装网络底层细节及复杂性
2:提供足够简单且灵活的接口
3:至少能在windows和linux下使用
几个关键设计的考虑
1:跨平台
要满足跨平台的需求,可考虑以下3种方式
a:编写2套系统,提供一致的接口,用#ifdef等预编译指令识别操作系统
b:在ACE框架下进行开发
c:在boost asio库上进行开发
经过考虑,我采用了第3种方案,原因是采用第一种方案开发成本过高,在设计,编码和调试上需要投入较多的时间,而ACE过于庞大,抽象层次太高,学习曲线高,并且难以调试和维护,而boost asio正好合适,整个库的源代码也就1万多行,结合其他的boost库,节省了大部分跨平台工作。(ps. asio是一个轻量级的高质量c++库,由boost 1.35引入,封装了iocp,epoll,kqueue等网络模型,并提供了一致的proactor模式)
2:线程
相对于游戏逻辑,网络层已经非常容易采用多线程实现了,这里没有特殊的考虑,采用传统的资源加锁的方式。发送和接收数据均采用线程池管理。
3:内存管理
主要体现在缓冲区的设计上面,我在第一次思考这个问题的时候,快速的编写了一个var_buffer类,该类的功能类似std::vector等容器可动态增长,并在一定的时间内没使用后收回未用的内存,后来改成了环形缓冲区,同样支持动态增长和内存回收,这里有个小插曲,当想到用环形缓冲区时,首先想到的是boost::circluar_buffer,但是当打开boost的文档,不到1分钟就被推翻了:boost::circluar_buffer被设计成泛型容器,若采用boost::circluar_buffer<char>,每次内存拷贝都循环push进去,对效率的影响很大。
4:发送策略
对网络库来说,receive是很自然的事情,而send就要麻烦一些。经过仔细的思考,我的发送策略如下:
第一步:存放数据——客户程序调用网络库的send接口时,直接把数据传输到session的发送缓冲区,然后通知发送线程有数据需要发送,send函数(调用线程中)返回
第二步:处理数据——然后发送线程取出数据,进行加密,压缩后再放入另外一个缓冲区B1。这里加密压缩等需要消耗一定的cpu资源的操作切到了发送线程池中处理。
第三步:发送数据——如果该session目前没有数据发送,则投递发送请求,发送缓冲区B1中的数据
这里有2个问题:
1) 如果在B1缓冲区发送过程中,又有新的发送请求,如何处理
2) B1缓冲区在发送过程中,如何保持B1的有效性,又如何在发送数据完成后删除该缓冲区
对于第一个问题,采用了一个小技巧:引入缓冲区B2,且B1,B2形成一个只存在2个元素的queue,在发送数据的时候,先冲queue中pop出一个buffer,再进行发送,若在发送过程中又有请的发送请求,则写入到queue.front()中,这样数据始终是压入到queue.front()的缓冲区。当B1发送完成后,再把B1 push到queue中,这样就形成了一个循环。
对于第2个问题,为保证buffer的有效性,采用了带引用计数的智能指针管理buffer,在投递发送请求的时候,把该智能指针作为一个参数传递进去,在发送处理完成后再push到上述的queue中,这样buffer只会在连接断开之后才会被清除,形成了循环利用。
5:数据接收策略
数据接收策略采取了常用的两次读取策略,先接收一个固定长度的header,然后从该header中解析出包长度信息,再投递一个recv请求读取body.另外,所有需要反馈给用户的网络响应都被封装成消息(包括新连接建立,断开连接等事件),串行化到消息队列中,库的使用者调用一个handle_event的api从队列中取出一个消息
6:服务器主动断开连接
服务器主动断开连接,要做以下几个保证:
1) 在接口函数disconnect返回之后,不会再收到该连接的消息
2) 缓存在网络库中的剩余数据会发送到客户端。
7:包头结构
包头需要包含以下信息:
1:包长度
2:包序号
3:加密信息——通常是密钥
4:压缩信息
5:校验信息——防止包被纂改
8:二进制接口设计
结合项目情况,考虑提供2套接口,一套接口针对c++程序员,借鉴了一些com的设计,另外提供一套c api,其目的是可用脚本调用。