WebRTC服务器理论铺垫(三):实现高性能网络服务器的四种方式:fork,select,epoll,开源多路IO库

本文介绍了实现高性能网络服务器的四种主要方式,包括通过fork创建子进程、使用select进行多路复用、利用epoll的高效事件通知以及epoll与fork的结合使用。同时提到了异步事件处理的开源库,如libevent和libuv等,强调了在不同场景下选择合适的方法对服务器性能的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

一、实现高性能网络服务器的四种方式

在之前我们实现的tcp server, tcp client, udp server, udp client中,默认只允许一个连接,在代码逻辑中表现为一个while死循环。为了允许更多的连接,我们需要使用一些机制来实现高性能网络服务器,具体的有以下四种方式。

二、通过fork实现高性能网络服务器

  1. 每收到一个连接就创建一个子进程。
  2. 父进程负责接收连接。
  3. 通过fork来创建子进程
#include<iostream>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<unistd.h>
#define PORT 8111
#define MESSAGE_LEN 1024
int main(int argc,char* argv[])
{
    int socket_fd = -1,accept_fd = -1;
    int on = 1;
    int curpos = 0;
    int backlog = 1000;
    int flag = 1;
    int ret = -1;
    char in_buffer[MESSAGE_LEN] = {0,};
    struct sockaddr_in localaddr,remoteaddr;
    //create
    socket_fd = socket(AF_INET, SOCK_STREAM,0);
    if(socket_fd == -1)
    {
      std::cout<<"FAILED to CREATE SOCKET!"<<std::endl;
      exit(-1);
    }
    ret = setsockopt(socket_fd,
                  SOL_SOCKET,
                  SO_REUSEADDR,
                  &flag,
                  (socklen_t)(sizeof(flag)));
    if(ret == -1)
    {
      std::cout<<"FAILED to set socket option!"<<std::endl;
      exit(-1);
    }
    //bind    
    localaddr.sin_family = AF_INET;
    localaddr.sin_port = PORT;
    localaddr.sin_addr.s_addr = INADDR_ANY;
    int socketlen = sizeof(localaddr);
    // bzero() 会将内存块(字符串)的前n个字节清零;
    // s为内存(字符串)指针,n 为需要清零的字节数。
    // 在网络编程中会经常用到。
    // void bzero(void *s, int n);
    bzero(&(localaddr.sin_zero),8);
    //int ret_bind = bind(socket_fd,(struct sockaddr *)&localaddr,(socklen_t)(sizeof(struct sockaddr_in)));
    int bind_ret = bind(socket_fd,
        (struct sockaddr *)&localaddr,
        sizeof(struct sockaddr));
    if(bind_ret < 0)
    {
      std::cout<<"FAILED to bind addr!"<<std::endl;
      exit(-1);
    }
    int ret_listen = listen(socket_fd,backlog);
    if(ret_listen == -1)
    {
      std::cout<<"FAILED to listen!"<<std::endl;
      exit(-1);
    }
    pid_t pid = 0;
    for(;;){
      socklen_t sockaddr_len = sizeof(struct sockaddr_in);
      int accept_fd = accept(socket_fd,
      (struct sockaddr *) &remoteaddr, 
      &sockaddr_len);

      pid = fork();
      if(pid == 0){
      //子进程收发数据  
          for(;;)
          {
            //memset(in_buffer,0,MESSAGE_LEN);
            int recv_ret = recv(accept_fd,(void *)in_buffer,MESSAGE_LEN,0);
            if(recv_ret == 0)
            {
                break;
            }
            std::cout<<"recv : "<<in_buffer<<std::endl;
            send(accept_fd,(void*)in_buffer,MESSAGE_LEN,0);
          }
      
      std::cout<<"Close Client Connection"<<std::endl;
      close(accept_fd);
    }
    }
    if(pid!=0)close(socket_fd);
    return 0;
}

缺点:

  1. 以fork方式创建进程会一直占用系统资源。
  2. 在高并发情况下下创建子进程的时间开销大。

三、通过select实现高性能网络服务器

select实现高性能网络服务器其实是基于异步IO的思想。
异步IO,是指以事件触发的机制来对IO操作进行处理。
与多进程和多线程技术相比,异步IO技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些线程和进程,从而大大减小了系统的开销。

select步骤:

  1. 遍历文件描述符集中的所有描述符,找出有变化的描述符。
  2. 对于监听的socket和进行数据处理的socket要区别对待。
  3. socket必须要设置为非阻塞方式工作。

代码如下:

