C++高性能服务器网络框架设计与实现

本文探讨如何设计和实现C++的高性能服务器网络框架,重点在于网络通信组件和服务器程序结构。通过讲解如何处理网络事件、避免无用功、利用IO复用技术,阐述了服务器高效处理高并发的策略。文章还讨论了协议设计、服务器程序结构的组织,以及流量控制和缓存技术在服务器开发中的应用。

这篇文章将从两个方面来介绍,一个是服务器中的基础的网络通信部件;另外一个是,如何利用这些基础通信部件整合成一个完整的高效的服务器框架。注意:本文以下内容中的客户端是相对概念,指的是连接到当前讨论的服务程序的终端,所以这里的客户端既可能是我们传统意义上的客户端程序,也可能是连接该服务的其他服务器程序。

一、网络通信部件

我们先从服务程序的网络通信部件开始介绍,需要解决的问题:

既然是服务器程序肯定会涉及到网络通信部分,那么服务器程序的网络通信模块要解决哪些问题?目前,网络上有很多网络通信框架,如libevent、boost asio、ACE,但都网络通信的常见的技术手段都大同小异,至少要解决以下问题:

  1. 如何检测有新客户端连接?
  2. 如何接受客户端连接?
  3. 如何检测客户端是否有数据发来?
  4. 如何收取客户端发来的数据?
  5. 如何检测连接异常?发现连接异常之后,如何处理?
  6. 如何给客户端发送数据?
  7. 如何在给客户端发完数据后关闭连接?

稍微有点网络基础的人,都能回答上面说的其中几个问题,比如接收客户端连接用socket API的accept函数,收取客户端数据用recv函数,给客户端发送数据用send函数,检测客户端是否有新连接和客户端是否有新数据可以用IO multiplexing技术(IO复用)的select、poll、epoll等socket API。确实是这样的,这些基础的socket API构成了服务器网络通信的地基,不管网络通信框架设计的如何巧妙,都是在这些基础的socket API的基础上构建的。但是如何巧妙地组织这些基础的socket API,才是问题的关键。我们说服务器很高效,支持高并发,实际上只是一个技术实现手段,不管怎样,从软件开发的角度来讲无非就是一个程序而已,所以,只要程序能最大可能地满足“尽量减少等待或者不等待”这一原则就是高效的,也就是说高效不是“忙的忙死,闲的闲死”,而是大家都可以闲着,但是如果有活要干,大家尽量一起干,而不是一部分忙着依次做事情123456789,另外一部分闲在那里无所事事。说的可能有点抽象,下面我们来举一些例子具体来说明一下。

例如:

默认情况下,recv函数如果没有数据的时候,线程就会阻塞在那里;

默认情况下,send函数,如果tcp窗口不是足够大,数据发不出去也会阻塞在那里;

connect函数默认连接另外一端的时候,也会阻塞在那里;

又或者是给对端发送一份数据,需要等待对端回答,如果对方一直不应答,当前线程就阻塞在这里。

以上都不是高效服务器的开发思维方式,因为上面的例子都不满足“尽量减少等待”的原则,为什么一定要等待呢?有没用一种方法,这些过程不需要等待,最好是不仅不需要等待,而且这些事情完成之后能通知我。这样在这些本来用于等待的cpu时间片内,我就可以做一些其他的事情。有,也就是我们下文要讨论的IO Multiplexing技术(IO复用技术)。

资料领取直通车:大厂面试题锦集+视频教程icon-default.png?t=M85Bhttps://link.zhihu.com/?target=https%3A//docs.qq.com/doc/DTlhVekRrZUdDUEpy

免费学习网站:C/C++Linux服务器开发/后台架构师icon-default.png?t=M85Bhttps://link.zhihu.com/?target=https%3A//ke.qq.com/course/417774%3FflowToken%3D1028592

几种IO复用机制的比较

目前windows系统支持select、WSAAsyncSelect、WSAEventSelect、完成端口(IOCP),linux系统支持select、poll、epoll。这里我们不具体介绍每个具体的函数的用法,我们来讨论一点深层次的东西,以上列举的API函数可以分为两个层次:

层次一: select和poll

层次二: WSAAsyncSelect、WSAEventSelect、完成端口(IOCP)、epoll

