漫谈:一个简单的单线程基于epoll的echo服务器(附简单的性能测试)

为什么使用epoll

这个是老生常谈了,四个字,多路复用,要不单线程只能停等排队。另外select和poll不如epoll强大好用。

程序结构漫谈

代码很简陋,基本属于玩具。但是还是随便谈谈。
在单线程模型下使用epoll,只能使用一个epoll的instance同时监听socket描述符和connection描述符。当socket描述符就位时,就调用accept处理三次握手建立连接,同时将调用epoll_ctl将这个connfd加入epoll的事件监听表中。如果connfd就位,就调用recv函数从缓冲区中读数据然后原封不动的send回去。

epoll的两种模式和注意点

epoll有LT(Level-Trigger)和ET(Edge-Trigger)模式,为了简便本文使用了LT模式。
使用LT模式的时候,sockfd和connfd都不需要设置成非阻塞(non-blocking)模式,但是使用ET模式的时候,需要将connfd设置为非阻塞模式,下面来着重讨论一下这个问题。
如果ET模式“必须”使用connfd设置为非阻塞模式的说法不太严谨,应该说“不建议”或者“不适合”。
在边缘触发模式下,EPOLLIN必须要在对端有新数据写入后才能触发,如果一次EPOLLIN事件触发后,用户代码没有将缓冲区内所有的数据取出来,那只能等待对端的下一次写入,才能把上一次遗漏的一并取出来。
试想这样一个情况,服务端和客户端在进行一问一答的双向通信,服务端需要完整解析出客户端的报文,再根据客户端的请求返回信息。这个流程完成后,客户端才能进行下一次请求。如果出现了上文描述的情况(边缘触发且没有完整读完数据),那么服务端就不会给客户端返数据,客户端收不到数据,自然就不会有下一次的对端写入,形成了一次双向等待,神似死锁。

性能测试

由于是单线程echo服务器,所以为了方便起见使用一个多线程客户端循环读写数据。注意,使用单线程的客户端是没有意义的,因为单线程的客户端难以对单线程的服务端造成压力,要测试出最大的qps,必须使用多线程的客户端。
下图记录了不同线程数量所测得的QPS。

可以看到线程数较少的时候,无法测试出较为真实的QPS,因为无法对服务器造成最大负载。

Server Code

// SimpleEpollEcho.h

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <vector>

class SimpleEpollEcho {
public:
    SimpleEpollEcho(bool enable_et);
    ~SimpleEpollEcho();

    int init();
    void start_lt();

private:
    int SetNonblocking(int fd);
    bool enable_et;
    int sockfd;
    int epfd;

    const char *ip = "localhost";
    const static int port = 8888;
    const static int MAX_EPOLL_EVENT_NUM = 1024;
    const static int BUFFER_SIZE = 16;

    epoll_event events[MAX_EPOLL_EVENT_NUM];
    char buf[BUFFER_SIZE];
};

// SimpleEpollEcho.cpp

// use Level-Trigger mode, echo message must be 16bytes long. the connfd is in blocking mode
//

#include <strings.h>
#include <cassert>
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include "SimpleEpollEcho.h"

SimpleEpollEcho::SimpleEpollEcho(bool enable_et) : enable_et(enable_et)
{

}

SimpleEpollEcho::~SimpleEpollEcho()
{

}

int SimpleEpollEcho::init()
{
    int ret;
    do
    {
        struct sockaddr_in address;
        bzero(&address, sizeof(address));
        address.sin_family = AF_INET;
        inet_pton( AF_INET, ip, &address.sin_addr );
        address.sin_port = htons(port);

        sockfd = socket(PF_INET, SOCK_STREAM, 0);
        assert(sockfd >= 0);

        int on = 1;
        setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

        //SetNonblocking(sockfd);

        ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
        assert(ret != -1);
        ret = listen(sockfd, 5);
        assert(ret != -1);

        epfd = epoll_create(5);
        assert(epfd != -1);
        // add socket fd to epoll fd
        {
            epoll_event e;
            e.events = EPOLLIN;
            if(enable_et)
                e.events |= EPOLLET;
            e.data.fd = sockfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &e);
        }

    }
    while(0);
}