#include<iostream>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<unistd.h>
#include<fcntl.h>
#define max(a,b) ((a)>(b)?(a):(b))
#define PORT 8111
#define MESSAGE_LEN 1024
#define FD_SIZE 1024 //select默认只允许1024个连接
int main(int argc,char* argv[])
{
    int socket_fd = -1,accept_fd = -1;
    int on = 1;
    int curpos = -1;
    int backlog = 1000;
    int flag = 1;
    int max_fd = -1;
    int event = 0;
    int ret = -1;
    fd_set fd_sets;
    int accept_fds[FD_SIZE] = {-1};
    char in_buffer[MESSAGE_LEN] = {0,};
    struct sockaddr_in localaddr,remoteaddr;
    //create
    socket_fd = socket(AF_INET, SOCK_STREAM,0);
    if(socket_fd == -1)
    {
      std::cout<<"FAILED to CREATE SOCKET!"<<std::endl;
      exit(-1);
    }
    flag = fcntl(socket_fd,F_GETFL,0);
    //将fd设置为非阻塞
    fcntl(socket_fd,F_SETFL,flag | O_NONBLOCK);
    max_fd = socket_fd;
    ret = setsockopt(socket_fd,
                  SOL_SOCKET,
                  SO_REUSEADDR,
                  &flag,
                  (socklen_t)(sizeof(flag)));
    if(ret == -1)
    {
      std::cout<<"FAILED to set socket option!"<<std::endl;
      exit(-1);
    }
    //bind    
    localaddr.sin_family = AF_INET;
    localaddr.sin_port = PORT;
    localaddr.sin_addr.s_addr = INADDR_ANY;
    int socketlen = sizeof(localaddr);
    // bzero() 会将内存块(字符串)的前n个字节清零;
    // s为内存(字符串)指针,n 为需要清零的字节数。
    // 在网络编程中会经常用到。
    // void bzero(void *s, int n);
    bzero(&(localaddr.sin_zero),8);
    //int ret_bind = bind(socket_fd,(struct sockaddr *)&localaddr,(socklen_t)(sizeof(struct sockaddr_in)));
    int bind_ret = bind(socket_fd,
        (struct sockaddr *)&localaddr,
        sizeof(struct sockaddr));
    if(bind_ret < 0)
    {
      std::cout<<"FAILED to bind addr!"<<std::endl;
      exit(-1);
    }
    int ret_listen = listen(socket_fd,backlog);
    if(ret_listen == -1)
    {
      std::cout<<"FAILED to listen!"<<std::endl;
      exit(-1);
    }
    pid_t pid = 0;
    for(;;){
      FD_ZERO(&fd_sets);
      FD_SET(socket_fd,&fd_sets);
      
      for(int i = 0;i < FD_SIZE;i++)
      {
        //是一个有效的socket,才加入到fd_set中
        if(accept_fds[i] != -1){
          max_fd = max(max_fd,accept_fds[i]);
          FD_SET(accept_fds[i],&fd_sets);
        }
      }
      event = select(max_fd + 1,&fd_sets,NULL,NULL,NULL);
      if(event < 0){
         std::cout<<"Failed to use select!"<<std::endl;
         break;
      }else if(event == 0)
      {
        std::cout<<"Timeout!"<<std::endl;
        //由于没有设置超时时间,因此不可能等于零
        continue;
      }else{
        //该socket是否是监听的socket
        if(FD_ISSET(socket_fd,&fd_sets))
        {
           for(int i = 0;i < FD_SIZE;i++)
           {
              if(accept_fds[i] == -1) {
                curpos = i;
                break;
              }
           }
        
        socklen_t sockaddr_len = sizeof(struct sockaddr_in);
        accept_fd = accept(socket_fd,(struct sockaddr *) &remoteaddr, &sockaddr_len);
        flag = fcntl(accept_fd,F_GETFL,0);
        fcntl(accept_fd,F_SETFL,flag | O_NONBLOCK);
        //插入当前创建的accept_fd;
        accept_fds[curpos] = accept_fd;
        }
        //如果当前的socket不是用于监听的socket
        for(int i = 0;i<FD_SIZE;i++)
        {
          if(accept_fds[i] != -1 && FD_ISSET(accept_fds[i],&fd_sets))
          {
            memset(in_buffer,0,MESSAGE_LEN);
            int recv_ret = recv(accept_fds[i],(void *)in_buffer,MESSAGE_LEN,0);
            if(recv_ret == 0)
            {
              close(accept_fds[i]);
              break;
            }
            std::cout<<"recv : "<<in_buffer<<std::endl;
            send(accept_fds[i],(void*)in_buffer,MESSAGE_LEN,0);
          }
        }
      }

    }
    close(socket_fd);
    return 0;
}

四、通过epoll实现高性能网络服务器

关于epoll的优点请参考我的另一篇博客。

#include<iostream>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<fcntl.h>
#define PORT 8111
#define MESSAGE_LEN 1024
#define TIMEOUT 500 // 500 ms 
#define MAX_EVENTS 20 //同一时刻的并发数

