网络编程---IO复用方法

select

函数是一个在多路复用I/O中经常使用的系统调用,它允许你监视多个文件描述符(通常是套接字)的状态,以确定哪些文件描述符可以进行读取、写入或者已经发生了异常等情况。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

//nfds:被监视的文件描述符的数量,通常设置为最大文件描述符加 1。

//readfds:指向一个 fd_set 结构的指针,用于监视可读事件(数据已经准备好读取的套接字)。

//writefds:指向一个 fd_set 结构的指针,用于监视可写事件(数据可以写入的套接字)。

//exceptfds:指向一个 fd_set 结构的指针,用于监视异常事件。

//timeout:设置 select 调用的超时时间,它是一个 struct timeval 结构,可以设置为 NULL 表示无限等待,也可以设置为一个时间间隔,使 select 在指定的时间内超时返回。

工作方式

select 函数的工作方式如下:

①当调用 select 时,它会检查被监视的文件描述符集合(readfds、writefds 和 exceptfds)以查看哪些文件描述符已经准备好了。如果有任何文件描述符处于就绪状态,select 返回;否则它会等待,直到有文件描述符变为就绪状态或者超时。

②一旦 select 返回,我们可以遍历文件描述符集合,检查每个文件描述符的状态以确定它们的就绪状态。

③通常,我们会使用 FD_ISSET 宏来检查文件描述符是否在集合中。如果在集合中,表示相应的操作可以执行(读、写或异常)。

fd_set结构体

fd_set 是一个用于表示文件描述符集合的数据结构,它通常用于 select 函数中,用来指定需要监视的文件描述符。

操作和用法

①初始化 fd_set:在使用 fd_set 之前,通常需要使用 FD_ZERO 来将其初始化为空集合,表示没有任何文件描述符。

fd_set readfds;
FD_ZERO(&readfds);

②添加文件描述符:当要监视一个文件描述符,可以使用 FD_SET 将其添加到集合中。

int sockfd = ...;  // 假设有一个套接字
FD_SET(sockfd, &readfds);

③检查文件描述符状态:可以使用 FD_ISSET 宏来检查文件描述符是否在集合中,以确定其状态。通常,这是在 select 返回后用于检查就绪状态的操作。

if (FD_ISSET(sockfd, &readfds)) 
{
    // sockfd 已经准备好进行读取操作
}

④从集合中移除文件描述符:如果你不再需要监视某个文件描述符,可以使用 FD_CLR 将其从集合中移除。

FD_CLR(sockfd, &readfds);

使用fd_set的注意事项

①fd_set 是一个有限大小的数据结构,通常大小由操作系统定义。因此,它不能包含所有可能的文件描述符,特别是在高并发的网络应用中可能会受到限制。

②在不同的操作系统上, fd_set 的大小可能会有所不同。你可以使用 FD_SETSIZE 宏来获取 fd_set 的大小限制。

③当使用 select 函数时,它会修改 fd_set,将其中不符合就绪条件的文件描述符从集合中移除,因此在每次调用 select 之前,通常需要重新初始化 fd_set。

④fd_set 通常是一个全局变量,因为它需要在 select 调用之前和之后都可见。

select实例

接下来写一个基于非阻塞 I/O 多路复用的简单服务器程序。它可以处理多个客户端连接,同时监听客户端发来的数据,然后回复 "ok"。

服务器端

#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/socket.h>
#include<sys/select.h>
#include <arpa/inet.h>

#define MAXFD 10  //定义了最大文件描述符数量,即服务器能够处理的最大连接数。

using namespace std;

int socket_init()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
    if (sockfd == -1)
    {
        return -1;
    }

    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;//地址族
    saddr.sin_port = htons(6000);//端口号
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//地址

    int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));

    if (res == -1)
    {
        cout << "bind error" << endl;
        return -1;
    }

    res = listen(sockfd, 5);

    if (res == -1)
    {
        return -1;
    }

    return sockfd;
}

//初始化文件描述符数组,将所有元素设为 -1
void fds_init(int fds[])
{
    for (int i = 0; i < MAXFD; i++)
    {
        fds[i] = -1;
    }
}

//向文件描述符数组中添加文件描述符
void fds_add(int fds[], int fd)
{
    for (int i = 0; i < MAXFD; i++)
    {
        if (fds[i] == -1)
        {
            fds[i] = fd;
            break;
        }
    }
}