void SimpleEpollEcho::start_lt()
{
    while(1)
    {
        int ready = epoll_wait(epfd, events, MAX_EPOLL_EVENT_NUM, -1);
        if(ready < 0)
        {
            std::cout << "epoll_wait error" << std::endl;
            break;
        }

        for(int i = 0; i < ready; i++)
        {
            // the sockfd in epoll table will be never removed
            // so if there comes a new connection, we can accept it and get the connfd
            if((events[i].data.fd == sockfd) && (events[i].events & EPOLLIN))
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept(sockfd, (struct sockaddr *)&client_address, &client_addrlength);
                {
                    epoll_event e;
                    e.events = EPOLLIN;
                    if(enable_et)
                        e.events |= EPOLLET;
                    e.data.fd = connfd;

                    //SetNonblocking(connfd);

                    epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &e);
                }
            }
            // if there comes new data of some connection
            // now we read the connection data echo it
            else if((events[i].data.fd != sockfd) && (events[i].events & EPOLLIN))
            {
                int connfd = events[i].data.fd;
                // if flags is 0, recv behave just like read on a socket fd

                // with Level-Trigger Mode, it's ok to not read all data in buffer
                // because if there's data remained, kernel will remind us again
                // however, this requires every connection a index and buffer
                // to store their data. In single-thread program, I suppose it's not good
                // use multi-thread stack to store that may be more reasonable

                // so in single thread, we read it all
                char *pbuf = buf;
                int recvn = 0, sendn = 0;
                int recv_len = BUFFER_SIZE;

                while(recvn < recv_len)
                {
                    recvn += recv(connfd, pbuf, recv_len - recvn, 0);
                    pbuf += recvn;
                }

                pbuf = buf;
                while(sendn < recvn)
                {
                    sendn += send(connfd, pbuf, recvn - sendn, 0);
                    pbuf += sendn;
                }

                if(buf[0] == '0')
                {
                    close(connfd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
                }
            }
        }

    }
}

int SimpleEpollEcho::SetNonblocking(int fd) {
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

Client Code

#include <iostream>
#include <cassert>
#include <pthread.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <errno.h>
#include <strings.h>

const int MAX_BUF_LEN = 16;
char normal_buf[MAX_BUF_LEN] = "12345678901234\n";
char exit_buf[MAX_BUF_LEN] = "01234567980123\n";

const int MAX_THREAD_NUM = 64;

void* thread_test(void* ptr)
{
    printf("thread start\r\n");
    struct timeval tv_before;
    struct timeval tv_after;
    struct timeval tv_total_gap;
    struct timezone tz;

    struct  sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8888);
    inet_pton( AF_INET, "localhost", &servaddr.sin_addr );

    socklen_t socklen = sizeof (servaddr);
    int sockfd = socket(AF_INET,SOCK_STREAM, 0 );
    assert(sockfd > 0);

    connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr));
    int x = errno;

    for(int j = 0; j < 10; j++)
    {
        gettimeofday(&tv_before, &tz);
        for(int i = 0; i < 10000; i++)
        {
            send(sockfd, normal_buf, MAX_BUF_LEN, MSG_WAITALL);
            recv(sockfd, normal_buf, MAX_BUF_LEN, MSG_WAITALL);
        }

        gettimeofday(&tv_after, &tz);
        tv_total_gap.tv_usec += tv_after.tv_usec - tv_before.tv_usec;
        tv_total_gap.tv_sec += tv_after.tv_sec - tv_before.tv_sec;
    }

    send(sockfd, exit_buf, MAX_BUF_LEN, MSG_WAITALL);
    recv(sockfd, exit_buf, MAX_BUF_LEN, MSG_WAITALL);
    printf("total second gap, %ld, total usecond gap: %ld\r\n", tv_total_gap.tv_sec, tv_total_gap.tv_usec);
    close(sockfd);
}

