C++网游服务端开发(一):又无奈的重复造了个轮子,一个底层网络库
最近公司游戏准备内测,但是原来的网络层写得很是混乱,搞的已经无法维护,结果不得不重写一遍。这坨烂摊子交到了我的手上,花了两个星期重写了公司的网络层,登录验证以及网关服务器。内测目前表现十分稳定。
我是及其反对重复造轮子的,所以一开始主张使用boost asio来做我们的网络层(asio写得非常优美,我自认为自己再怎么写也是无法达到这种水准的)。结果却遭到了一些人的反对,其观点无非是boost太庞大之类。既然如此,还是服从上级吧,不过心中仍然在想,大?难道服务器硬盘不够吗?
编写一个网络库,很多地方需要考虑和选择,我列举几个点:
1. proactor还是reactor设计?
asio使用的是proactor设计,windows完成端口和epoll统一成异步api,我感觉设计的非常优美。但是组内的人并不喜欢这种function bind回调的模式。结果我只能使用reactor的设计。
2.使用什么手段来管理应用层buffer,buffer list? ringbuffer?
在我们原来的设计中,采用预先分配一块ringbuff的方式来做应用层缓冲区,其好处就是可以在Read的过程中减少一次内存拷贝,其缺点也很明显,ringbuffer采用mmap分配,需要占用一个描述符,并且大小是固定的,这样逻辑封包就不能超过ringbuffer的最大长度。具体怎么实现大家可以google之。总之我认为,网络层的设计不应该限制逻辑怎么实现。所以我重写网络层放弃了这个设计。
我采用了buffer list来管理应用层buffer,应用层buff由很多个固定大小的块连接,每块大小为8K。应用层数据往buffer head块写,写满之后new一块新的buffer块,header指向新的块继续写。读也一样,读在tail端读,读完之后,delete掉尾端那块,把tail指向tail-1块,继续读。数据处理读写都经过两次copy,每次read和write能尽量操作更多数据,减少read write次数
a. 读: tcp缓冲->应用层缓冲区->实际的包
b. 写: 实际包->应用层缓冲区->tcp缓冲区
例如,某一帧,服务端向逻辑端要发送1000个封包30K的数据,那我应用层将有4个buff块,只需要4次write即可将数据写入tcp缓冲区。
3.使用epoll et 还是 lt?
这个东西被很多人都说烂了,但大多数都不够细致,并没说到重点。实际编写过程中很多小细节需要注意。需要注意的是写事件。
a. 对于et来说,应用层向tcp缓冲区写,有可能应用层数据写完了,但是tcp缓冲没有写到EAGAIN事件,那么此时需要在应用层做个标记,表明tcp缓冲区是可写的,否则,由于et是只触发一次,应用层就再也不会被通知缓冲区可写了。
b. 对于lt来说,应用层确实会每次通知可写事件,问题在于,如果应用层没数据需要往Tcp缓冲区写的话,epoll还是会不停的通知你可写,这时候需要把描述符移出epoll,避免多次无效的通知
设计过程中,由于需要兼容一些以前的东西,某些地方不得不作出一些牺牲了,例如必须重用我们原来的epoll dispatch类做数据驱动.当然也有些便利的东西,例如原来server是个单线程无阻塞的网络库,那我重写自然也不需要考虑多线程了。最后各种妥协,完成了如下设计:
稍微解释一下,Descriptor类是对描述符的封装,两个子类TSocket是用于收发数据,TAcceptor用于处理连接事件。TSession里面封装了一些通用的session处理逻辑。提供两个虚函数Read和Write用于往应用层缓冲区读写数据。TServer封装了通用的Server逻辑,例如超时无数据则断开session。有了TSession和TServer之后,要写一个server就很简单了,直接继承这两个类即可。