11 Linux I/O多路复用


前言 (含目录)


多路I/O共用一个线程或进程

  • 先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该表中.
  • 然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数返回. (该函数为阻塞函数) (函数对文件描述符的检测操作是由内核完成的)
  • 在返回时,它告诉进程有多少(或哪些)文件描述符要进行I/O操作.

此篇的服务器端也可配合上一篇的客户端测试





select

/**
 * @param nfds 要检测的文件描述符中最大的fd + 1  (1024)
 * @param readfds 读集合
 * @param writefds 写集合
 * @param exceptfds 异常集合
 * @param timeout
 * @return I/O发生变化的文件描述符的个数
 * 				NULL 永久阻塞,直到检测到fd引用的对象发生变化时
 * 				== 0 不阻塞
 * 				> 0  设置多少,阻塞多久
 */
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

// 文件描述符集类型
// 文件描述符操作函数
// // // // // // // 
// 全部清空
void FD_ZERO(fd_set *set);
// 从集合中删除某一项
void FD_CLR(int fd, fd_set *set);
// 将某个文件描述符添加到集合
void FD_SET(int fd, fd_set *set);
// 判断某个文件描述符是否在集合中
int FD_ISSET(int fd, fd_set *set);

struct timeval
{
	long tv_sec;
	long tv_usec;
};
/**
 * @name server.c
 * @author IYATT-yx
 * @brief select I/O多路复用
 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdbool.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>

int main(void)
{
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in server;
    socklen_t serverLen = sizeof(server);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    server.sin_port = htons(11111);

    int flag = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));

    if (bind(lfd, (struct sockaddr *)&server, serverLen) == -1)
    {
        perror("bind error");
        exit(EXIT_FAILURE);
    }

    if (listen(lfd, 20) == -1)
    {
        perror("listen error");
        exit(EXIT_FAILURE);
    }
    printf("服务器启动监听...\n");

    struct sockaddr_in client;
    socklen_t clientLen = sizeof(client);

    // 最大的文件描述符
    int maxfd = lfd;
    // 文件描述符读集合
    fd_set read, temp;
    // 初始化
    FD_ZERO(&read);
    FD_SET(lfd, &read);
    // 保存建立连接的IP和端口, 状态码
    char ipbuf[1024][64];
    uint16_t port[1024];
    bool status[1024] = {false};
    while (true)
    {
        // 让内核做I/O检测
        temp = read;
        if (select(maxfd + 1, &temp, NULL, NULL, NULL) == -1)
        {
            perror("select error");
            exit(EXIT_FAILURE);
        }
        
        // 有客户端发起了新连接
        if (FD_ISSET(lfd, &temp))
        {
            // 接收连接
            int cfd = accept(lfd, (struct sockaddr *)&client, &clientLen);
            if (cfd == -1)
            {
                perror("accept error");
                exit(EXIT_FAILURE);
            }

            // 将cfd加入下一次待检测的读集合中
            FD_SET(cfd, &read);
            // 更新最大的文件描述符
            maxfd = maxfd > cfd ? maxfd : cfd;
        }
        // 已经连接的客户端有数据送达
        for (int i = lfd + 1; i <= maxfd; ++i)
        {
            if (FD_ISSET(i, &temp))
            {
                char buf[1024] = {0};
                ssize_t len = recv(i, buf, sizeof(buf), 0);
                if (len == -1)
                {
                    perror("recv error");
                    exit(EXIT_FAILURE);
                }
                else if (len == 0)
                {
                    printf("客户端%s:%d断开了连接\n", ipbuf[i], port[i]);
                    close(i);
                    status[i] = false;
                    // 从读集合中删除
                    FD_CLR(i, &read);
                }
                else
                {
                    if (status[i] == false)
                    {
                        inet_ntop(AF_INET, &client.sin_addr.s_addr, ipbuf[i], sizeof(ipbuf[0]));
                        port[i] = ntohs(client.sin_port);
                        status[i] = true;
                    }
                    printf("收到来自%s:%d消息: %s\n", ipbuf[i], port[i], buf);
                    memset(buf, 0, sizeof(buf));
                    strcpy(buf, "服务器已收到数据\n");
                    send(i, buf, strlen(buf) + 1, 0);
                }
                
            }
        }
    }

    close(lfd);
}




poll

/**
 * @param fds 数组的地址
 * @param nfds 数组的最大长度 (数组中最后使用的一个元素的下标 + 1)
 * @param timeout
 * 					-1 永久阻塞
 * 					0  调完后立即返回
 * 					>0 阻塞时常 (ms)
 * @return I/O发生变化的文件描述符的个数
 */
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd
{
	// 文件描述符
	int fd;
	// 等待的事件
	short events;
	// 实际发生的事件
	short revents;
};
事件常值作为events的值作为revents的值说明
读事件 POLLIN普通或优先带数据可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读
POLLPRI高优先级数据可读
写事件POLLOUT普通或优先带数据可读
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
错误事件POLLERR发生错误
POLLHUP发生挂起
POLLNVAL描述不是打开的文件




