epoll实现异步请求数据---以UDP为例


同步UDP请求数据的问题

不管是请求DNS资源还是其他资源。如果以串行的方式请求数据,也就是send以后recv阻塞等待获取数据,这样做的效率非常低效,网络延迟、服务器处理请求、再加上recv从内核态拷贝到用户态,这段时间还是比较长的。

接着我们考虑解决办法:

  1. 主线程send完成以后,创建一个子线程,或者使用线程池,让子线程来负责recv。这确实是一个不错的解决办法,但如果服务器处理数据的时间很长,那子线程还是会被recv阻塞住,并且如果send发送的请求很多,会出现线程池不够用,或者频繁创建线程的情况。
  2. 主线程send完成以后,将其加入epoll中等待,当有事件发生时再recv。这个方法相比于第一种使用了更少的线程资源,但本质上还是同步读取的过程。

实际上,我们可以采取主线程send发送,然后创建一个子线程,让子线程帮我们epoll_wait等待读取,这样主线程发送完就可以去执行别的任务,而不需要同步去recv读取请求的数据。


异步请求的模型

要实现异步处理主要分为四个步骤:

  1. init
    调用epoll_create创建一个句柄,然后调用pthread_create创建一个子线程执行回调函数,从而实现异步处理。
  2. commit
    建立网络连接,返回对应的sock,然后组织好对应的协议(比如DNS,也就是组织好对应要发送的数据),发送到对应的服务器,将sock加入到epoll中。
  3. callback
    设置回调函数,让第一步的子线程执行,回调函数主要负责epoll_wait第二步所有的sock,当有事件发生时,就recv接收数据,然后对得到的数据进行操作。
  4. close
    当对应的sock不再需要时,就在回调函数中close它们。

具体的代码

以客户端给服务器发送数据,然后服务器返回对应的数据为例:

服务器代码:
服务器并不是重点,怎么实现都行。

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sstream>
#include <stdlib.h>
#include <sys/epoll.h>
using namespace std;
struct ep_arg
{
    int sockfd;
};
class udpServer
{
private:
    // std::string ip;
    int port; //端口号
    int sock; //套接字
    int epfd;

public:
    // 127.0.0.1本地环回,8080默认端口号
    //通常用来进行网络代码的本地测试
    udpServer(int _port = 9999)
        : port(_port)
    {
    }
    void initServer()
    {
        // socket的返回值为文件描述符,类似于open函数
        sock = socket(AF_INET, SOCK_DGRAM, 0);
        struct sockaddr_in local;

        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        //转换ip
        // local.sin_addr.s_addr=inet_addr(ip.c_str());

        // INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP
        local.sin_addr.s_addr = INADDR_ANY;

        //绑定套接字,ip地址和端口号
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind error!\n"
                      << std::endl;
            exit(1);
        }
        epfd = epoll_create(1); //
        if (epfd < 0)
        {
            cout << "epoll_create失败" << endl;
        }
        struct epoll_event ev;
        ev.data.fd = sock;
        ev.events = EPOLLIN;
        epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
    }
    static void *callback(void *ptr)
    {
        int sock = *(int *)ptr;
        char msg[64];
        msg[0] = '\0';
        struct sockaddr_in end_point;
        socklen_t len = sizeof(end_point);
        ssize_t s = recvfrom(sock, msg, sizeof(msg) - 1, 0, (struct sockaddr *)&end_point, &len);
        if (s > 0)
        {
            //拿到发送端的ip地址,将整形四字节转成点分十进制的字符串
            std::string cli = inet_ntoa(end_point.sin_addr);
            cli += ":";
            //拿到发送端的端口号
            //要将网络字节序转为主机字节序,并且再转成字符串
            cli += std::to_string(ntohs(end_point.sin_port));

            msg[s] = '\0';
            std::cout << cli << "#" << msg << std::endl;
            //再给客户端发消息
            std::string echo_string = msg;
            echo_string += "[server echo!]";
            //发之前休眠1秒,模拟网络延迟
            sleep(1);
            sendto(sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&end_point, len);
        }
    }
    void start()
    {
        while (1)
        {
            struct epoll_event events[128] = {0};
            int nready = epoll_wait(epfd, events, 128, -1);
            if (nready < 0)
            {
                if (errno == EINTR || errno == EAGAIN)
                {
                    continue;
                }
                else
                {
                    break;
                }
            }
            else if (nready == 0)
            {
                continue;
            }
            int i = 0;
            for (i = 0; i < nready; i++)
            {
                pthread_t thread_id;
                int ret = pthread_create(&thread_id, NULL, callback, &events[i].data.fd);
            }
        }
    }
    ~udpServer()
    {
        close(sock);
    }
};
int main(int argc, char *argv[])
{
    //绑定端口号
    udpServer *up = new udpServer(atoi("9999"));
    up->initServer(); //将服务器初始化
    up->start();
    delete up;
    return 0;
}

