服务器搭建(TCP套接字)-epoll版(服务端)

2023-09-19 17:27:35

   epoll 是一种在 Linux 系统上用于高效事件驱动编程的 I/O 多路复用机制。它相比于传统的 select 和 poll 函数具有更好的性能和扩展性。

epoll 的主要特点和原理

1、事件驱动:epoll 是基于事件驱动的模型,它通过监听事件来触发相应的回调函数,而不是像传统的阻塞模型那样持续轮询。这样可以避免无效的轮询操作,提高效率。

2、高效:epoll 使用了红黑树(rbtree)和哈希表(hash table)的数据结构来存储和管理大量的文件描述符,使得在大规模连接的情况下,对文件描述符的管理和查找操作具备较高的效率。

3、边缘触发:epoll 提供了边缘触发(edge-triggered)的工作模式。在边缘触发模式下,只有当文件描述符的状态发生变化时,epoll 才会通知应用程序。这使得应用程序能够更精确地处理事件,避免了事件的丢失和重复触发。

4、扩展性:epoll 支持高并发的连接,可以同时监听大量的文件描述符,且不随文件描述符数量的增加而性能下降。它采用了事件通知的方式,只有在文件描述符发生状态变化时才会通知应用程序,避免了大量的轮询操作。

使用 epoll 的基本步骤如下:

1、创建 epoll 实例,通过调用 epoll_create 函数创建一个 epoll 对象

2、将需要监听的文件描述符加入 epoll 实例,通过调用 epoll_ctl 函数将文件描述符添加到 epoll 中,并指定需要监听的事件类型。
epoll_ctl 是一个用于控制 epoll 实例的系统调用函数,它用于向 epoll 实例中添加、修改或删除文件描述符及其关联的事件。

3、 进入事件循环,调用 epoll_wait 函数等待事件发生。该函数会阻塞程序执行,直到有事件发生或超时。
epoll_wait 是一个用于等待事件的系统调用函数,它在 epoll 实例上进行阻塞等待,直到有事件就绪或超时。

4、 当 epoll_wait 返回时,根据返回的就绪事件进行相应的处理。可以通过遍历返回的事件列表来获取就绪的文件描述符和事件类型。

    epoll 在网络编程中广泛应用,特别适用于高并发的服务器开发,能够处理大量的并发连接和高频率的 I/O 事件。它提供了高效的事件驱动模型,可以大大提升程序的性能和可扩展性。

一、epoll_create

int epoll_create(int size);

epoll_create 函数接受一个参数 size,该参数指定了 epoll 实例所能处理的最大文件描述符数目。它返回一个整数值,表示 epoll 实例的文件描述符,用于后续的 epoll 相关操作。

入参:
size 参数是一个提示值,用于告诉内核 epoll 实例需要处理的最大文件描述符数目。内核会根据此提示值进行一些优化,但实际上该值在大多数情况下并不会限制 epoll 实例所能处理的文件描述符数目。

返回值:

  • 成功时返回一个非负整数,表示 epoll 实例的文件描述符。
  • 如果调用失败,返回值为 -1,并设置相应的错误码,可以通过 errno 来获取具体的错误信息。

epoll_create 函数创建的 epoll 实例是默认的边缘触发模式(Edge Triggered Mode)。这意味着当文件描述符上的事件状态从未就绪变为就绪时,epoll 会返回该事件,而不是只在事件状态为就绪时返回一次。

在使用完 epoll 实例后,应当使用 close 函数显式关闭 epoll 实例的文件描述符,以释放相关资源。

二、epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

入参:

  • epfd:表示 epoll 实例的文件描述符,即通过 epoll_create 创建的返回值。
  • op:表示要进行的操作类型,可以是以下三种值之一:
    • EPOLL_CTL_ADD:将文件描述符 fd 添加到 epoll 实例中,关联的事件由 event 参数指定。
    • EPOLL_CTL_MOD:修改已添加到 epoll 实例中的文件描述符 fd 的关联事件,新的事件由 event 参数指定。
    • EPOLL_CTL_DEL:从 epoll 实例中删除文件描述符 fd。
  • fd:表示要添加、修改或删除的文件描述符。
  • event:指向一个 struct epoll_event 结构体的指针,用于设置文件描述符的关联事件。
    event 字段表示要关注的事件类型,可以是以下事件类型的组合:
    • EPOLLIN:表示可读事件。
    • EPOLLOUT:表示可写事件。
    • EPOLLRDHUP:表示对端关闭连接或关闭写端。
    • EPOLLPRI:表示有紧急数据可读。
    • EPOLLERR:表示发生错误。
    • EPOLLHUP:表示连接关闭。
    • EPOLLET:使用边缘触发模式(Edge Triggered Mode)。
    • EPOLLONESHOT:在事件触发后,将文件描述符从 epoll 实例中删除,需要重新添加才能再次触发。
