分类: C/C++, Linux

关于Linux环境C/C++网络框架的一点思考

最近又看了一个网络框架的源码,和之前看过的比起来,应该说是各有特色,互有所长。在这个全民写框架的时代,可能是因为框架(Framework)听起来逼格比较高,所以大家都乐于去写一个自己的“框架”,那么,一个合格的网络框架究竟应该是什么样的?我们又该从何下手?

什么是网络框架

网络框架,顾名思义,是给网络应用程序使用的框架,本文中指代在Linux环境下使用C/C++编写的网络服务器框架。用户在使用框架时应该能够做到在对底层网络完全不了解或者所知很少的情况下,轻松实现自己所需要的后台网络服务应用。

原材料

听上去似乎很神奇,但实际上网络框架所要完成的只有一件事情——封装。网络框架所做的事情就是将Linux提供的底层网络API进行封装,向用户提供一套没有网络细节的接口。

在Linux中,我们所用到的网络接口就是《UNIX环境网络编程》中所提到的基本通信接口,下面借用一个非常老套的图来说明这些接口:

Linux网络API

Linux网络API

图的左侧是TCP连接的通信过程,右侧则是UDP连接的通信过程,你可以在任何一本关于网络编程的书或是网站上查到非常详尽的资料,当然最推荐的依然是《UNIX网络编程》以及Linux man 工具。

在Linux中有了这些网络API,你就可以在TCP、UDP通信上为所欲为了,除了网络之外,作为一个Linux网络服务器框架,无可避免的还需要封装一些其他的API,比如文件读写、进程相关API(如 fork )、线程库pthread、信号处理、进程间通信IPC等等,这些工作非常零碎,但幸好在著作《UNIX环境高级编程》中都有详尽的描述。

也就是说,如果你想要在Linux上实现一个C/C++的网络框架,必须要首先了解《UNIX环境高级编程》和《UNIX环境网络编程》这两本著作中的内容,如果连可以使用的原材料都不了解,又该如何去进行封装呢?

目标

在了解了能够使用的工具后,具体的封装就完全是一个见仁见智的过程了,对用户接口的设计并没有所谓的规范,也并不是一成不变的。每个框架针对自己所面向的应用类别和用户,都会做出最为合适的接口设计。

举例来说,有的框架面向通用的网络应用编写,向用户提供多个类似回调函数的接口,即当一个TCP连接创建后,执行用户提供的某个函数;当数据到来,执行用户提供的函数等等,做到了网络接口细节的隐藏。而有的框架则面向更加特别的应用场景,在TCP之上构造应用层协议,通过复用单TCP连接实现多个对话并发,并且封装多线程处理回调函数,做到了非常高的处理性能。

这些接口的设计并没有优劣之分,都是为自己所面向的应用类型特别设计的,所以做网络框架的第一步必须认识清楚自己所要服务的应用是什么样的,这样才能做出真正有价值的网络框架。

这很难么?

那么,在有了明确的设计目标后,实现这样的网络封装的难度又如何呢?

仅仅以实现一个网络框架作为要求的话,是很容易的,只要修过计算机网络的学生应该都可以完成这样的工作。但是,想要实现一个好的网络框架,却是一件非常困难的工作(似乎在计算机领域很多事情都是这样的),下面就来谈谈在实现一个网络框架时需要考虑的一些问题。

稳定

首先第一点必须是稳定。

应用开发者对于网络框架的期望值很高,不难想象当他们发现服务崩溃而原因居然是框架bug导致其心情会是多么的崩溃,所以一个实现良好的网络框架,稳定是重中之重。如果不能稳定运行,其他什么高大上的功能都无力回天。

高性能

第二点才是性能。

应用开发者期望可以通过简单的配置就可以做出承载很大压力的网络服务,这样的责任自然而然的转嫁给了框架开发者。如何为应用开发者提供良好的性能保证是框架设计初期就一定要考虑好的。包括框架是否该采用多进程、多线程甚至协程来处理请求?框架该采用怎样的I/O模型?等等这些问题是决定一个网络框架性能的重要因素。

举例来说:epoll想必大家都耳熟能详了,作为Linux独家提供的基于事件响应的I/O多路复用模型,epoll在后台开发尤其是网络应用开发中早已大行其道。假如你准备在你的网络框架中采用epoll来监听TCP连接,并且采用了多进程的模型来加强对CPU的利用,那么,该如何处理epoll造成的惊群问题?又比如说, epoll_create 该写在 fork 之前还是之后呢?

因为对性能的苛求,看似无关紧要的细节在网络框架的开发中被极大的放大了。

内存管理

C/C++似乎永远逃脱不了管理内存的诅咒,为了应用开发者的幸福,框架开发者必须尽可能的承担内存管理的职责。

在网络编程中的 readwritesendtorecv 等等接口中,无一例外都需要提供用户空间缓冲区,每条TCP连接都需要这样的空间来保证数据的独立性。显然作为框架开发者不能将缓冲区的管理交给应用开发者,也不可能频繁的为每一条连接申请、释放内存。因此,内存池似乎成了每个网络框架中唯一没有变化的设计,其中的差别在于内存池中内存的管理算法(伙伴系统的上镜率很高)和接口的实现细节。

其他

除了对基础网络需求的封装,针对特殊的应用场景网络框架中也常常会封装一些其他通用的工具,比如Log模块,自定义的哈希表模块以及数据结构序列化、反序列化的模块等等。

总之

实现一个网络框架并不难,但实现一个好的网络框架很难。

如何实现一个好的网络框架基于对应用场景的深刻理解之上,再加上非常扎实的基本功方能完成。在没有搞明白现有的网络框架为何不能满足自己的需求之前,千万不要去写自己的网络框架,否则只会是无用功。

以上是我在学习网络框架源码过程中产生的一点粗糙的思考,分享给大家。