C++实现基于IO复用模型的echo服务器

       IO复用,即IO multiplexing,是使用select/poll/epoll这一系列的多路选择器,可使进程或线程能够处理多个连接;实质上不是复用IO,而是多个IO复用进程或线程;

编程模型

(1)单进程IO复用服务器,它适合IO密集型的应用,不适合计算密集性的应用,因为计算需要耗费CPU资源,使得其他IO连接无法得到服务网络延迟也可能会大一些,本来进行一次read系统调用可以获得数据,现在需要一次poll和一次read才能获得数据;

(2)IO复用服务器+多线程服务器,它(1)的基础上,收到的计算请求不在主进程中进行处理,而是创建新的线程中进行处理;如果为一个连接上的多个请求而创建多个线程处理,会有次序的问题,多个请求会分发给多个线程去计算,这样就会占满全部的CPU;而且线程的创建数目是受限的,因此不宜创建过多的请求;

(3)在(2)的基础上,同一个连接上的计算请求都发往同一个线程中去计算;但是这样并发的连接数将受限于线程的数目;与(2)不同的是,一个含有很多请求的连接,只会固定的在同一个线程进行处理,只会占用一个CPU,让其它CPU可以用于其他连接的请求处理;

(4)IO复用服务器+计算线程池;将IO全部工作都放在一个线程中处理,其他的计算任务交给线程池;线程池的另一个作用可以执行阻塞操作,因为有的操作可能没有对应的非阻塞操作,或必须需要等待同步;

(5)在(4)的基础上,IO复用只在一个线程中来处理,可以换成在多个线程进行IO处理,有一个主线程accept后分发连接给各个IO处理线程,小规模计算可以在当前线程中进行,否则放入线程池中去计算;

(6)在(1)的基础上,使用多进程进行IO复用,各个工作进程相互独立,便于升级

特点

(1)IO复用几乎要使用非阻塞IO,否则一旦线程在处理具体逻辑时阻塞在某系统调用,如read,那么该线程的其他连接即使可读或可写,也无法得到服务;IO复用既然使用的是非阻塞IO,那么不能保证每次读写都可以完全,那么需要应用层缓冲;

(2)IO复用使得一个进程或线程,几乎不处于阻塞状态,高效利用CPU资源,避免为每一个连接创建进程或线程,从而引起资源紧张,进程切换等不必要的开销;

(3)IO复用可以处理键盘输入和socket可读的情况,可以简化程序编写的逻辑;

实现内容

(1)本节实现的网络模型为单进程IO复用服务器;

(2)实现的内容是一个echo服务器,由客户端从键盘输入相关内容,发送给服务器,然后由服务器收到后转发至客户端,客户端打印至终端;

(3)服务器不主动断开连接,而由客户端从键盘获得EOF或客户端退出后,将会发送Reset报文至服务器,服务器也将会退出;

TcpServer服务端实现

TcpServer接口

class TcpServer final  
{  
public:  
  TcpServer(const TcpServer&) = delete;  
  TcpServer& operator=(const TcpServer&) = delete;  
  
  explicit TcpServer(const struct sockaddr_in& serverAddr);  
  
  ~TcpServer();  
  
  void start();  
  
  static void signalHandle(int sig);  
  
private:  
  void _serviceRead(int connfd, size_t i);  
  void _serviceWrite(int connfd, size_t i);  
  void _closeConnection(size_t& index);  
  void _newConnection();  
  
  const int _listenfd;  
  const struct sockaddr_in _serverAddress;  
  bool _started;  
  unsigned int _usercount;  
  
  std::vector<struct pollfd> _pollfds;  
  std::map<int, std::vector<char>> _buffers;  
};  
说明几点:

(1)TcpServer不具有值语义,具有对象语义,拷贝一个TcpSever并不能让系统多一个一模一样的TcpServer主进程服务器(包括所管理的孩子进程),假设拷贝了一份TcpServer,那么系统中仅仅是多了对TcpServer本身数据的拷贝,而主进程和孩子进程仍然是不变的,因此对象拷贝是禁止的;继承也是被禁止的,因为TcpServer不具有多态语意,实在需要复用时,使用组合的方式;分别使用C++11新特性中delete和final来禁止拷贝和继承;

(2)_listenfd作为监听描述符;_serverAddress作为绑定的服务器端地址,需要在TcpServer显式指定;_usercount表示当前poll需要IO复用的文件描述符号个数;

(3)_pollfds存放各文件描述符需要对应pollfd结构的集合;_buffers存放每一个连接描述符的对应的应用层缓冲

(4)_serviceRead()对应连接读可访问的处理,而_serviceWrite()对应写可访问的处理;