struct epoll_event {
    __uint32_t events;  // 表示要关注的事件类型
    epoll_data_t data;  // 用户数据,可以是文件描述符或指针
};

typedef union epoll_data {
    void *ptr;  // 指针类型的用户数据
    int fd;     // 文件描述符类型的用户数据
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

返回值:

  • 成功时返回 0
  • 失败时返回 -1,并设置相应的错误码,可以通过 errno 来获取具体的错误信息。

三、epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

入参:

  • epfd:表示 epoll 实例的文件描述符,即通过 epoll_create 创建的返回值。
  • events:指向一个 struct epoll_event 数组的指针,用于存储就绪的事件信息。
  • maxevents:表示 events 数组的大小,即最多可以存储的事件数目。
  • timeout:表示等待的超时时间,以毫秒为单位。可以是以下值之一:
    • -1:表示永久阻塞,直到有事件就绪。
    • 0:表示非阻塞,立即返回。
    • 大于 0:表示超时时间,等待指定的毫秒数后返回。

返回值:

  • epoll_wait 函数用于阻塞等待 epoll 实例上的事件就绪。当有事件就绪时,它将填充 events 数组,并返回就绪事件的数量。
  • 如果 epoll_wait 函数返回值大于 0,则表示有就绪事件,并且可以通过遍历 events 数组来获取每个就绪事件的相关信息。
  • 如果 epoll_wait 函数返回值为 0,表示超时时间到达,即没有事件就绪。
  • 如果 epoll_wait 函数返回值为 -1,表示调用出错,可以通过 errno 来获取具体的错误信息。

四、代码实现

#include <iostream>
//socket
#include <sys/types.h>
#include <sys/socket.h>
//close
#include <unistd.h>
//exit
#include <stdlib.h>
//perror
#include <stdio.h>
//memset
#include <string.h>
//htons
#include <arpa/inet.h>
/* According to earlier standards */
#include <sys/time.h>
//epoll
#include <sys/epoll.h>


#define PORT 8596
#define MESSAGE_SIZE 1024
#define FD_SIZE 1024
#define MAX_EVENTS 20
#define TIME_OUT 500

int main(){

  int ret=-1;
  int socket_fd=-1;
  int accept_fd=-1;

  int backlog=10;
  int flags=1;

  struct sockaddr_in local_addr,remote_addr;

  struct epoll_event ev,events[FD_SIZE];
  int epoll_fd=-1;
  int event_number=0;

  //create socket
  socket_fd=socket(AF_INET,SOCK_STREAM,0);
  if(socket_fd == -1){
    perror("create socket error");
    exit(1);
  }
 //set option of socket
  ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags));
  if ( ret == -1 ){
    perror("setsockopt error");
  }

 //set socket address
  local_addr.sin_family=AF_INET;
  local_addr.sin_port=htons(PORT);
  local_addr.sin_addr.s_addr=INADDR_ANY;
  bzero(&(local_addr.sin_zero),8);
 //bind socket
 ret=bind(socket_fd, (struct sockaddr *)&local_addr,sizeof(struct sockaddr_in));
 if(ret == -1){
    perror("bind socket error");
    exit(1);
 }

  ret=listen(socket_fd, backlog);
  if(ret ==-1){
   perror("listen error");
   exit(1);
  }
  //创建epoll
  epoll_fd=epoll_create(256);
  ev.data.fd=socket_fd;
  ev.events=EPOLLIN;
  //将socket_fd加入到epoll中
  epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);
  //loop to accept client
  for(;;){
   event_number=epoll_wait(epoll_fd,events,MAX_EVENTS,TIME_OUT);
   for(int i=0;i<event_number;i++){
     if(events[i].data.fd==socket_fd){
      socklen_t addrlen = sizeof(remote_addr);
      accept_fd=accept(socket_fd,( struct sockaddr *)&remote_addr, &addrlen);
      ev.data.fd=accept_fd;
      ev.events=EPOLLIN | EPOLLET;
      //添加accept_fd到epoll中
      //添加accept_fd到epoll中
      if((epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev))==-1){
        close(accept_fd);
      } 
     }else if(events[i].events & EPOLLIN){
        //有数据可读
        char in_buf[MESSAGE_SIZE];
        memset(in_buf, 0, MESSAGE_SIZE);
        //receive data
        ret = recv( events[i].data.fd, &in_buf, MESSAGE_SIZE, 0 );
        if(ret <= 0){

            switch (errno){
              case EAGAIN: //暂时没有数据
                break;
              case EINTR: //被终断
                ret = recv(events[i].data.fd, &in_buf, MESSAGE_SIZE, 0);
                break;
              default:
                printf("the client is closed, fd:%d\n", events[i].data.fd);
                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
                close(events[i].data.fd);
                break;
            }  
        }      
               
        printf("receive message:%s\n", in_buf);
        send(events[i].data.fd, &in_buf, ret, 0);
    }
   }
 }
  printf("quit server....");
  // 关闭监听 socket 和 epoll 实例
  close(socket_fd);
  close(epoll_fd);
  return 0;
}

  • 服务端
    在这里插入图片描述
  • 客户端1
    在这里插入图片描述
  • 客户端2
    在这里插入图片描述