//从文件描述符数组中删除文件描述符
void fds_del(int fds[], int fd)
{
    for (int i = 0; i < MAXFD; i++)
    {
        if (fds[i] == fd)
        {
            fds[i] = -1;
            break;
        }
    }
}

//接收客户端数据,如果客户端关闭连接,则从文件描述符数组中删除该套接字
void accept_client(int sockfd, int fds[])
{
    int c = accept(sockfd, NULL, NULL);
    if (c < 0)
    {
        return;
    }

    cout << "c= " << c << endl;

    fds_add(fds, c);
}

void recv_data(int c, int fds[])
{
    char buff[128] = { 0 };
    int n = recv(c, buff, 127, 0);
    if (n <= 0)
    {
        fds_del(fds, c);
        close(c);
        cout << "client close" << endl;
        return;
    }
    cout << "buff " << c << "=" << buff << endl;
    send(c, "ok", 2, 0);
}

int main()
{
    // 创建套接字
    int sockfd = socket_init();
    //创建文件描述符集合
    fd_set fdset;
    //初始化文件描述符数组
    int fds[MAXFD];
    fds_init(fds);
    //添加套接字到数组中
    fds_add(fds, sockfd);

    while (1)
    {
        // 清空文件描述符集合并找到最大的文件描述符
        FD_ZERO(&fdset);
        
        //确定监听范围,即确定有几个待处理文件描述符
        int maxfd = -1;
        for (int i = 0; i < MAXFD; i++)
        {
            if (fds[i] == -1)// 说明此位置没有文件描述符
            {
                continue;
            }
            //将文件描述符添加到集合中
            FD_SET(fds[i], &fdset);

            if (maxfd < fds[i])
            {
                maxfd = fds[i];
            }
        }

        struct timeval tv = { 5, 0 };//设定超时时间为5秒
        
        int n = select(maxfd + 1, &fdset, NULL, NULL, &tv);
        if (n == -1)
        {
            cout << "select error" << endl;
        }
        else if (n == 0)
        {
            cout << "time out" << endl;
        }
        else
        {
            for (int i = 0; i < MAXFD; i++)
            {
                if (fds[i] == -1)
                {
                    continue;
                }
                //检查文件描述符并确定其状态
                if (FD_ISSET(fds[i], &fdset))
                {
                    if (fds[i] == sockfd) // 表明有新连接请求
                    {
                        accept_client(sockfd, fds);
                    }
                    else//表明是已连接的客户端发送数据
                    {
                        recv_data(fds[i], fds);
                    }
                }
            }
        }
    }
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main() 
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) 
    {
        perror("Socket creation failed");
        exit(1);
    }

    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if (res == -1) 
    {
        perror("Connect error");
        exit(1);
    }

    while (1) 
    {
        char buff[128] = {0};
        printf("Input (type 'end' to exit):\n");
        fgets(buff, 128, stdin);
        if (strcmp(buff, "end\n") == 0) 
        {
            break;
        }
        send(sockfd, buff, strlen(buff) - 1, 0);
        memset(buff, 0, 128);
        recv(sockfd, buff, 127, 0);
        printf("Received: %s\n", buff);
    }

    close(sockfd);
    exit(0);
}

运行结果:

poll

poll() 函数是一个用于进行多路复用的系统调用,它与 select() 类似.

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

//fds: 是一个指向 pollfd 结构体数组的指针,每个结构体描述一个待监视的文件描述符以及监视的事件类型。

//nfds: 是 fds 数组的大小,即待监视的文件描述符的数量。

//timeout: 是超时时间,以毫秒为单位。如果设置为负数,poll() 会一直阻塞直到有事件发生。如果设置为0,poll() 会立即返回。如果设置为正数,poll() 会等待指定时间后返回,不管是否有事件发生。

pollfd 结构体

struct pollfd 
{
    int fd;         // 文件描述符
    short events;   // 待监视的事件类型
    short revents;  // 实际发生的事件类型(由内核填充)
};

//events: 要监视的事件类型,可以是以下之一或它们的组合:

POLLIN文件描述符上有可读数据。
POLLOUT文件描述符上可以写入数据。
POLLPR有紧急数据可读(如带外数据)
POLLERR发生错误
POLLHUP发生挂起事件
POLLNVAL文件描述符不是一个有效的打开文件

返回值:

大于0:表示有事件发生,返回值为就绪文件描述符的数量。

等于0:表示超时,没有文件描述符处于就绪状态。

小于0:表示出现错误

poll实例

服务器端

#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/socket.h>
#include<sys/select.h>
#include <arpa/inet.h>
#include<poll.h>