(5)_newConnection()对应新连接到来的处理,_closeConnection()对应连接关闭;

构造函数

TcpServer::TcpServer(const struct sockaddr_in& serverAddr):  
    _listenfd(sockets::createBlockingSocket()),  
    _serverAddress(serverAddr),  
    _started(false),  
    _usercount(0)  
{  
  assert(_listenfd >= 0);  
  sockets::setReuseAddr(_listenfd);  
  sockets::signal(SIGCHLD, &TcpServer::signalHandle);  
  _pollfds.resize(100);  
}  
说明几点:

(1)初始化_listenfd监听描述符,绑定_serverAddress服务器地址后;将服务器地址设置为可重用,这样bind一个正处于TIME_WAIT状态上的连接会失败,因此使用setReuseAddr()设置socket的SO_REUSEADDR选项;

(2) sockets::signal(SIGCHLD, &TcpServer::signalHandle);设置SIGCHLD信号处理程序;其中TcpServer::signalHandle为静态成员函数,这样才能保证与普通函数具有相同的语义;

(3)_pollfds首先默认初始化为100,当_usercount大于该大小时,将使用resize()以两倍的速度增长;

服务器启动

void TcpServer::start()  
{  
  assert(!_started);  
  sockets::bind(_listenfd, _serverAddress);  
  sockets::listen(_listenfd);  
  
  _pollfds[0].fd = _listenfd;  
  _pollfds[0].events = POLLIN | POLLERR;  
  _pollfds[0].revents = 0;  
  
  printf("TcpServer start...\n");  
  ++_usercount;  
  _started = true;  
  
  while (_started)  
    {  
      printf("poll wait...\n");  
      int ret = ::poll(&(*_pollfds.begin()), _usercount, -1);  
      if (ret < 0)  
        {  
          printf("failed in TcpServer::start, poll error :%s\n", strerror_r(errno, g_errorbuf, sizeof g_errorbuf));  
          continue;  
        }  
  
      for (size_t i = 0; i < _usercount; ++i)  
        {  
          if (_pollfds[i].fd == _listenfd && _pollfds[i].revents & POLLIN)  
            {  
              printf("server new conn\n");  
              _newConnection();  
            }  
          else if (_pollfds[i].revents & POLLERR)  
            {  
              int err = sockets::socketError(_pollfds[i].fd);  
              printf("socket fd[%d] error: %s\n", _pollfds[i].fd, strerror_r(err, g_errorbuf, sizeof g_errorbuf));  
            }  
          else if (_pollfds[i].revents & POLLRDHUP)  
            {  
              printf("a client fd[%d] conenction down\n", _pollfds[i].fd);  
              _closeConnection(i);  
            }  
          else if (_pollfds[i].revents & POLLIN)  
            {  
              printf("server conn can read\n");  
              int conn = _pollfds[i].fd;  
              _serviceRead(conn, i);  
            }  
          else if (_pollfds[i].revents & POLLOUT)  
            {  
              printf("server conn can write\n");  
              int conn = _pollfds[i].fd;  
              _serviceWrite(conn, i);  
            }  
        }  
  
    }  
}  
说明几点:

(1)首先构造监听描述符 _pollfds[0].fd = _listenfd;_pollfds[0].events = POLLIN | POLLERR;用于可读以及错误的监听,不需要用于写;

(2)当poll返回时,说明有相应的描述符已经准备好了;对于监听描述符判断,if (_pollfds[i].fd == _listenfd && _pollfds[i].revents & POLLIN)说明有新的连接到来,具体处理函数见下文;

(3)对于出错的描述符,if (_pollfds[i].revents & POLLERR),使用getsocketopt获取对应的出错码,并输出;

(4)对于if (_pollfds[i].revents & POLLRDHUP),说明客户端断开了连接,此时触发服务器断开连接;具体处理函数见下文:

(5)对于连接描述符可读,if (_pollfds[i].revents & POLLIN),说明客户端发送的数据已经在Tcp的内核接收缓冲区,具体处理函数见下文;对于连接描述符可写,if (_pollfds[i].revents & POLLOUT),说明Tcp的内核发送缓冲区可以让我们存放对应的发送给客户端的数据,具体处理函数见下文;(注,一般应用层要设置发送缓冲区和接收缓冲区,但是本程序为了简化相关逻辑,只使用一个缓冲区,读写只能互斥的访问这个缓冲区,即缓冲区为空时,连接可以写入数据至缓冲区,否则要等到缓冲区的数据发送完为空);

新连接处理