从代码直观就能看出epoll的代码量和逻辑实现相比select,都特别简洁。epoll 在网络编程中广泛应用,特别适用于高并发的服务器开发,能够处理大量的并发连接和高频率的 I/O 事件。它提供了高效的事件驱动模型,可以大大提升程序的性能和可扩展性。

更多推荐

ASP.NET dotnet 3.5 实验室信息管理系统LIMS源码

技术架构:ASP.NETdotnet3.5LIMS作为一个信息管理系统,它有着和ERP、MIS之类管理软件的共性,如它是通过现代管理模式与计算机管理信息系统支持企业或单位合理、系统地管理经营与生产,最大限度地发挥现有设备、资源、人、技术的作用,最大限度地产生经济效益。但其也与企业管理软件存在着差异,首先,LIMS作为实

基于Java+SpringBoot+Vue前后端分离毕业设计系统设计和实现

博主介绍:✌全网粉丝30W+,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌🍅文末获取源码联系🍅👇🏻精彩专栏推荐订阅👇🏻不然下次找不到哟2022-2024年最全的计算机软件毕业设计选题

Python项目Flask ipv6双栈支持改造

一、背景Flask是一个微型的(轻量)使用Python语言开发的WSGIWeb框架(一组库和模块),基于WerkzeugWSGI工具箱/库和Jinja2模板引擎,当然,Python的WEB框架还有:Django、Tornado、Webpy,这暂且不提。Flask使用BSD授权。Flask也被称为microframewo

excel中的引用与查找函数篇2

如下所有案例中表头均不参与范围查找内:1、LOOKUP(lookup_value,lookup_vector,[result_vector]):在一行或者一列中查找某个值并从另一行或者列中找到同位置的值记住:中括号内的参数可以不赋值,若在中间用逗号隔开这个参数,若在末尾则不予理会lookup(查找对象,查找对象所在的行

AI+低代码:开启普惠人工智能时代的新篇章

🌷🍁博主猫头虎带您GotoNewWorld.✨🍁🦄博客首页——猫头虎的博客🎐🐳《面试题大全专栏》文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺🌊《IDEA开发秘籍专栏》学会IDEA常用操作,工作效率翻倍~💐🌊《100天精通Golang(基础入门篇)》学会Golang语言,畅玩云原生,走遍大

深入学习 Redis Cluster - 基于 Docker、DockerCompose 搭建 Redis 集群,处理故障、扩容方案

目录一、基于Docker、DockerCompose搭建Redis集群1.1、前言1.2、编写shell脚本1.3、执行shell脚本,创建集群配置文件1.4、编写docker-compose.yml文件1.5、启动容器1.6、构建集群1.7、使用集群1.8、如果集群中,有节点挂了,怎么办?二、集群故障、扩容处理2.1

Selenium常见问题解析

1、元素定位失败:在使用Selenium自动化测试时,最常见的问题之一是无法正确地定位元素,这可能导致后续操作失败。解决方法包括使用不同的定位方式(如xpath、CSSselector、id等),等待页面加载完全后再进行操作,或者增加元素定位的鲁棒性。举个例子:假设我们要定位一个登录页面的“用户名”输入框,但是该输入框

docker四种网络模式

文章目录一.为什么要了解docker网络二.docker网络理论三.docker的四类网络模式3.1bridge模式3.2host模式3.3container模式3.4none模式四.bridge模式下容器的通信4.1防火墙开启状态4.2防火墙关闭状态一.为什么要了解docker网络当你开始大规模使用Docker时,你

Docker基础-namespace

Docker-namespacenamespace基础命令dd命令mkfsdfmountunsharepid隔离试验mount隔离namespacenamespace是Linux内核用来隔离内核资源的方式。通过namespace可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,

Spark 框架概述

目录一、Spark是什么1.1统一分析引擎?二、Spark风雨十年​三、SparkVSHadoop(MapReduce)3.1面试题:Hadoop的基于进程的计算和Spark基于线程方式优缺点?四、Spark四大特点​4.1速度快4.2易于使用4.3通用性强​4.4运行方式五、Spark框架模块5.1介绍5.2Spar

Python+Selenium定位不到元素常见原因及解决办法(报:NoSuchElementException)

这篇文章主要介绍了Python+Selenium定位不到元素常见原因及解决办法(报:NoSuchElementException),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧在做web应用的自动化测试时,定位元素是必不可少的,这个过程经常会碰到定

热文推荐