#define MAXFD 10

using namespace std;

int socket_init()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
    if (sockfd == -1)
    {
        return -1;
    }

    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;//地址族
    saddr.sin_port = htons(6000);//端口号
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//地址

    int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));

    if (res == -1)
    {
        cout << "bind error" << endl;
        return -1;
    }

    res = listen(sockfd, 5);

    if (res == -1)
    {
        return -1;
    }

    return sockfd;
}

void fds_init(struct pollfd fds[])
{
    for (int i = 0; i < MAXFD;i++)
    {
        fds[i].fd = -1;
        fds[i].events = 0;
        fds[i].revents = 0;
    }
}

void fds_add(struct pollfd fds[],int fd)
{
    for (int i = 0; i < MAXFD;i++)
    {
        if(fds[i].fd==-1)
        {
            fds[i].fd = fd;
            fds[i].events = POLLIN;//r
            fds[i].revents = 0;
            break;
        }
    }
}

void fds_del(struct pollfd fds[],int fd)
{
    for (int i = 0; i < MAXFD;i++)
    {
        if(fds[i].fd==fd)
        {
            fds[i].fd = -1;
            fds[i].events = 0;
            fds[i].revents = 0;
            break;
        }
    }
}

void accept_client(int sockfd,struct pollfd fds[])
{
    int c = accept(sockfd, NULL, NULL);
    if(c<0)
    {
        return;
    }
    cout << "accept c =" << c << endl;

    fds_add(fds, c);
}

void recv_data(int c,struct pollfd fds[])
{
    char buff[128] = {0};
    int n = recv(c, buff, 127, 0);
    if(n<=0)
    {
        fds_del(fds, c);
        close(c);
        cout << "client close" << endl;
        return;
    }
    cout << "recv buff =" << buff << endl;
    send(c, "ok", 2, 0);
}

int main()
{
    int sockfd = socket_init();
    if(sockfd==-1)
    {
        exit(1);
    }

    struct pollfd fds[MAXFD];
    fds_init(fds);
    fds_add(fds, sockfd);

    while(1)
    {
        int n = poll(fds, MAXFD, 5000);//可嫩会阻塞在这
        if(n==-1)
        {
            cout << "poll error" << endl;
        }

        else if(n==0)
        {
            cout << "time out" << endl;
        }

        else
        {
            for (int i = 0; i < MAXFD;i++)
            {
                if(fds[i].fd==-1)
                {
                    continue;
                }
                if(fds[i].revents&POLLIN)
                {
                    if(fds[i].fd==sockfd)//新客户
                    {
                        accept_client(sockfd, fds);
                    }
                    else//老客户
                    {
                        recv_data(fds[i].fd, fds);
                    }
                }
            }
        }
    }
}

客户端

与select实例中的相同

epoll

"epoll" 是 Linux 系统上用于高效 I/O 多路复用的一组系统调用和相关数据结构。它相比于传统的 select 和 poll 等方式,在处理大量连接时更加高效。

epoll 支持多种事件类型,其中常见的包括:

EPOLLIN表示文件描述符上有数据可读
EPOLLOUT表示文件描述符上可以写入数据
EPOLLET使用边缘触发模式(Edge Triggered),只通知一次事件
EPOLLRDHUP对端关闭连接或者半关闭
EPOLLERR发生错误
EPOLLHUP发生挂起事件

epoll_creat

int epoll_create(int size);//创建一个新的 epoll 实例的系统调用

//size:表示 epoll 实例可以管理的文件描述符的数量

工作方式

①调用 epoll_create 创建一个 epoll 实例。

②使用 epoll_ctl 添加要监视的文件描述符和事件类型,比如 EPOLLIN(读事件)或 EPOLLOUT(写事件)。

③调用 epoll_wait 等待事件的发生。这个函数会一直阻塞,直到有文件描述符上的事件发生或者达到设置的超时时间。

epoll_ctl

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

//epfd:epoll 实例的文件描述符,是之前由 epoll_create 创建的。 //op:表示要执行的操作,可以是以下值之一:

EPOLL_CTL_ADD添加一个文件描述符到 epoll 实例中,使得 epoll 可以监视它
EPOLL_CTL_MOD修改一个文件描述符在 epoll 实例中的事件监听
EPOLL_CTL_DEL从 epoll 实例中删除一个文件描述符,不再监听它

//fd:要操作的文件描述符,可以是套接字、文件等。

//event:一个 struct epoll_event 结构,描述了要监听的事件类型,可以包括读事件、写事件等。

