网络学习(三)--高并发的socket编程(epoll)

目录

一、引言
二、高并发背景
------> 2.1、select
三、epoll
------> 3.1、功能分离
------> 3.2、就绪列表
------> 3.3、有关epoll的EPOLLOUT
------> 3.4、代码讲解
------> 3.5、流程总结

一、引言

前一章讲了socket的编程,这章来介绍一下高并发服务器的相关机制
原文很大程度参考了这篇文章Epoll原理解析

二、高并发背景

我们回顾一下上章讲的socket编程,在主机和服务器链接成功,开始通讯后,主机调用recv()来获取数据,当程序运行到 Recv 时,它会一直等待,直到接收到数据才往下执行

一个 Socket 对应着一个端口号,而网络数据包中包含了 IP 和端口的信息,内核可以通过端口号找到对应的 Socket。
那么,如何监视多个 Socket 呢?这就是本章要将的东西

服务端需要管理多个客户端连接,而 Recv 只能监视单个 Socket,这种矛盾下,人们开始寻找监视多个 Socket 的方法。Epoll 的要义就是高效地监视多个 Socket。而在epoll之前,一直采用的就是select和pol机制,两者很像,这里就只讲select了

1、select

在下边的代码中,先准备一个数组 FDS,让 FDS 存放着所有需要监视的 Socket。
然后调用 Select,如果 FDS 中的所有 Socket 都没有数据,Select 会阻塞,直到有一个 Socket 接收到数据,Select 返回,唤醒进程。
用户可以遍历 FDS,通过 FD_ISSET 判断具体哪个 Socket 收到数据,然后做出处理。

int s = socket(AF_INET, SOCK_STREAM, 0);   
bind(s, ...) 
listen(s, ...) 
 
int fds[] =  存放需要监听的socket 
 
while(1){ 
    int n = select(..., fds, ...) 
    for(int i=0; i < fds.count; i++){ 
        if(FD_ISSET(fds[i], ...)){ 
            //fds[i]的数据处理 
        } 
    } 
} 
Select 的流程

Select 的实现思路很直接,假如程序同时监视如下图的 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后
1、操作系统把进程 A 分别加入这三个 Socket 的等待队列中。
2、当任何一个 Socket 收到数据后,中断程序将唤起进程。所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面
3、经由这些步骤,当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。

select的缺点

这种简单方式行之有效,在几乎所有操作系统都有对应的实现。但是简单的方法往往有缺点,主要是:
1、次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定 Select 的最大监视数量,默认只能监视 1024 个 Socket。
2、进程被唤醒后,程序并不知道哪些 Socket 收到数据,还需要遍历一次。

为了解决这些缺点,就引入了epoll机制

三、epoll

Epoll 是在 Select 出现 N 多年后才被发明的,是 Select 和 Poll(Poll 和 Select 基本一样,有少量改进)的增强版本。Epoll 通过以下一些措施来改进效率:

1、功能分离

Select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。
在这里插入图片描述
相比 Select,Epoll 拆分了功能

如上图所示,每次调用 Select 都需要这两步操作,然而大多数应用场景中,需要监视的 Socket 相对固定,并不需要每次都修改。

Epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升。

为方便理解后续的内容,我们先了解一下 Epoll 的用法。如下的代码中,先用 epoll_create 创建一个 Epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的 Socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据:

int s = socket(AF_INET, SOCK_STREAM, 0);    
bind(s, ...) 
listen(s, ...) 
 
int epfd = epoll_create(...); 
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中 
 
while(1){ 
    int n = epoll_wait(...) 
    for(接收到数据的socket){ 
        //处理 
    } 
} 
2、就绪列表

Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历。

当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中 Epfd 所代表的对象)。

创建 Epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 Socket。如果通过 epoll_ctl 添加 Sock1、Sock2 和 Sock3 的监视,内核会将 eventpoll 添加到这三个 Socket 的等待队列中。

当 Socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程,同时中断程序会给 eventpoll 的“就绪列表”添加 Socket 引用。

eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。

当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。

epoll_wait 阻塞进程时内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。

当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态
Epoll 唤醒进程也因为 Rdlist 的存在,进程 A 可以知道哪些 Socket 发生了变化。