客户端代码:

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/epoll.h>
using namespace std;

class udpClient
{
public:
    std::string ip;
    int port; //端口号
    int sock;
    int epfd;

public:
    static void *callback(void *ptr)
    {
        int epfd = *(int *)ptr;
        while (1)
        {
            struct epoll_event events[128] = {0};
            int nready = epoll_wait(epfd, events, 128, -1);
            if (nready < 0)
            {
                if (errno == EINTR || errno == EAGAIN)
                {
                    continue;
                }
                else
                {
                    break;
                }
            }
            else if (nready == 0)
            {
                continue;
            }
            int i = 0;
            for (i = 0; i < nready; i++)
            {
                int sockfd = events[i].data.fd;
                char buffer[1024] = {0};
                struct sockaddr_in addr;
                size_t addr_len = sizeof(struct sockaddr_in);
                int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);
                std::cout << "server#" << buffer << std::endl;
                int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
                printf("epoll_ctl DEL --> sockfd:%d\n", sockfd);
                close(sockfd);
            }
        }
    }
    void initEpoll()
    {
        epfd = epoll_create(1); //
        if (epfd < 0)
        {
            cout << "epoll_create失败" << endl;
        }
        cout << epfd << endl;
        pthread_t thread_id;
        int ret = pthread_create(&thread_id, NULL, callback, &epfd);
        if (ret)
        {
            perror("pthread_create");
            return;
        }
        usleep(1); // 子线程先走

        cout << "epoll_create成功" << endl;
    }

    //连服务器的ip和端口号
    udpClient(std::string _ip = "127.0.0.1", int _port = 9999)
        : ip(_ip), port(_port)
    {
    }

    void start()
    {
        struct sockaddr_in peer;
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        peer.sin_addr.s_addr = inet_addr(ip.c_str());
        for (int i = 0; i < 500; i++)
        {
            std::string msg;
            //创建socket,客户端不需要绑定
            sock = socket(AF_INET, SOCK_DGRAM, 0);
            msg.resize(5, 'A' + i % 50);
            sendto(sock, msg.c_str(), msg.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
            char echo[128];
            //接收服务端发回来的消息
            struct epoll_event ev;
            ev.data.fd = sock;
            ev.events = EPOLLIN;

            epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);

            // char buffer[1024] = {0};
            // struct sockaddr_in addr;
            // size_t addr_len = sizeof(struct sockaddr_in);
            // int n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);
            // std::cout << "server#" << buffer << std::endl;
        }
    }
    ~udpClient()
    {
        close(sock);
    }
};

void Usage(std::string proc)
{
    std::cout << "Usage:" << proc << "server_ip server_port" << std::endl;
}
//通过参数列表获取输入的ip和端口号,该ip和端口号就是服务器的ip和端口号
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    //绑定ip地址和端口号,端口号是一个整数,要进行强转
    udpClient uc(argv[1], atoi(argv[2]));
    uc.initEpoll();
    uc.start();
    while (1)
    {
        sleep(1);
        cout << "主线程执行其他任务" << endl;
    }
    return 0;
}

具体的效果:
在这里插入图片描述

由于客户端采用了异步的方式,相比于原来的串行,即使传输和接收有延迟,也能够很快接收出来,并且不影响主线程执行其他任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也要写bug、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值