TCP服务器 IO多路复用的实现:select、poll、epoll

简介: TCP服务器 IO多路复用的实现:select、poll、epoll

一、多线程并发和IO多路复用(select、poll、epoll)

一请求一线程是通过多线程实现的,而selet,poll,epoll是通过io多路复用

  • 一请求一线程
    简单,但线程个数有限,C10K (1W个线程)
  • select 复杂度O(1)
    1.select,1024fd,多做几次select,可以突破C10K(1W个线程),但是往上走没法突破C1000K (100W个线程)
    2.首先要把cpu中的rset全部拷贝出来,然后在select里面选,虽然只有几个nready,但是要把所有的rset拷贝出来,所以select具有O(n)的无差别轮询复杂度
  • poll 复杂度O(n)
    和select基本上没有差别,但是它比select方便,只是不需要定义读、写、检查差错,三样东西了,全部包含在一起。它没有最大连接数的限制,原因是它是基于链表来存储的.
  • epoll 复杂度O(n)
    不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

select、poll、epoll之间的区别

二、代码部分

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAXLNE  4096
#define POLL_SIZE 1024
//8m * 4G = 128 , 512
//C10k
void *client_routine(void *arg) { //
  int connfd = *(int *)arg;
  char buff[MAXLNE];
  while (1) {
    int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
        send(connfd, buff, n, 0);//服务器向客户端的应答
        } else if (n == 0) {
            close(connfd);
      break;
        }
  }
  return NULL;
}
int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 #if 0
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    printf("========waiting for client's request========\n");
    while (1) {
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
        send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        //close(connfd);
    }
#elif 0
  //这种情况:每个客户端连接后只能发送一次消息
  //accept如果没有连接会阻塞(TCP三次握手发生在listen后,accept前),recv没有接受也会阻塞
  //因此想要发送第二条消息的时候,就会被阻塞到accept了
    printf("========waiting for client's request========\n");
    while (1) {
    struct sockaddr_in client;
      socklen_t len = sizeof(client);
      if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
          printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
          return 0;
      }
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
        send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        //close(connfd);
    }
#elif 0
  // 一请求一线程
  //通过多线程的方式,在client_routine里面放一个while(1)循环,使得可以反复recv,从而解决上面的问题(客服端只能发送出一条数据)
  //这种方式的  优点: 简单   缺点:线程个数有限,导致可以接入的客户端个数有限
  while (1) {
    struct sockaddr_in client;
      socklen_t len = sizeof(client);
      if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
          printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
          return 0;
      }
    pthread_t threadid;
    pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
    }
#elif 0
  // select 选有事件可以处理的io,利用select解决了 上述用一请求一线程,用多个线程去处理的问题
  fd_set rfds, rset, wfds, wset;//fd_set通过bitset,将其比特位置为1表明有数据,0表示没数据
  FD_ZERO(&rfds);
  FD_SET(listenfd, &rfds);//将listenfd加入set集合
  FD_ZERO(&wfds);
  int max_fd = listenfd;//在select的时候是通过遍历bitmap(集合)去找到的,因此把listenfd作为最大的索引,因此最大个数要为listenfd+1(刚开始的时候只有0、1、2(标准输入、输出、错误)和listenfd=3),因此初始化最大值 为listenfd+1)
  while (1) {
    rset = rfds;
    wset = wfds;
    int nready = select(max_fd+1, &rset, &wset, NULL, NULL);//select的做法就是将fd接受到的数据,传入集合中(rset,wset),后续只需要处理,rset,wset就行。最后一个参数timeout:如果设置为NULL(0),并且没有可读的数据,会一直阻塞;如果设置>0,那么经过该时间,阻塞就会停止,向下继续运行
    if (FD_ISSET(listenfd, &rset)) { //如果有新的客户端连接,rset中有存在listenfd接受到数据的标志(连接)
      struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
      FD_SET(connfd, &rfds);
      if (connfd > max_fd) max_fd = connfd;//动态扩容
      if (--nready == 0) continue;//如果只有listenfd(只有新客户端连接,但没有接收到其他客户端数据),就不执行下面内容了
    }
    int i = 0;
    for (i = listenfd+1;i <= max_fd;i ++) {//fd是依次增加的,fd=0,1,2是确定好的(标注输入,输出,错误值),listenfd=3,后续的connfd都是在4、5、6...依次增加的。如果connfd为8了,但connfd=4回收了,那么下一个就从4开始
      if (FD_ISSET(i, &rset)) { // 读操作
        n = recv(i, buff, MAXLNE, 0);
            if (n > 0) {
                buff[n] = '\0';
                printf("recv msg from client: %s\n", buff);
          FD_SET(i, &wfds); // 接收到数据后,就赋予客户端 读操作,也就是服务端的 写操作wfds。 但并不会在此轮进行FD_ISSET(i, &wset),而是在下一轮select后,进入FD_ISSET(i, &wset)
          //reactor
          //send(i, buff, n, 0);
            } else if (n == 0) { //如果断开连接(rset有客户端断开的信息的标志)
          FD_CLR(i, &rfds); // 如果不关闭会导致一直进入(n==0这个情况,一致打印disconnect。因为下面有close(i),所以这行即使不加也不会循环打印,但最好要加)
          // printf("disconnect\n");
                close(i);
            }
        if (--nready == 0) break;
      } else if (FD_ISSET(i, &wset)) {
        send(i, buff, n, 0);//为什么send要发送一堆16进制内容??
        FD_SET(i, &rfds);//设置读操作
      }
    }
  }