为什么这么分呢?先来介绍第一层次,select和poll函数本质上还是在一定时间内主动去查询socket句柄(可能是一个也可能是多个)上是否有事件,比如可读事件,可写事件或者出错事件,也就是说我们还是需要每隔一段时间内去主动去做这些检测,如果在这段时间内检测出一些事件来,我们这段时间就算没白花,但是倘若这段时间内没有事件呢?我们只能是做无用功了,说白了,还是在浪费时间,因为假如一个服务器有多个连接,在cpu时间片有限的情况下,我们花费了一定的时间检测了一部分socket连接,却发现它们什么事件都没有,而在这段时间内我们却有一些事情需要处理,那我们为什么要花时间去做这个检测呢?把这个时间用在做我们需要做的事情不好吗?所以对于服务器程序来说,要想高效,我们应该尽量避免花费时间主动去查询一些socket是否有事件,而是等这些socket有事件的时候告诉我们去处理。这也就是层次二的各个函数做的事情,它们实际相当于变主动查询是否有事件为当有事件时,系统会告诉我们,此时我们再去处理,也就是“好钢用在刀刃”上了。只不过层次二的函数通知我们的方式是各不相同,比如WSAAsyncSelect是利用windows窗口消息队列的事件机制来通知我们设定的窗口过程函数,IOCP是利用GetQueuedCompletionStatus返回正确的状态,epoll是epoll_wait函数返回而已。

例如,connect函数连接另外一端,如果用于连接socket是非阻塞的,那么connect虽然不能立刻连接完成,但是也是会立刻返回,无需等待,等连接完成之后,WSAAsyncSelect会返回FD_CONNECT事件告诉我们连接成功,epoll会产生EPOLLOUT事件,我们也能知道连接完成。甚至socket有数据可读时,WSAAsyncSelect产生FD_READ事件,epoll产生EPOLLIN事件,等等。所以有了上面的讨论,我们就可以得到网络通信检测可读可写或者出错事件的正确姿势。这是我这里提出的第二个原则:尽量减少做无用功的时间。这个在服务程序资源够用的情况下可能体现不出来什么优势,但是如果有大量的任务要处理,这里就成了性能的一个瓶颈。

检测网络事件的正确姿势

根据上面的介绍,第一,为了避免无意义的等待时间,第二,不采用主动查询各个socket的事件,而是采用等待操作系统通知我们有事件的状态的策略。我们的socket都要设置成非阻塞的。在此基础上我们回到栏目(一)中提到的七个问题:

  • 如何检测有新客户端连接?
  • 如何接受客户端连接?

默认accept函数会阻塞在那里,如果epoll检测到侦听socket上有EPOLLIN事件,或者WSAAsyncSelect检测到有FD_ACCEPT事件,那么就表明此时有新连接到来,这个时候调用accept函数,就不会阻塞了。当然产生的新socket你应该也设置成非阻塞的。这样我们就能在新socket上收发数据了。

  • 如何检测客户端是否有数据发来?
  • 如何收取客户端发来的数据?

同理,我们也应该在socket上有可读事件的时候才去收取数据,这样我们调用recv或者read函数时不用等待,至于一次性收多少数据好呢?我们可以根据自己的需求来决定,甚至你可以在一个循环里面反复recv或者read,对于非阻塞模式的socket,如果没有数据了,recv或者read也会立刻返回,错误码EWOULDBLOCK会表明当前已经没有数据了。示例:

bool CIUSocket::Recv()
{
int nRet = 0;
while(true)
{
char buff[512];
nRet = ::recv(m_hSocket, buff, 512, 0);
if(nRet == SOCKET_ERROR) //一旦出现错误就立刻关闭Socket
{
if (::WSAGetLastError() == WSAEWOULDBLOCK)
break;
else
return false;
}
else if(nRet < 1)
return false;
m_strRecvBuf.append(buff, nRet);
::Sleep(1);
}
return true;
}

如何检测连接异常?发现连接异常之后,如何处理?

同样当我们收到异常事件后例如EPOLLERR或关闭事件FD_CLOSE,我们就知道了有异常产生,我们对异常的处理一般就是关闭对应的socket。另外,如果send/recv或者read/write函数对一个socket进行操作时,如果返回0,那说明对端已经关闭了socket,此时这路连接也没必要存在了,我们也可以关闭对应的socket。

如何给客户端发送数据?

这也是一道常见的网络通信面试题,某一年的腾讯后台开发职位就问到过这样的问题。给客户端发送数据,比收数据要稍微麻烦一点,也是需要讲点技巧的。首先我们不能像注册检测数据可读事件一样一开始就注册检测数据可写事件,因为如果检测可写的话,一般情况下只要对端正常收取数据,我们的socket就都是可写的,如果我们设置监听可写事件,会导致频繁地触发可写事件,但是我们此时并不一定有数据需要发送。所以正确的做法是:如果有数据要发送,则先尝试着去发送,如果发送不了或者只发送出去部分,剩下的我们需要将其缓存起来,然后再设置检测该socket上可写事件,下次可写事件产生时,再继续发送,如果还是不能完全发出去,则继续设置侦听可写事件,

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值