zoukankan      html  css  js  c++  java
  • (OK) Linux epoll模型—socket epoll server client chat

    http://www.cnblogs.com/venow/archive/2012/11/30/2790031.html

    http://blog.csdn.net/denkensk/article/details/41978015

    定义:

      epoll是Linux内核为处理大批句柄而作改进的poll,是Linux下多路复用IO接口select/poll的增强版本号。它能显著的降低程序在大量并发连接中仅仅有少量活跃的情况下的系统CPU利用率。

    由于它会复用文件描写叙述符集合来传递结果而不是迫使开发人员每次等待事件之前都必须又一次准备要被侦听的文件描写叙述符集合。还有一个原因就是获取事件的时候,它无须遍历整个被侦听的描写叙述符集,仅仅要遍历那些被内核IO事件异步唤醒而增加Ready队列的描写叙述符集合即可了。epoll除了提供selectpoll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,降低epoll_wait/epoll_pwait的调用。提供应用程序的效率。

    工作方式:

      LT(level triggered):水平触发。缺省方式,同一时候支持block和no-block socket。在这样的做法中,内核告诉我们一个文件描写叙述符是否被就绪了,假设就绪了,你就能够对这个就绪的fd进行IO操作。假设你不作不论什么操作。内核还是会继续通知你的,所以,这样的模式编程出错的可能性较小。

    传统的selectpoll都是这样的模型的代表。

      ET(edge-triggered):边沿触发,快速工作方式,仅仅支持no-block socket。在这样的模式下,当描写叙述符从未就绪变为就绪状态时,内核通过epoll告诉你。然后它会如果你知道文件描写叙述符已经就绪,而且不会再为那个描写叙述符发送很多其它的就绪通知,直到你做了某些操作导致那个文件描写叙述符不再为就绪状态了(比方:你在发送、接受或者接受请求。或者发送接受的数据少于一定量时导致了一个EWOULDBLOCK错误)。

    可是请注意,如果一直不正确这个fs做IO操作(从而导致它再次变成未就绪状态)。内核不会发送很多其它的通知。

      差别:LT事件不会丢弃,而是仅仅要读buffer里面有数据能够让用户读取。则不断的通知你。而ET则仅仅在事件发生之时通知。

    使用方式:

      1、int epoll_create(int size)

    创建一个epoll句柄,參数size用来告诉内核监听的数目。

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

    epoll事件注冊函数,

      參数epfd为epoll的句柄。

      參数op表示动作,用3个宏来表示:EPOLL_CTL_ADD(注冊新的fd到epfd),EPOLL_CTL_MOD(改动已经注冊的fd的监听事件),EPOLL_CTL_DEL(从epfd删除一个fd);

      參数fd为须要监听的标示符;

      參数event告诉内核须要监听的事件。event的结构例如以下:

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

      当中events能够用下面几个宏的集合:

      EPOLLIN :表示相应的文件描写叙述符能够读(包含对端SOCKET正常关闭)

      EPOLLOUT:表示相应的文件描写叙述符能够写

      EPOLLPRI:表示相应的文件描写叙述符有紧急的数据可读(这里应该表示有带外数据到来)

      EPOLLERR:表示相应的文件描写叙述符错误发生

      EPOLLHUP:表示相应的文件描写叙述符被挂断。

      EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的

      EPOLLONESHOT:仅仅监听一次事件,当监听完这次事件之后。假设还须要继续监听这个socket的话。须要再次把这个socket增加到EPOLL队列里

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

      等待事件的产生,类似于select()调用。

    參数events用来从内核得到事件的集合。maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,參数timeout是超时时间(毫秒。0会马上返回,-1将不确定,也有说法说是永久堵塞)。该函数返回须要处理的事件数目,如返回0表示已超时。

    应用举例:

      以下,我引用google code中别人写的一个简单程序来进行说明。svn路径:http://sechat.googlecode.com/svn/trunk/

      该程序一个简单的聊天室程序。用Linux C++写的,server主要是用epoll模型实现。支持高并发,我測试在有10000个client连接server的时候。server处理时间不到1秒,当然client仅仅是与server连接之后,接受server的欢迎消息而已,并没有做其它的通信。

    尽管程序比較简单。可是在我们考虑server高并发时也提供了一个思路。在这个程序中,我已经把全部的调试信息和一些与epoll无关的信息干掉。并加入必要的凝视,应该非常easy理解。

      程序共包括3个头文件和3个cpp文件。当中3个cpp文件里,每个cpp文件都是一个应用程序,server.cpp:server程序。client.cpp:单个client程序,tester.cpp:模拟高并发,开启10000个client去连server。

           utils.h头文件,就包括一个设置socket为不堵塞函数。例如以下:

        #include <fcntl.h>
        int setnonblocking(int sockfd)
        {
            CHK(fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0) | O_NONBLOCK));
            return 0;
        }
    
    

           local.h头文件,一些常量的定义和函数的声明,例如以下:


        #include "head.h"
        #define BUF_SIZE 1024        //默认缓冲区
        #define SERVER_PORT 11111    //监听port
        #define SERVER_HOST "127.0.0.1"    //服务器IP地址
        #define EPOLL_RUN_TIMEOUT -1    //epoll的超时时间
        #define EPOLL_SIZE 10000    //epoll监听的client的最大数目
    
        #define STR_WELCOME "Welcome to seChat! You ID is : Client #%d"
        #define STR_MESSAGE "Client #%d>> %s"
        #define STR_NOONE_CONNECTED "Noone connected to server expect you!"
        #define CMD_EXIT "EXIT"
    
        //两个实用的宏定义:检查和赋值而且检測
        #define CHK(eval) if(eval < 0){perror("eval"); exit(-1);}
        #define CHK2(res, eval) if((res = eval) < 0){perror("eval"); exit(-1);}
    
        int setnoblock(int sockfd);
        int handle_message(int new_fd);
    
    

         head.h文件。程序中所要用到的头文件,例如以下:

        //#ifndef __HEAD__H__
        //#define __HEAD__H__
        #include <stdio.h>
        #include <iostream>
        #include <netinet/in.h>
        #include <arpa/inet.h>
    
        #include <sys/socket.h>
    
        #include <unistd.h>
        #include <sys/epoll.h>
        #include <stdlib.h>
    
        #include <list>
        #include <string.h>
        #include <iterator>
        #include <time.h>
        //#endif
    
    

    server.cpp文件,epoll模型就在这里实现,例如以下:

        #include "local.h"
        #include "utils.h"
        #include "head.h"
        using namespace std;
    
        // 存放客户端socket描写叙述符的list
        list < int >clients_list;
    
        int main(int argc, char *argv[])
        {
            int listener;        //监听socket
            struct sockaddr_in addr, their_addr;
            addr.sin_family = PF_INET;
            addr.sin_port = htons(SERVER_PORT);
            addr.sin_addr.s_addr = inet_addr(SERVER_HOST);
            socklen_t socklen;
            socklen = sizeof(struct sockaddr_in);
    
            static struct epoll_event ev, events[EPOLL_SIZE];
            ev.events = EPOLLIN | EPOLLET;    //对读感兴趣,边沿触发
    
            char message[BUF_SIZE];
    
            int epfd;        //epoll描写叙述符
            clock_t tStart;        //计算程序执行时间
    
            int client, res, epoll_events_count;
    
            CHK2(listener, socket(PF_INET, SOCK_STREAM, 0));    //初始化监听socket
            setnonblocking(listener);    //设置监听socket为不堵塞
            CHK(bind(listener, (struct sockaddr *)&addr, sizeof(addr)));    //绑定监听socket
            CHK(listen(listener, 1));    //设置监听
    
            CHK2(epfd, epoll_create(EPOLL_SIZE));    //创建一个epoll描写叙述符。并将监听socket加入epoll
            ev.data.fd = listener;
            CHK(epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &ev));
    
            while (1) {
                CHK2(epoll_events_count, epoll_wait(epfd, events, EPOLL_SIZE, EPOLL_RUN_TIMEOUT));
                tStart = clock();
                for (int i = 0; i < epoll_events_count; i++) {
                    if (events[i].data.fd == listener)    //新的连接到来,将连接加入到epoll中。并发送欢迎消息
                    {
                        CHK2(client, accept(listener, (struct sockaddr *)&their_addr, &socklen));
                        setnonblocking(client);
                        ev.data.fd = client;
                        CHK(epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev));
    
                        clients_list.push_back(client);    // 加入新的客户端到list
                        bzero(message, BUF_SIZE);
                        res = sprintf(message, STR_WELCOME, client);
                        CHK2(res, send(client, message, BUF_SIZE, 0));
    
                    } else {
                        CHK2(res, handle_message(events[i].data.fd));    //注意:这里并没有调用epoll_ctl又一次设置socket的事件类型,但还是能够继续收到客户端发送过来的信息
                    }
                }
                printf("Statistics: %d events handled at: %.2f second(s)
    ", epoll_events_count, (double)(clock() - tStart) / CLOCKS_PER_SEC);
            }
    
            close(listener);
            close(epfd);
    
            return 0;
        }
    
        int handle_message(int client)
        {
            char buf[BUF_SIZE], message[BUF_SIZE];
            bzero(buf, BUF_SIZE);
            bzero(message, BUF_SIZE);
    
            int len;
    
            CHK2(len, recv(client, buf, BUF_SIZE, 0));    //接受客户端信息
    
            if (len == 0)        //客户端关闭或出错,关闭socket,并从list移除socket
            {
                CHK(close(client));
                clients_list.remove(client);
            } else            //向客户端发送信息
            {
                if (clients_list.size() == 1) {
                    CHK(send(client, STR_NOONE_CONNECTED, strlen(STR_NOONE_CONNECTED), 0));
                    return len;
                }
    
                sprintf(message, STR_MESSAGE, client, buf);
                list < int >::iterator it;
                for (it = clients_list.begin(); it != clients_list.end(); it++) {
                    if (*it != client) {
                        CHK(send(*it, message, BUF_SIZE, 0));
                    }
                }
            }
            return len;
        }
    
    

    tester.cpp文件,模拟server的高并发,开启10000个client去连接server。例如以下:

        #include "local.h"
        #include "utils.h"
        #include "head.h"
        using namespace std;
    
        char message[BUF_SIZE];        //接受服务器信息
        list < int >list_of_clients;    //存放全部客户端
        int res;
        clock_t tStart;
    
        int main(int argc, char *argv[])
        {
            int sock;
            struct sockaddr_in addr;
            addr.sin_family = PF_INET;
            addr.sin_port = htons(SERVER_PORT);
            addr.sin_addr.s_addr = inet_addr(SERVER_HOST);
    
            tStart = clock();
    
            for (int i = 0; i < EPOLL_SIZE; i++)    //生成EPOLL_SIZE个客户端,这里是10000个,模拟高并发
            {
                CHK2(sock, socket(PF_INET, SOCK_STREAM, 0));
                CHK(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0);
                list_of_clients.push_back(sock);
    
                bzero(message, BUF_SIZE);
                CHK2(res, recv(sock, message, BUF_SIZE, 0));
                printf("%s
    ", message);
            }
    
            list < int >::iterator it;    //移除全部客户端
            for (it = list_of_clients.begin(); it != list_of_clients.end(); it++)
                close(*it);
    
            printf("Test passed at: %.2f second(s)
    ", (double)(clock() - tStart) / CLOCKS_PER_SEC);
            printf("Total server connections was: %d
    ", EPOLL_SIZE);
    
            return 0;
        }
    
    

    我就不给出程序的运行结果的截图了,只是以下这张截图是代码作者自己測试的。能够看出。并发10000无压力呀

      单个客户端去连接server。client.cpp文件,例如以下:

        #include "local.h"
        #include "utils.h"
        #include "head.h"
    
        using namespace std;
    
        char message[BUF_SIZE];
    
        /*
            流程:
                调用fork产生两个进程,两个进程通过管道进行通信
                子进程:等待客户输入,并将客户输入的信息通过管道写给父进程
                父进程:接受server的信息并显示,将从子进程接受到的信息发送给server
        */
        int main(int argc, char *argv[])
        {
            int sock, pid, pipe_fd[2], epfd;
    
            struct sockaddr_in addr;
            addr.sin_family = PF_INET;
            addr.sin_port = htons(SERVER_PORT);
            addr.sin_addr.s_addr = inet_addr(SERVER_HOST);
    
            static struct epoll_event ev, events[2];
            ev.events = EPOLLIN | EPOLLET;
    
            //退出标志
            int continue_to_work = 1;
    
            CHK2(sock, socket(PF_INET, SOCK_STREAM, 0));
            CHK(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0);
    
            CHK(pipe(pipe_fd));
    
            CHK2(epfd, epoll_create(EPOLL_SIZE));
    
            ev.data.fd = sock;
            CHK(epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev));
    
            ev.data.fd = pipe_fd[0];
            CHK(epoll_ctl(epfd, EPOLL_CTL_ADD, pipe_fd[0], &ev));
    
            // 调用fork产生两个进程
            CHK2(pid, fork());
            switch (pid) {
            case 0:        // 子进程
                close(pipe_fd[0]);    // 关闭读端
                printf("Enter 'exit' to exit
    ");
                while (continue_to_work) {
                    bzero(&message, BUF_SIZE);
                    fgets(message, BUF_SIZE, stdin);
    
                    // 当收到exit命令时。退出
                    if (strncasecmp(message, CMD_EXIT, strlen(CMD_EXIT)) == 0) {
                        continue_to_work = 0;
                    } else {
                        CHK(write(pipe_fd[1], message, strlen(message) - 1));
                    }
                }
                break;
            default:        // 父进程
                close(pipe_fd[1]);    // 关闭写端
                int epoll_events_count, res;
                while (continue_to_work) {
                    CHK2(epoll_events_count, epoll_wait(epfd, events, 2, EPOLL_RUN_TIMEOUT));
    
                    for (int i = 0; i < epoll_events_count; i++) {
                        bzero(&message, BUF_SIZE);
                        if (events[i].data.fd == sock)    //从server接受信息
                        {
                            CHK2(res, recv(sock, message, BUF_SIZE, 0));
                            if (res == 0)    //server已关闭
                            {
                                CHK(close(sock));
                                continue_to_work = 0;
                            } else {
                                printf("%s
    ", message);
                            }
                        } else    //从子进程接受信息
                        {
                            CHK2(res, read(events[i].data.fd, message, BUF_SIZE));
                            if (res == 0) {
                                continue_to_work = 0;
                            } else {
                                CHK(send(sock, message, BUF_SIZE, 0));
                            }
                        }
                    }
                }
            }
            if (pid) {
                close(pipe_fd[0]);
                close(sock);
            } else {
                close(pipe_fd[1]);
            }
    
            return 0;
        }
    
    

    源码下载:
    linux-epoll-server-client-chat.tar.gz

    [root@localhost test]# g++ server.cpp -o server
    [root@localhost test]# g++ tester.cpp -o tester
    [root@localhost test]# g++ client.cpp -o client


    注意:

    linux下输入命令ulimit -a,能够看到open files 一般时1024,所以未经改动时做本实验,最大打开到1023号。所以要改动。

    改动方法:ulimit -n <能够同一时候打开的文件数目> 设置用户能够同一时候打开的最大文件数

    假设本參数设置过小,对于并发訪问量大的站点,可能会出现too many open files 的错误。


  • 相关阅读:
    判断
    数的读写
    单词长度
    c语言字符串大小写字母转换
    URL超长问题
    使用Loadrunner录制手机端http请求
    NoSql 数据库理解
    .NET, ASP.NET, ADO.NET, C# 区别
    Browser 对象
    装箱与拆箱
  • 原文地址:https://www.cnblogs.com/zsychanpin/p/7273927.html
Copyright © 2011-2022 走看看