工作方式

①创建一个 struct epoll_event 结构,设置需要监听的事件类型,例如 EPOLLIN 表示读事件、EPOLLOUT 表示写事件等。 ②调用 epoll_ctl 函数来添加、修改或删除文件描述符的事件监听。通过设置 op 参数来指定具体的操作。 ③如果添加或修改操作成功,epoll_ctl 函数返回 0,否则返回 -1,并可以使用 errno 获取错误信息

epoll_wait

是 epoll 系列系统调用之一,用于等待文件描述符上的事件发生,并将就绪的文件描述符返回给调用者。

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

//epfd:epoll 实例的文件描述符,由 epoll_create 创建。 //events:用于存储就绪事件的数组,每个元素都是一个 struct epoll_event 结构体。 //maxevents:events 数组的大小,表示最多可以存储多少个就绪事件。 //timeout:超时时间,单位是毫秒,指定等待多长时间。传递 -1 表示一直等待,传递 0 表示立即返回,传递正整数表示等待指定的毫秒数。

工作方式

①创建一个 epoll 实例,通过 epoll_create1 或者 epoll_create 函数,得到一个 epoll 文件描述符 epfd,该描述符代表了一个 epoll 实例。

②使用 epoll_ctl 函数将需要监听的文件描述符添加到 epoll 实例中,同时指定关心的事件类型(如读、写、异常等)以及相关的事件数据。

③调用 epoll_wait 函数等待事件的发生。epoll_wait 会一直阻塞,直到指定的文件描述符上发生就绪事件、超时时间到达或者被信号中断。

④一旦有事件发生,epoll_wait 返回就绪事件的数量,并将这些事件存储在传入的 events 数组中。

⑤应用程序可以遍历 events 数组,处理每个就绪事件。

⑥如果需要继续监听事件,可以再次调用 epoll_wait。

epoll_wait 的主要优势在于它可以高效处理大量的文件描述符,因为它不需要遍历所有文件描述符,只返回就绪的文件描述符,从而减少了系统调用的次数,提高了性能。

epoll实例

服务器端

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

#define MAXFD 10

using namespace std;

void epoll_add(int epfd, int fd)
{
    struct epoll_event ev;
    ev.data.fd = fd;
    ev.events = EPOLLIN; // r

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
    {
        cout << "epoll_ctl error" << endl;
    }
}

void epoll_del(int epfd, int fd)
{
    if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
    {
        cout << "epoll error1" << endl;
    }
}

void accept_client(int sockfd, int epfd)
{
    int c = accept(sockfd, NULL, NULL);
    if (c < 0)
    {
        return;
    }
    cout << "accept c=" << c << endl;

    epoll_add(epfd, c);
}

void recv_data(int c, int epfd)
{
    char buff[128] = {0};
    int n = recv(c, buff, 127, 0);
    if (n <= 0)
    {
        epoll_del(epfd, c);
        close(c);
        cout << "client close" << endl;
        return;
    }
    cout << "recv buff =" << buff << endl;
    send(c, "ok", 2, 0);
}

int socket_init()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
    if (sockfd == -1)
    {
        return -1;
    }

    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET; //地址族
    saddr.sin_port = htons(6000); //端口号
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //地址

    int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));

    if (res == -1)
    {
        cout << "bind error" << endl;
        return -1;
    }

    res = listen(sockfd, 5);

    if (res == -1)
    {
        return -1;
    }

    return sockfd;
}

int main()
{
    int sockfd = socket_init();
    if (sockfd == -1)
    {
        exit(1);
    }

    int epfd = epoll_create(MAXFD); //创建内核事件表
    if (epfd == -1)
    {
        exit(1);
    }

    epoll_add(epfd, sockfd);
    struct epoll_event evs[MAXFD]; //存放就绪描述符

    while (1)
    {
        int n = epoll_wait(epfd, evs, MAXFD, 5000); //阻塞,等就绪事件发生
        if (n < 0)
        {
            cout << "epoll error" << endl;
            continue;
        }
        else if (n == 0)
        {
            cout << "time out" << endl;
        }
        else
        {
            for (int i = 0; i < n; i++)
            {
                if (evs[i].events & EPOLLIN)
                {
                    if (evs[i].data.fd == sockfd) //新客户
                    {
                        accept_client(sockfd, epfd);
                    }
                    else //老客户
                    {
                        recv_data(evs[i].data.fd, epfd);
                    }
                }
            }
        }
    }
}

客户端

与select中的相同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值