#elif 0
  struct pollfd fds[POLL_SIZE] = {0};
  fds[0].fd = listenfd;//当然也可以写成fds[listenfd]=listenfd;
  fds[0].events = POLLIN;
  int max_fd = listenfd;
  int i = 0;
  for (i = 1;i < POLL_SIZE;i ++) {
    fds[i].fd = -1;
  }
  while (1) {
    int nready = poll(fds, max_fd+1, -1);
    if (fds[0].revents & POLLIN) {
      struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
      printf("accept \n");
      fds[connfd].fd = connfd;
      fds[connfd].events = POLLIN;
      if (connfd > max_fd) max_fd = connfd;
      if (--nready == 0) continue;
    }
    //int i = 0;
    for (i = listenfd+1;i <= max_fd;i ++)  {
      if (fds[i].revents & POLLIN) {
        n = recv(i, buff, MAXLNE, 0);
            if (n > 0) {
                buff[n] = '\0';
                printf("recv msg from client: %s\n", buff);
          send(i, buff, n, 0);
            } else if (n == 0) { //
          fds[i].fd = -1;
                close(i);
            }
        if (--nready == 0) break;
      }
    }
  }
#else
  //poll/select --> 
  // epoll_create 
  // epoll_ctl(ADD, DEL, MOD)
  // epoll_wait
  int epfd = epoll_create(1); //int size
  struct epoll_event events[POLL_SIZE] = {0};//epfd可以理解为一个快递员,而events可以理解为快递员的盒子,POLL_SIZE可以理解为盒子大小,不用担心POLL_SIZE设置的小,无非是快递员多跑几趟
  struct epoll_event ev;
  ev.events = EPOLLIN;
  ev.data.fd = listenfd;
  epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
  while (1) {
    int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
    if (nready == -1) {//设置一个大于0的timeout,如果没有接收到任何信息,就会返回-1
      continue;
    }
    int i = 0;
    for (i = 0;i < nready;i ++) {
      int clientfd =  events[i].data.fd;
      if (clientfd == listenfd) {
        struct sockaddr_in client;
          socklen_t len = sizeof(client);
          if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
              printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
              return 0;
          }
        printf("accept\n");
        ev.events = EPOLLIN;
        ev.data.fd = connfd;
        epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
      } else if (events[i].events & EPOLLIN) {
        n = recv(clientfd, buff, MAXLNE, 0);
            if (n > 0) {
                buff[n] = '\0';
                printf("recv msg from client: %s\n", buff);
          send(clientfd, buff, n, 0);//非标准写法
            } else if (n == 0) { //
          ev.events = EPOLLIN;
          ev.data.fd = clientfd;
          epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
                close(clientfd);          
            }
      }
    }
  }