int main(int argc,char* argv[])
{
    int socket_fd = -1,accept_fd = -1;
    int on = 1;
    int curpos = 0;
    int backlog = 1000;
    int flag = 1;
    int ret = -1;
    int epoll_fd = -1;
    int event_number = -1;
    int recv_ret = -1;
    struct epoll_event ev,events[MAX_EVENTS];
    char in_buffer[MESSAGE_LEN] = {0,};
    struct sockaddr_in localaddr,remoteaddr;
    //create
    socket_fd = socket(AF_INET, SOCK_STREAM,0);
    if(socket_fd == -1)
    {
      std::cout<<"FAILED to CREATE SOCKET!"<<std::endl;
      exit(-1);
    }
    //需要将socket_fd设置为非阻塞
    flag=fcntl(socket_fd,F_GETFL,0);
    fcntl(socket_fd,F_SETFL,flag | O_NONBLOCK);

    ret = setsockopt(socket_fd,
                  SOL_SOCKET,
                  SO_REUSEADDR,
                  &flag,
                  (socklen_t)(sizeof(flag)));
    if(ret == -1)
    {
      std::cout<<"FAILED to set socket option!"<<std::endl;
      exit(-1);
    }
    //bind    
    localaddr.sin_family = AF_INET;
    localaddr.sin_port = PORT;
    localaddr.sin_addr.s_addr = INADDR_ANY;
    int socketlen = sizeof(localaddr);
    // bzero() 会将内存块(字符串)的前n个字节清零;
    // s为内存(字符串)指针,n 为需要清零的字节数。
    // 在网络编程中会经常用到。
    // void bzero(void *s, int n);
    bzero(&(localaddr.sin_zero),8);
    //int ret_bind = bind(socket_fd,(struct sockaddr *)&localaddr,(socklen_t)(sizeof(struct sockaddr_in)));
    int bind_ret = bind(socket_fd,
        (struct sockaddr *)&localaddr,
        sizeof(struct sockaddr));
    if(bind_ret < 0)
    {
      std::cout<<"FAILED to bind addr!"<<std::endl;
      exit(-1);
    }
    int ret_listen = listen(socket_fd,backlog);
    if(ret_listen == -1)
    {
      std::cout<<"FAILED to listen!"<<std::endl;
      exit(-1);
    }

    epoll_fd = epoll_create(256);
    ev.events = EPOLLIN; // 为了保证客户端的连接一定能被响应,我们使用水平触发
    ev.data.fd = socket_fd;
    //负责监听的socket
    epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,
             &ev);
    for(;;){
      event_number = epoll_wait(epoll_fd,events,MAX_EVENTS,TIMEOUT);
      for(int i=0;i<event_number;i++){
        //监听的socket,创建新链接
        if(events[i].data.fd==socket_fd){
           socklen_t sockaddr_len = sizeof(struct sockaddr_in);
           accept_fd = accept(socket_fd,
      (struct sockaddr *) &remoteaddr, 
      &sockaddr_len);
           flag=fcntl(accept_fd,F_GETFL,0);
           fcntl(accept_fd,F_SETFL,flag | O_NONBLOCK);
           ev.events = EPOLLIN | EPOLLET; //边缘触发
           ev.data.fd = accept_fd;
           epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev);
        }else if(events[i].events & EPOLLIN)
        {
            //出现了输入的事件
            do{
                memset(in_buffer,0,MESSAGE_LEN);
                recv_ret = recv(events[i].data.fd,(void *)in_buffer,MESSAGE_LEN,0);//recv_ret表示收到消息的长度
                if(recv_ret == 0)
                {
                    std::cout<<"Quit Socket"<<std::endl;
                    close(events[i].data.fd);
                }
                if(recv_ret==MESSAGE_LEN)//进行阶段
                {
                  std::cout<<"maybe need to truncate data..."<<std::endl;
                }
            }while(ret < 0 && errno == EINTR);
            if(recv_ret < 0)
            {
              switch(errno){
                case EAGAIN: break;
                default: break; 
              }
            }
            if(recv_ret > 0){
              std::cout<<"recv : "<<in_buffer<<std::endl;
              send(events[i].data.fd,(void*)in_buffer,MESSAGE_LEN,0);
            }
        }
      }
    }
    close(socket_fd);
    return 0;
}

五、epoll + fork实现高性能网络服务器

一般在服务器上,CPU是多核的,上述epoll实现方式只使用了其中的一个核,造成了资源的大量浪费。因此我们可以将epoll和fork结合来实现更高性能的网络服务器。

但是在这种情况下,异步IO会出现惊群现象。我们可以通过在一个线程进行监听,加锁等方法解决。

六、通过异步事件的开源库来实现高性能网络服务器

一些著名的异步事件的开源库有

  1. libevent(Linux平台下基于epoll的开源库,用堆来管理超时事件)
  2. libevthp(基于libevent的http应用开发)
  3. libuv(Node.js正是基于此,在libev基础上进行调整,支持linux以外的几个平台)
  4. libev(改善了libevent的一些不合理之处,更加高效)

libevent重要的函数:

  1. event_base_new ( ~ epoll_create)
  2. event_base_dispatch ( ~ epoll_wait)
  3. event_new, event_add,event_del,event_free字面含义,这四个组成了epoll_ctl这个API
  4. evconnlistener_new_bind(把socket和事件的触发绑定到了一起)

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值