epoll

/**
 * @brief 创建句柄
 * @param size epoll关注的最大文件描述符数
 */
int epoll_create(int size);

/**
 * @brief 事件注册
 * @param epfd 前一个函数生成的epoll专用文件描述符
 * @param op 
 * 			EPOLL_CTL_ADD		注册
 * 			EPOLL_CTL_MOD		修改
 * 			EPOLL_CTL_DEL		删除
 * @param fd 关联的文件描述符
 * @param event 告诉内核要监听什么事件
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

struct epoll_event
{
	uint32_t events;
	epoll_data_t data;
};
// events:
// EPOLLIN 读
// EPOLLOUT 写
// EPOLLERR 异常

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

/**
 * @brief 等待I/O事件产生
 * @param epfd 要检测的句柄
 * @param events 用于回传待处理事件的数组
 * @param maxevents 告诉内核这个events的大小
 * @param timeout
 * 				-1		永久阻塞
 * 				0		立即返回
 * 				>0		阻塞的时间 (ms)
 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/**
 * @name server.c
 * @author IYATT-yx
 * @brief epoll I/O多路复用
 */
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>

// 自定义数据结构用于用于存放连入的客户端的IP和端口信息以及对应的文件描述符
typedef struct info
{
    int fd;
    char ipbuf[64];
    uint16_t port;
} info;

int main(void)
{
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    server.sin_port = htons(11111);
    socklen_t serverLen = sizeof(server);

    int flag = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));

    if (bind(lfd, (struct sockaddr *)&server, serverLen) == -1)
    {
        perror("bind error");
        exit(EXIT_FAILURE);
    }

    if (listen(lfd, 20) == -1)
    {
        perror("listen error");
        exit(EXIT_FAILURE);
    }
    printf("服务器开始监听...\n");


    struct sockaddr_in client;
    socklen_t clientLen = sizeof(client);

    // 创建epoll树根节点
    int epfd = epoll_create(1024);
    // 初始化epoll树
    struct epoll_event ev;
    struct info ino;
    ev.events = EPOLLIN;
    ino.fd = lfd;
    ev.data.ptr = &ino;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev) == -1)
    {
        perror("初始化epoll_ctl error");
        exit(EXIT_FAILURE);
    }

    struct epoll_event all[1024];
    while (true)
    {
        // 使用epoll通知内核
        int ret = epoll_wait(epfd, all, 1024, -1);
        if (ret == -1)
        {
            perror("epoll_wait error");
            exit(EXIT_FAILURE);
        }

        // 遍历all数组的前ret个元素
        for (int i = 0; i < ret; ++i)
        {
            info *inf = (info *)all[i].data.ptr;
            // 判断是否有新的连接
            if (inf->fd == lfd)
            {
                // 接收连接请求
                int cfd = accept(lfd, (struct sockaddr *)&client, &clientLen);
                if (cfd == -1)
                {
                    perror("accept error");
                    exit(EXIT_FAILURE);
                }

                // 将新得到的cfd, 对应的IP和端口挂到epoll树上
                info *infTemp = (info *)malloc(sizeof(inf));
                infTemp->fd = cfd;
                inet_ntop(AF_INET, &client.sin_addr.s_addr, infTemp->ipbuf, sizeof(infTemp->ipbuf));
                infTemp->port = ntohs(client.sin_port);

                struct epoll_event eventTemp;
                eventTemp.events = EPOLLIN;
                eventTemp.data.ptr = infTemp;

                if (epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &eventTemp) == -1)
                {
                    perror("添加epoll_ctl error");
                    exit(EXIT_FAILURE);
                }
            }
            else
            {
                // 处理已连接的客户端发送过来的数据
                if (!all[i].events & EPOLLIN)
                {
                    continue;
                }

                // 读数据
                char buf[1024] = {0};
                ssize_t len = recv(inf->fd, buf, sizeof(buf), 0);
                if (len == -1)
                {
                    perror("recv error");
                    exit(EXIT_FAILURE);
                }
                else if (len == 0)
                {
                    printf("客户端%s:%d断开了连接\n", inf->ipbuf, inf->port);
                    if (epoll_ctl(epfd, EPOLL_CTL_DEL, inf->fd, NULL) == -1)
                    {
                        perror("删除epoll_ctl error");
                        exit(EXIT_FAILURE);
                    }
                    close(inf->fd);
                }
                else
                {
                    printf("收到%s:%d的信息: %s\n", inf->ipbuf, inf->port, buf);
                    memset(buf, 0, sizeof(buf));
                    strcpy(buf, "服务器已收到消息");
                    send(inf->fd, buf, strlen(buf) + 1, 0);
                }
            }
            
        }
    }
    close(lfd);
}
epoll的工作模式
水平触发模式 (默认)fd的缓冲区有数据就返回,与数据发送次数无关
边沿触发模式 EPOLLET发送一次就返回一次,不管数据是否读完
边沿非阻塞触发模式在边沿触发模式上,对文件描述符设置非阻塞
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值