int main()
{
    pthread_t threads[MAX_THREAD_NUM];
    for(int i = 0; i< MAX_THREAD_NUM; i++)
    {
        pthread_create(&threads[i], NULL, thread_test, NULL);
    }

    for(int i = 0; i< MAX_THREAD_NUM; i++)
    {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

原文地址:https://www.cnblogs.com/jo3yzhu/p/12363946.html

时间: 2024-11-12 09:00:34

漫谈:一个简单的单线程基于epoll的echo服务器(附简单的性能测试)的相关文章

epoll高并发多路复用,基于epoll的高性能服务器

并发测试工具ab使用 linux命令安装这个工具:apt-get install apache2 windows中装好apache之后就会再带一个工具 windows命令使用方法 ab -n 200 -c 5 http://www.baidu.com/ 1000就是测试的数量 -c 10 就是开启的线程数 测试的地址 反回了一些测试信息,如 使用时间,每次要多久等信息. linux也是一样用的. epoll多路复用IO 高并发 epoll多路复用是专门用来处理高并发的,在linux多路复用中有多

一个非常完善的基于Socket的多服务器通信框架

一共4个文件 XServerReceiver : Socket接受处理XServer返回工具类 XServerReceiver充当服务器 XServerSender : Socket连接发送XServer服务器工具类 XServerSender充当客户端 ClientTest : 客户端测试程序 ServerTest : 服务端测试程序 XServerReceiver.java import java.io.BufferedReader; import java.io.InputStream;

基于EPOLL模型的局域网聊天室和Echo服务器

一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说select/poll的缺点,以体现epoll的优点. select: (1)可监听的socket受到限制,在32位的系统中,默认最大值为1024. (2)采用轮询方式,当要监听的sock数量很大时,效率低. (3)随着要监听socket数据的增加,要维护一个存放大量fd的数据结构,系统开销太大. pol

基于epoll的简单服务器

一.epoll 1.epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用 (1)int epoll_create(int size); 创建一个epoll的句柄.自从linux2.6.8之后,size参数是被忽略的.需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽. (2)int epoll

基于epoll的简单的http服务器

本人用epoll写了一个简单的http服务器,该服务器在客户端第一次发送数据时可以正确处理,但是当客户端不关闭继续发送数据时,服务器无法读取,请求大家帮忙看看哪里有问题,谢谢 </pre></p><p>server.h</p><p><pre name="code" class="cpp">/* * server.h * * Created on: Jun 23, 2014 * Author: f

基于ThreadPoolExecutor,自定义线程池简单实现

一.线程池作用 在上一篇随笔中有提到多线程具有同一时刻处理多个任务的特点,即并行工作,因此多线程的用途非常广泛,特别在性能优化上显得尤为重要.然而,多线程处理消耗的时间包括创建线程时间T1.工作时间T2.销毁线程时间T3,创建和销毁线程需要消耗一定的时间和资源,如果能够减少这部分的时间消耗,性能将会进一步提高,线程池就能够很好解决问题.线程池在初始化时会创建一定数量的线程,当需要线程执行任务时,从线程池取出线程,当任务执行完成后,线程置回线程池成为空闲线程,等待下一次任务.JDK1.5提供了一个

一个简单的零配置命令行HTTP服务器

http-server 是一个简单的零配置命令行HTTP服务器, 基于 nodeJs. 如果你不想重复的写 nodeJs 的 web-server.js, 则可以使用这个. 安装 (全局安装加 -g) : npm install http-server Windows 下使用: 在站点目录下开启命令行输入 http-server 访问: http://localhost:8080 or http://127.0.0.1:8080  使用于package.json "scripts":

SPWebServer:一个基于 SPServer 的 web 服务器框架

SPWebServer:一个基于 SPServer 的 web 服务器框架 博客分类: OpenSource项目 应用服务器框架Web网络应用多线程 看到这个题目,估计很多人会问:为什么要再实现一个 web 服务器? 这里有几个原因: 1.这是一个 web 服务器框架,不是一个完整的 web 服务器.也就是说 SPWebServer 提供的是一套 API 和类库,可以方便地集成到现有的应用程序中.可以称 SPWebServer 为 embedded web server . 2.有些时候,我们需

分享一个自己用的基于mvc编程工作管理

前言: 最近在家没事学习下mvc,正好把以前用webform写的一个帮助自己编码的工具重构成了mvc,另外根据自己的编程工作感悟添加了公司常用软件维护 ,数据库操作记录这些新功能. 技术没什么高深的技术,就是mvc+jquery easyUi+简单的三层,生成文档的模板引擎用的Razor. 主要还是解决沟通成本太高的问题,都是根据工作中遇到的问题慢慢解决的,至少很多功能可以解决小型软件团队很多流程问题,自己摸索真是又痛苦又快乐.相信也有很多朋友和我一样没有去过大型软件公司,很多问题只能看别人的文