百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

socket网络编程(四)——epoll多路复用问题

liebian365 2024-10-27 13:13 22 浏览 0 评论

1、epoll诞生的原因

问大家一个问题,如果要设计一款有着千万级别并发的系统,你的客户端和服务端的网络通信底层该怎么设计?我在上一篇文章(计算机通信之谜,带你彻底理解socket网络编程(三) )中有说到用select可以实现IO多路复用,但是select的设计有瓶颈所在,超过十万的并发效率就非常慢。那么又该怎么办呢?

于是epoll就腾空出世了!

2、epoll是什么

什么是epoll呢?epoll和select一样,也是为IO多路复用而生的。而epoll最大的优点也是select的不足之处,我们知道,select的最大连接数被限制在了1024个,而且select是通过轮询所有的连接的方式寻找需要的那个连接,所以对select来说,连接数越多,耗费的资源就越大,这是一个无法调和的矛盾。

而epoll就是无需通过轮询就是可以找到那个发生IO事件的连接,它通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,epoll在内核初始化的时候向内核注册了一个文件系统,用于存储上述被监控的socket,所以无需轮询所有的socket连接,有点类似用空间换时间的意思。至于epoll底层的实现原理暂时不在本文的讨论范围,以后我会弄个章节出来特别讨论下。

3、具体实现

首先,还是先不扯其他的,我先扔出代码,然后结合代码讲解epoll,带着代码的疑问去思考,这样子感觉学起来更加的有效率。如果大家不习惯的话,可以先跳过以下的代码,先看代码下方的讲解部分。

3.1、服务端代码

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>

#define EPOLL_SIZE 1023 //epoll监听客户端的最大数目
#define MAX_EVENTS 64
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

int main()
{
    //创建套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }

    //初始化socket元素
    struct sockaddr_in server_addr;
    int server_len = sizeof(server_addr);
    memset(&server_addr, 0, server_len);

    server_addr.sin_family = AF_INET;
    //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(39002);

    //绑定文件描述符和服务器的ip和端口号
    int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
    if (m_bindfd < 0)
    {
        ERR_EXIT("bind ip and port fail");
    }

    //进入监听状态,等待用户发起请求
    int m_listenfd = listen(m_sockfd, 20);
    if (m_listenfd < 0)
    {
        ERR_EXIT("listen client fail");
    }

    //定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    //int m_connfd;

    printf("client accept success\n");

    //创建一个监听描述符epoll,并将监听套接字加入监听列表
    int epollfd = epoll_create(EPOLL_SIZE);
    if (epollfd < 0)
    {
        ERR_EXIT("epoll create fail");
    }

    struct epoll_event eve;
    eve.events = EPOLLIN;
    eve.data.fd = m_sockfd;
    //控制epoll文件描述符上的动作
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, m_sockfd, &eve) < 0)
    {
        ERR_EXIT("epoll control fail");
    }

    struct epoll_event evelist[MAX_EVENTS];

    //接收客户端数据,并相应
    char buffer[BUF_SIZE];
    while (1)
    {
        int ret = epoll_wait(epollfd, evelist, MAX_EVENTS, -1);
        if (ret < 0)
        {
            ERR_EXIT("epoll fail");
        }
        else if (ret == 0)
        {
            printf("epoll timeout\n");
            continue;
        }

        for (int i = 0; i < ret; i++)
        {
            //客户端请求连接
            if (evelist[i].data.fd == m_sockfd)
            {
                int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
                if (m_connfd < 0)
                {
                    ERR_EXIT("server accept fail");
                }

                //把客户端新建立的连接添加到epoll的监听中
                struct epoll_event eve;
                eve.events = EPOLLIN | EPOLLRDHUP; //监听连接套接字的可读和退出
                eve.data.fd = m_connfd;
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, m_connfd, &eve) < 0) //将新连接的套接字加入监听
                {
                    ERR_EXIT("epoll control fail, accept client");
                }

                printf("we got a new connection, client_socket=%d, ip=%s, port=%d\n", m_connfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
            }
            //客户端发来数据
            else if (evelist[i].events & EPOLLIN)
            {
                memset(buffer, 0, sizeof(buffer)); //重置缓冲区
                int recv_len = recv(evelist[i].data.fd, buffer, sizeof(buffer) - 1, 0);
                if (recv_len < 0)
                {
                    ERR_EXIT("recv data fail");
                }
                //客户端断开连接
                else if (recv_len == 0)
                {
                    //打印断开的客户端数据
                    printf("a client close, ip=%s, port=%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, evelist[i].data.fd, &eve);
                    close(evelist[i].data.fd);
                }
                else
                {
                    printf("server recv:%s\n", buffer);
                    strcat(buffer, "+ACK");
                    send(evelist[i].data.fd, buffer, sizeof(buffer) - 1, 0);
                }
            }
            //客户端退出
            else if (evelist[i].events & EPOLLRDHUP)
            {
                printf("a client is quit, ip=%s, port=%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                epoll_ctl(epollfd, EPOLL_CTL_DEL, evelist[i].data.fd, &eve);
                close(evelist[i].data.fd);
            }
        }
    }

    //关闭套接字
    close(m_sockfd);

    printf("server socket closed!!!\n");

    return 0;
}