#endif
    close(listenfd);
    return 0;
}

三、补充

1.tcp三次握手的时刻

可以把listenfd理解成 迎宾的人

把clientfd理解成 服务员

tcp三次握手发生在listen后

accpet前,accpet只是在三次握手后取出一个clientfd

2.epoll_create()

int epfd = epoll_create(1); 只要传一个大于0的参数就行了。早期是一个固定大小,但是现在是以链表实现的,没有上限,这么做的目的是为了代码兼容

3.EPOLLOUT触发条件

把EPOLLIN理解为可读(当管道内为空是,不可读)

把EPOLLOUT理解为可写(当管道满时候,不可写)


相关文章
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
163 0
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
268 1
Linux C/C++之IO多路复用(aio)
用 Go 基于 epoll 实现一个最小化的IO库
Go 语言社区中存在多个异步网络框架,如 evio、nbio、gnet 和 netpoll 等。这些框架旨在解决标准库 netpoll 的低效问题,如一个连接占用一个 goroutine 导致的资源浪费。easyio 是一个最小化的 IO 框架,核心代码不超过 500 行,仅实现 Linux 下的 epoll 和 TCP 协议。它通过 Worker Pool、Buffer 等优化提高了性能,并提供了简单的事件处理机制。
|
6月前
|
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
150 0
Linux C/C++之IO多路复用(poll,epoll)
阿里云经济型e实例云服务器评测:企业官网搭建的性价比之选
阿里云服务器经济型e实例可以用来搭建企业网站吗?云服务器作为搭建企业官网的基础设施,其性能、稳定性、成本等因素直接影响着官网的运营效果。阿里云经济型e实例云服务器作为一款性价比较高的产品,备受用户关注。许多企业在选择云服务器搭建官网时,都会将其纳入考虑范围。本文将详细探讨阿里云经济型e实例云服务器的特点、性能表现、稳定性与可靠性,以及成本考量,最终解答是否适合用它来搭建企业官网。
阿里云特惠云服务器99元与199元配置与性能和适用场景解析:高性价比之选
2025年,阿里云长效特惠活动继续推出两款极具吸引力的特惠云服务器套餐:99元1年的经济型e实例2核2G云服务器和199元1年的通用算力型u1实例2核4G云服务器。这两款云服务器不仅价格亲民,而且性能稳定可靠,为入门级用户和普通企业级用户提供了理想的选择。本文将对这两款云服务器进行深度剖析,包括配置介绍、实例规格、使用场景、性能表现以及购买策略等方面,帮助用户更好地了解这两款云服务器,以供参考和选择。
DeepSeek服务器繁忙解决方法:使用阿里云一键部署DeepSeek个人网站!
通过阿里云一键部署DeepSeek个人网站,解决服务器繁忙问题。学生用户可领取300元代金券实现0成本部署,普通用户则可用99元/年的服务器。教程涵盖从选择套餐、设置密码到获取百炼API-KEY的全流程,助您快速搭建专属大模型主页,体验DeepSeek、Qwen-max、Llama等多款模型,无需代码,最快5分钟完成部署。支持绑定个人域名,共享亲友使用,日均成本仅约1元。
147 10
阿里云实验基于ECS搭建云上博客
实验网址https://developer.aliyun.com/adc/scenario/fdecd528be6145dcbe747f0206e361f3?spm=a2c6h.13858375.devcloud-scene-list.4.d1a04090jMEG9j
61 26
|
9天前
|
阿里云linux服务器使用脚本通过安全组屏蔽异常海外访问ip
公网网站可能会遭受黑客攻击导致访问异常,使用此脚本可以屏蔽掉异常IP 恢复访问。也可自行设置定时任务定期检测屏蔽。
108 28
阿里云轻量应用服务器38元与云服务器99元和199元区别及选择参考
2025年,阿里云推出了多款价格比较实惠的轻量应用服务器和云服务器,这些产品以其卓越的性能和亲民的价格,吸引了众多个人开发者、小型网站以及中小企业的关注。本文将对这几款轻量应用服务器和云服务器进行详细对比和测评,分析其性能和适用场景,以供大家在选择时参考。

热门文章

最新文章