3、有关epoll的EPOLLOUT

EPOLLIN(读)监听事件的类型,大家一般使用起来一般没有什么疑问,无非是监听某个端口,一旦客户端连接的服务端有数据发送,它马上通知服务端有数据,一般用一个回调的读函数,从这个相关的socket接口读取数据就行了。

但是有关EPOLLOUT(写)监听的使用,网上的资料却讲得不够明白,理解起来有点麻烦。因为监听一般都是被动操作,客户端有数据上来需要读写(被动的读操作,EPOLIN监听事件很好理解,但是服务器给客户发送数据是个主动的操作,写操作如何监听呢?

如果将客户端的socket接口都设置成 EPOLLIN | EPOLLOUT(读,写)两个操作都设置,那么这个写操作会一直监听,有点影响效率。经过查阅大量资料,我终于明白了EPOLLOUT(写)监听的使用场,一般说明主要有以下三种使用场景:

1: 对客户端socket只使用EPOLLIN(读)监听,不监听EPOLLOUT(写),写操作一般使用socket的send操作

2:客户端的socket初始化为EPOLLIN(读)监听,有数据需要发送时,对客户端的socket修改为EPOLLOUT(写)操作,这时EPOLL机制会回调发送数据的函数,发送完数据之后,再将客户端的socket修改为EPOLL(读)监听

3:对客户端socket使用EPOLLIN 和 EPOLLOUT两种操作,这样每一轮epoll_wait循环都会回调读,写函数,这种方式效率不是很好

4、代码讲解

这里的写操作监听使用了上述的第二个方法

int main(int argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;

......
    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
    struct epoll_event ev,events[20];
    
    //生成用于处理accept的epoll专用的文件描述符
    epfd=epoll_create(256);
    
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    //设置与要处理的事件相关的文件描述符
    ev.data.fd=listenfd;
    
    //设置要处理的事件类型
    ev.events=EPOLLIN|EPOLLET;
    //ev.events=EPOLLIN;

    //注册epoll事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);

    serveraddr.sin_port=htons(portnumber);
    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for ( ; ; ) {
        //等待epoll事件的发生

        nfds=epoll_wait(epfd,events,20,500);
        //处理所发生的所有事件

        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                //setnonblocking(connfd);

                char *str = inet_ntoa(clientaddr.sin_addr);
                cout << "accapt a connection from " << str << endl;
                //设置用于读操作的文件描述符

                ev.data.fd=connfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                //ev.events=EPOLLIN;

                //注册ev
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。
            {
                cout << "EPOLLIN" << endl;
                if ( (sockfd = events[i].data.fd) < 0)
                    continue;
                if ( (n = read(sockfd, line, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                        close(sockfd);
                        events[i].data.fd = -1;
                    } else
                        std::cout<<"readline error"<<std::endl;
                } else if (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                line[n] = '/0';
                cout << "read " << line << endl;
                //设置用于写操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的写操作事件

                ev.events=EPOLLOUT|EPOLLET;
                
                //修改sockfd上要处理的事件为EPOLLOUT,EPOLL机制会回调发送数据的函数
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
            else if(events[i].events&EPOLLOUT) // 如果有数据发送
            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                //设置用于读操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                
                //修改sockfd上要处理的事件为EPOLIN,发送完数据之后,再将客户端的socket修改为EPOLL(读)监听
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
    }
    return 0;
}
5、流程总结

主要流程如下
1、epoll_create创建用于处理accept的epoll专用的文件描述符
2、创建监听socket,将其放入epoll_event 中要处理的文件描述符,并且填充事件类型,其中EPOLLLT和EPOLLET的区别
3、之后bind、listen开始监听连接套接字
4、epoll_wait 先判断epoll_event 中的文件描述符,如果是监听描述符,则表示有新连接发生,accept一个新的套接字,重新填充epoll_event ,再使用epoll_ctl注册到epoll的描述符内
5、epoll_wait 如果是已经连接的用户,判断epoll_wait 类型(EPOLLIN/EPOLLOUT),分别调用read、write

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

文艺小少年

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

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

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

打赏作者

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

抵扣说明:

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

余额充值