3.2、客户端代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

int main()
{
    //创建套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }

    //服务器的ip为本地,端口号
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
    server_addr.sin_port = htons(39002);

    //向服务器发送连接请求
    int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (m_connectfd < 0)
    {
        ERR_EXIT("connect server fail");
    }

    //发送并接收数据
    char buffer[BUF_SIZE];
    while (1)
    {
        memset(buffer, 0, sizeof(buffer)); //重置缓冲区
        printf("client send:");
        scanf("%s", buffer);
        send(m_sockfd, buffer, sizeof(buffer) - 1, MSG_NOSIGNAL);
        recv(m_sockfd, buffer, sizeof(buffer) - 1, 0);
        printf("client recv:%s\n", buffer);
    }

    //断开连接
    close(m_sockfd);

    printf("client socket closed!!!\n");

    return 0;
}

4、epoll结构剖析

int epoll_create(int size);

首先利用创建一个epoll句柄,参数size用来告诉内核监听的数目,size为epoll所支持的最大句柄数。它其实是在内核申请空间,用来存放你想监听的套接字描述符上是否有读、写或者异常的各类事件。不管是什么样的写法,只要是用到了epoll,就必须要有这个函数。

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

epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事件,struct epoll_event结构如下:

struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

最后一个函数就像是阻塞函数,等待着客户端发来请求或者数据,只要有连接发来,这个函数就会响应。

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

epfd : epoll特有的文件描述符

events :从内核中的就绪队列中拷贝出就绪的文件描述符,不可以是空指针,内核只负责将数据拷贝到这里,不会为我们开辟空间。

maxevent : 高速内核events有多大,一般不能超过epoll_create传递的size,

timeout : 函数超时时间,0表示非阻塞式等待,-1表示阻塞式等待,函数返回0表示已经超时。

说实话,epoll的代码写法上会比select容易理解得多,基本上就是围绕着那三个函数展开的,一个创建,一个注册,一个等待。至于底层的具体部分,都已经封装好了,属于操作系统层面。对于想自己搭建底层通信协议的童鞋来说懂得如何用epoll已经足够了,如果要深入了解epoll的话,需要更加阅读深入的教程,但已不属于本文所讨论的范围。后期如果有时间,我一定一点点和大家剖析底层的实现原理。


更多精彩内容,请关注同名公众:一点笔记alittle

相关推荐

Linux-常用操作命令介绍(linux常用的命令大全)

1.帮助命令帮助命令1.1help命令语法格式:命令--help作用:查看某个命令的帮助信息示例#ls--help#netstat--help1.2man命令语法格式:man命令...

推荐:一个小而美的Java工具类库(java工具软件)