void TcpServer::_newConnection()  
{  
  int connfd = sockets::accept(_listenfd, NULL);     //so far, we will not concern client address  
  
  if (connfd >= 0)  
    {  
      if (_usercount == _pollfds.size())  
        _pollfds.resize(2 * _pollfds.size());  
  
      _pollfds[_usercount].fd = connfd;  
      _pollfds[_usercount].events = POLLIN | POLLERR | POLLRDHUP;  
      _pollfds[_usercount].revents = 0;  
      ++_usercount;  
  
      sockets::setNonBlockingFd(connfd);  
      _buffers[connfd] = std::vector<char>(20);  
    }  
  else  
    {  
      printf("In TcpServer::_newConnection, accept error : %s\n", strerror_r(errno, g_errorbuf, sizeof g_errorbuf));  
    }  
  
}  

说明几点:

(1)使用accept获取新连接,将获取的新连接,如果此时_pollfds已经放满,那么就扩大_pollfds的空间;

(2)设置对应的连接描述符的相关值, _pollfds[_usercount].events = POLLIN | POLLERR | POLLRDHUP;表明该连接是不关注写可用的;

(3)并使用sockets::setNonBlockingFd(connfd);将该连接描述符connfd设置成非阻塞方式,加入到_pollfds中;将每一个新到连接的缓冲区设置成20个字节;

连接关闭

  1. void TcpServer::_closeConnection(size_t& i)  
    {  
      sockets::close(_pollfds[i].fd);  
      std::map<int, std::vector<char>>::iterator iter = _buffers.find(_pollfds[i].fd);  
      if (iter != _buffers.end())  
        {  
          _buffers.erase(iter);  
        }  
      
      _pollfds[i] = _pollfds[_usercount - 1];  
      --i;  
      --_usercount;  
    }  <span style="font-size: 11.9999990463257px; line-height: 13.1999998092651px; font-family: Consolas, 'Courier New', Courier, mono, serif; background-color: inherit;"> </span>

    说明几点:

    (1)使用close关闭对应的连接,并将该连接在map中对应映射的缓冲区清除;

    (2)将该连接在_pollfds与最后一个连接的pollfd相对换,并递减_usercount;

读服务

void TcpServer::_serviceRead(int connfd, size_t i)  
{  
  char buf[20];  
  int n = sockets::read(connfd, buf, sizeof buf);  
  
  if (n <= 0)  
    return;  
  
  std::vector<char>& buffer = _buffers[connfd];  
  buffer.resize(n);  
  std::copy(buf, buf + n, buffer.begin());  
  
  _pollfds[i].events &= ~POLLIN;  
  _pollfds[i].events |= POLLOUT;  
}  

说明几点:

(1)使用read先将客户端发来的数据读入缓冲中,并将该缓冲中的值拷贝到该连接所对应的缓冲区,必须使用引用到对应的连接缓冲区;

(2)由于该缓冲独占访问,需要关闭读关注,打开写关注;否则若不关闭读关注,那么读入的数据可能会扰乱对应的发送数据;

(3)为了效率,其实可以不关闭读关注,将读写访问同步,每次将socket数据读入到缓冲的尾部,但是这样会造成整个缓冲区过大,而且写指针指向的缓冲区的前部过大,可通过循环缓冲来解决;

写服务

void TcpServer::_serviceWrite(int connfd, size_t i)  
{  
  std::vector<char>& buffer = _buffers[connfd];  
     
  int n = sockets::write(connfd, &(*buffer.begin()), buffer.size());  
    
  if (n == static_cast<int>(buffer.size()))  
    {  
      _pollfds[i].events |= POLLIN;  
      _pollfds[i].events &= ~POLLOUT;  
    }  
  else if (n >= 0)  
    {  
      std::copy(buffer.begin() + n, buffer.end(), buffer.begin());  
      buffer.resize(buffer.size() - n);  
    }  
  else if (n < 0)  
    {  
      printf("In TcpServer::_serviceWrite, write error : %s\n", strerror_r(errno, g_errorbuf, sizeof g_errorbuf));  
      _pollfds[i].events |= POLLIN;  
      _pollfds[i].events &= ~POLLOUT;  
    }  
}  
说明几点:

(1)使用write将该连接缓冲区(必须使用引用)的数据拷贝到对应的该连接的内核发送缓冲区,有可能不能完全写入;

(2)若完全写入,则关闭写关注,打开读关注;否则继续关注写,并移动剩余的数据至缓冲区头;若出错,那就关闭写关注,打开读关注,因为有可能还有读数据需要处理;

(3)为了效率,其实可以不关闭读关注,将读写访问同步,每次将socket数据读入到缓冲的尾部,但是这样会造成整个缓冲区过大,而且写指针指向的缓冲区的前部过大,可通过循环缓冲来解决;

TcpClient客户端的实现

与一个客户一个进程的TcpClient客户端一样,不再赘述;










  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值