Linux:基于epoll机制的socket通信

epoll定义及配置

  1. epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
  2. 目前epell是linux大规模并发网络程序中的热门首选模型
  3. epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

首先,使用cat /proc/sys/fs/file-max命令查看一个进程可以打开文件描述符的最大数目,如下图:
Linux:基于epoll机制的socket通信_第1张图片
然后,使用sudo vi /etc/security/limits.conf命令查看最大打开文件描述符的限制,并在该文件中写入以下配置:(如下图最后两行,即soft软限制和hard硬限制)
Linux:基于epoll机制的socket通信_第2张图片

epoll API

epoll_create

头文件:

#include

原函数:

int epoll_create(int size)

参数:

size: 告诉内核监听的数目

功能:

创建一个epoll句柄,参数size用来告诉内核监听的文件描述符个数,跟内存大小有关

epoll_ctl

头文件:

#include

原函数:

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

参数:

epfd: 为epoll_creat的句柄
op: 表示动作,用3个宏来表示:

  • EPOLL_CTL_ADD(注册新的fd到epfd),
  • EPOLL_CTL_MOD(修改已经注册的fd的监听事件),
  • EPOLL_CTL_DEL(从epfd删除一个fd);

event: 告诉内核需要监听的事件

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

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

Epoll_events操作模式:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(ET模式是相对于水平触发(LT)来说的)
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

epoll_wait

头文件:

#include

原函数:

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

参数:

events: 从内核得到事件的集合,
maxevents: 告知内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout: 超时时间,其中:

-1:阻塞
 0:立即返回,非阻塞
>0:指定微秒

返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

出错处理函数

wrap.h

#ifndef WRAP_H
#define WRAP_H

size_t mystrlen(const char *s);
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
void Bind(int fd, const struct sockaddr *sa, socklen_t salen);
void Connect(int fd, const struct sockaddr *sa, socklen_t salen);
void Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
void Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
static ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);

#endif

wrap.c

#include 
#include 
#include 
#include 
#include 
#include "wrap.h"

size_t mystrlen(const char *s)
{
    size_t len = 0;
    if (s == NULL)
    {
        return 0;
    }
    while (*s != '\n')
    {
        s++;
        len++;
    }
    return len;
}

void perr_exit(const char *s)
{
    perror(s);
    exit(1);
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
    int n;
again:
    if ( (n = accept(fd, sa, salenptr)) < 0) {
        if ((errno == ECONNABORTED) || (errno == EINTR))
            goto again;
        else
            perr_exit("accept error");
    }
    return n;
}

void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if (bind(fd, sa, salen) < 0)
        perr_exit("bind error");
}

void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if (connect(fd, sa, salen) < 0)
        perr_exit("connect error");
}

void Listen(int fd, int backlog)
{
    if (listen(fd, backlog) < 0)
        perr_exit("listen error");
}

int Socket(int family, int type, int protocol)
{
    int n;
    if ( (n = socket(family, type, protocol)) < 0)
        perr_exit("socket error");
    return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
    ssize_t n;
again:
    if ( (n = read(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
    ssize_t n;
again:
    if ( (n = write(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

void Close(int fd)
{
    if (close(fd) == -1)
        perr_exit("close error");
}

ssize_t Readn(int fd, void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nread;
    char *ptr;
    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ( (nread = Read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;
            else
                return -1;
        } else if (nread == 0)
            break;
        nleft -= nread;
        ptr += nread;
    }
    return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ( (nwritten = Write(fd, ptr, nleft)) <= 0) {
            if (nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

static ssize_t my_read(int fd, char *ptr)
{
    static int read_cnt;
    static char *read_ptr;
    static char read_buf[100];
    if (read_cnt <= 0) {
again:
        if ( (read_cnt = Read(fd, read_buf, sizeof(read_buf))) < 0) {
            if (errno == EINTR)
                goto again;
            return -1;
        } else if (read_cnt == 0)
            return 0;
        read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
    ssize_t n, rc;
    char c, *ptr;
    ptr = vptr;
    for (n = 1; n < maxlen; n++) {
        if ( (rc = my_read(fd, &c)) == 1) {
            *ptr++ = c;
            if (c == '\n')
                break;
        } else if (rc == 0) {
            *ptr = 0;
            return n - 1;
        } else
            return -1;
    }
    *ptr = 0;
    return n;
}

服务器

server.h

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 8000
#define OPEN_MAX 1024

int main(int argc, char *argv[])
{
    int i, j, maxi, listenfd, connfd, sockfd;//listenfd监听描述符,connfd连接描述符,sockfd通信描述符
    int nready, efd, res;
    ssize_t n; //用来表示可被执读写操作的数据块大小
    char buf[MAXLINE], str[INET_ADDRSTRLEN];
    socklen_t clilen; //客户端地址长度
    int client[OPEN_MAX]; //用于查看客户端连接状态的数组
    struct sockaddr_in servaddr, cliaddr; //定义sockaddr_in结构体类型变量
    struct epoll_event tep, ep[OPEN_MAX]; //定义epoll_event结构体类型变量

    listenfd = Socket(AF_INET, SOCK_STREAM, 0); //创建监听套接字描述符

    bzero(&servaddr, sizeof (servaddr)); //清零整个servaddr结构体
    servaddr.sin_family = AF_INET; //设置地址类型
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //设置网络地址
    servaddr.sin_port = htons(SERV_PORT); //设置端口号

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof (servaddr)); //绑定listenfd和servaddr

    Listen(listenfd, 20); //设置监听成功则返回一个新的socket文件描述符,用于和客户端通信,失败返回-1

    for(i = 0; i< OPEN_MAX; i++)
    {
        client[i] = -1; //初始化client[]下标为-1
    }
    maxi = -1;

    efd = epoll_create(OPEN_MAX); //创建一个epoll句柄,efd指向红黑树根节点,参数size用来告诉内核监听的文件描述符个数
    if(efd == -1) //出错处理
    {
        perr_exit("epoll_create failed.");
    }

    tep.events = EPOLLIN; //指定监听读事件,默认为水平触发LT
    tep.data.fd = listenfd; //一般的epoll在此存放fd

    res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); //将listenfd和对应的epoll_event结构体设置到树上,即控制某个epoll监控的文件描述符上的事件:注册、修改、删除
    if(res == -1) //出错处理
    {
        perr_exit("epoll_ctl failed.");
    }

    for( ; ; )
    {
        nready = epoll_wait(efd, ep, OPEN_MAX, -1); //为server阻塞(默认)监听事件,等待所监听的文件描述符上有事件的发生。ep中存放满足条件后的所有事件结构体
        if(nready == -1) //出错处理
        {
            perr_exit("epoll_wait failed.");
        }
        for(i = 0; i < nready; i++) //遍历就绪的文件描述符
        {
            if(!(ep[i].events & EPOLLIN)) //该事件的文件描述符不可读,则跳过
            {
                continue;
            }
            if(ep[i].data.fd == listenfd) //ep数据域存放的描述符即红黑树上节点的值和监听描述符相同,说明有新连接到来
            {
                clilen = sizeof (servaddr);
                connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); //接收客户端连接
                printf("received from %s at PORT %d\n",
                       inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof (str)), ntohs(cliaddr.sin_port)); //打印客户端IP及端口号

                for(j = 0; j < OPEN_MAX; j++) //将接入的客户端套接字描述符加入client数组
                {
                    if(client[j] < 0)
                    {
                        client[j] = connfd; //保存描述符
                        break;
                    }
                }
                if(j == OPEN_MAX) //出错处理
                {
                    perr_exit("too many clients.");
                }
                if(j > maxi) //更新maxi的值(maxi代表最后一个连接的客户端的套接字)
                {
                    maxi = j; //client[]数组中的最大索引
                }
                tep.events = EPOLLIN; //指定监听读事件,默认为水平触发
                tep.data.fd = connfd; //存放连接描述符
                res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); //注册新的connfd到efd,即将新连接的客户端套接字加入红黑树并继续监听
                if(res == -1) //出错处理,新客户端加入失败
                {
                    perr_exit("epoll_ctl failed.");
                }
            }
            else //红黑树上节点的值不为listenfd,说明有客户端想要读/写数据
            {
                sockfd = ep[i].data.fd; //从红黑树中读取通信套接字描述符给sockfd
                n = Read(sockfd, buf, MAXLINE); //读取缓冲区数据到buf
                if(n == 0) //客户端关闭或无发送数据
                {
                    for(j = 0; j <= maxi; j++)
                    {
                        if(client[i] == sockfd) //如果关闭的客户端通信套接字还在client数组中,则将其置为-1
                        {
                            client[j] = -1;
                            break;
                        }
                    }
                    res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); //将sockfd从efd中删除,也就是将关闭连接的客户端通信套接字从红黑树中删除
                    if(res == -1) //出错处理,下树失败
                    {
                        perr_exit("epoll_ctl failed.");
                    }
                    Close(sockfd);
                    printf("client[%d] closed connection.\n", j);
                }
                else //处理在缓冲区读到的数据
                {
                    for(j = 0; j < n; j++)
                    {
                        buf[j] = toupper(buf[j]);
                    }
                    Writen(sockfd, buf, n); //将字节数为n的数据字符串buf写入sockfd中
                }
            }
        }
    }
    Close(listenfd);
    Close(efd);
    return 0;
}

客户端

client.c

#include 
#include 
#include 
#include 
#include 
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 8000

int main()
{
    struct sockaddr_in servaddr; //定义sockaddr_in结构体类型变量
    char buf[MAXLINE];
    int sockfd, n;

    sockfd = Socket(AF_INET, SOCK_STREAM, 0); //打开一个网络通讯端口,成功返回一个文件描述符

    //初始化sockaddr_in结构体
    bzero(&servaddr, sizeof (servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof (servaddr)); //与服务器建立连接

    while(fgets(buf, MAXLINE, stdin) != NULL)
    {
        Write(sockfd, buf, strlen(buf));
        n = Read(sockfd, buf, MAXLINE);

        if(n == 0)
        {
            printf("the other side has been closed.\n");
        }
        else
        {
            Write(STDIN_FILENO, buf, n);
        }
    }
    Close(sockfd);

    return 0;
}

运行结果:

(此epoll模型的功能为将输入的小写字符串,转换为大写字符串)
Linux:基于epoll机制的socket通信_第3张图片

附:epoll触发模式思维导图

Linux:基于epoll机制的socket通信_第4张图片

你可能感兴趣的:(Linux)