前言是的,你没看错,没看错,它就是hutool!相信很多做java开发的朋友应该都已经认识并使用过它了,今天带大家再重温一下它都有哪些功能,并以示例来看看hutool是如何简便实现JWT认...

【SpringBoot后端开发】第三部分 Linux操作系统常用命令(3)

创作不易,请帮忙转发、点赞和评论!四、Linux常用命令对于Linux系统来说,中央处理器、内存、磁盘驱动器、键盘、鼠标、用户等都是文件,而Linux系统管理的命令是它正常运行的核心,与之DOS命令类...

linux常用命令在线查询工具(linux常用命令在线查询工具有哪些)

linuxvi编辑器常用命令linux查看iplinuxfind-name查找文件名linuxshelllinux查看端口占用linux删除文件命令linuxcp命令复制文件到另一个...

使用免费绿色工具chfs,将文件夹共享成网盘

需求:业务需求方有个需要将apk包上传到服务器中,通过chfs可以将服务器目录共享出来,可以可以登录后台自行上传apk文件包。本文就教大家三个知识点1.centos7下使用chfs,共享目录。2.使用...

Mysql和Hive之间通过Sqoop进行数据同步

文章回顾理论大数据框架原理简介大数据发展历程及技术选型实践搭建大数据运行环境之一搭建大数据运行环境之二本地MAC环境配置CPU数和内存大小查看CPU数sysctlmachdep.cpu#核数为...

真实案例记录Linux被植入rootkit导致服务器带宽跑满的解决过程

一、关于linux下的rootkitrootkit是Linux平台下最常见的一种木马后门工具,它主要通过替换系统文件来达到攻击和和隐蔽的目的,这种木马比普通木马后门更加危险和隐蔽,普通的检测工...

python周期任务调度工具Schedule使用详解

如果你想周期性地执行某个Python脚本,最出名的选择应该是Crontab脚本,但是Crontab具有以下缺点:不方便执行秒级任务。当需要执行的定时任务有上百个的时候,Crontab的管...

Linux 系统日常巡检脚本(shell巡检脚本)

Linux系统日常巡检脚本,巡检内容包含了,磁盘,内存cpu进程文件更改用户登录等一系列的操作直接用就行了。报告以邮件发送到邮箱在log下生成巡检报告。#!/bin/bash#@Au...

Schedule—简单实用的 Python 周期任务调度工具

如果你想周期性地执行某个Python脚本,最出名的选择应该是Crontab脚本,但是Crontab具有以下缺点:1.不方便执行秒级任务。2.当需要执行的定时任务有上百个的时候,Cronta...

celery定时与异步任务详解(定时任务异步执行)

celery简介Celery是一个简单、灵活且可靠的,处理大量消息的分布式系统,专注于实时处理的异步任务队列,同时也支持任务调度。Celery的架构由三部分组成,消息中间件(messagebroke...

开源免费的定时任务管理系统:Gocron

Gocron:精准调度未来,你的全能定时任务管理工具!-精选真开源,释放新价值。概览Gocron是github上一个开源免费的定时任务管理系统。它使用Go语言开发,是一个轻量级定时任务集中调度和管理...

PHP Laravel定时任务Schedule(laravel定时任务原理)

前提:本文方法是利用Linux的crontab定时任务来协助实现Laravel调度(Mac也一样)。一、首先添加Crontab定时任务,这里只做简单介绍:用命令crontab-e添加如下内容**...

Linux的常用命令就是记不住,怎么办?于是推出了这套教程

1.帮助命令1.1help命令#语法格式:命令--help#作用:查看某个命令的帮助信息#示例:#ls--help查看ls命令的帮助信息#netst...

如何定期执行 Python 脚本:5 种常见方法

定期执行任务是自动化工作流程中的重要环节,无论是数据抓取、文件备份,还是定期报告生成,定时运行脚本都可以极大提高效率。本文将介绍五种方法,通过这些方法,你可以轻松设置定期执行Python脚本的任务...

取消回复欢迎 发表评论: