0
点赞
收藏
分享

微信扫一扫

6个免费的ChatGPT网站

金牛豆豆 2024-03-23 阅读 26
linuxtcp/ip

I/O复用

用途:I/O 复用能同时监听多个文件描述符。

在Linux下,有三种系统调用函数,分别是select,poll,epoll。

1.select

用途:在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件。

1.接口介绍:

1.int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
2. fd_set 是用来存放描述符的对应事件的集合。

在这里插入图片描述

2.例子

1.用select监听键盘是否有数据输入

#include <stdio.h>
#include <sys/select.h>
#include <stdlib.h>
#include <sys/time.h>
#include <string.h>
#include <unistd.h>
int main()
{
    char buff[256] = {0};
    fd_set fdset;
 
    while (1)
    {
        FD_ZERO(&fdset);
        FD_SET(0, &fdset);
        struct timeval tim = {5,0};  //每次都需要传入一个新的timeval结构体
        int n = select(1,&fdset,NULL,NULL,&tim);//select中会改变tim的值
        if(-1 == n)
        {
            printf("input error\n");
            continue;
        }
        else if(0 == n)
        {
            printf("timeout\n");
            continue;
        }
        else 
        {
            if(FD_ISSET(0,&fdset))
            {
                memset(buff,0,256);
                read(0,buff,255);
                printf("read:%s\n",buff);
            }
        }
    }
}

运行结果:

在这里插入图片描述

2.用select处理多个并发客户端

原理图:

在这里插入图片描述

服务端参考代码:

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

#define MaxLen 60
void fds_init(int fds[]) //初始化
{
    for (int i = 0; i < MaxLen; ++i)
    {
        fds[i] = -1;  // 无效的描述符
    }
}
void fds_add(int fd, int fds[]) //添加描述符
{
    for (int i = 0; i < MaxLen; ++i)
    {
        if (-1 == fds[i])
        {
            fds[i] = fd;
            break;
        }
    }
}
void fds_del(const int fd, int fds[]) //删除描述符
{
    for (int i = 0; i < MaxLen; ++i)
    {
        if (fd == fds[i])
        {
            fds[i] = -1;
            break;
        }
    }
}
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        exit(1);
    }
    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);                   // htons 将主机字节序转换为网络字节
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址
    int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (-1 == res)
    {
        exit(1);
    }
    res = listen(sockfd, 5);
    if (-1 == res)
    {
        exit(1);
    }

    int fds[MaxLen];  // 存放套结字
    fds_init(fds);    //初始化
    fds_add(sockfd, fds); //加入本地服务器的套结字

    fd_set fdset;
    while (1)
    {
        FD_ZERO(&fdset);
        int maxfd = -1;
        for (int i = 0; i < MaxLen; ++i) // 将所有已经建立连的接描述符写入fdset
        {
            if (-1 != fds[i])
            {
                FD_SET(fds[i], &fdset);
                maxfd = maxfd > fds[i] ? maxfd : fds[i];  //记录文件描述符最大值
            }
        }

        struct timeval tim = {5, 0};
        int n = select(maxfd + 1, &fdset, NULL, NULL, &tim);
        if (-1 == n)
        {
            printf("error\n");
            continue;
        }
        else if (0 == n)
        {
            printf("timeout\n");
            continue;
        }
        else
        {
            for (int i = 0; i < MaxLen; ++i)
            {
                if (-1 == fds[i])
                {
                    continue;
                }
                if (FD_ISSET(fds[i], &fdset))
                {
                    if (fds[i] == sockfd)
                    {
                        struct sockaddr_in caddr;
                        int len = sizeof(caddr);
                        int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
                        if (-1 == c)
                        {
                            continue;
                        }
                        printf("c:%d\n", c);
                        fds_add(c, fds); //建立连接,保存
                    }
                    else
                    {
                        char buff[256] = {0};
                        int n = recv(fds[i], buff, 255, 0);
                        if (0 >= n)
                        {
                            fds_del(fds[i], fds); //断开连接,清除
                            close(fds[i]);
                            printf("one client close");
                        }
                        else
                        {
                            printf("client[%d]:%s\n", fds[i], buff);
                            send(fds[i], "ok", 2, 0);
                        }
                    }
                }
            }
        }
    }
}

客户端参考代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd != -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 (-1 == res)
    { 
        exit(1);
    }
    while (1)
    {
        char buff[128] = {0};
        printf("input:\n");
        fgets(buff, 128, stdin); // 会取得\n
        if (strncmp(buff, "end", 3) == 0)
        {
            break;
        }
        send(sockfd, buff, strlen(buff), 0);
        memset(buff, 0, 128);
        recv(sockfd, buff, 127, 0);
        printf("buff=%s\n", buff);
    }
    close(sockfd);
    exit(0);
}

运行结果:

在这里插入图片描述

2.poll

用途:在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪的 。

1.接口介绍:
1.int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2.pollfd 结构体
3.poll支持的事件:

在这里插入图片描述

2.例子

poll和select的原理相似;此处不再赘述。

本地服务器端参考代码:

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

#define MaxLen 60

void fds_init(struct pollfd fds[])
{
    for (int i = 0; i < MaxLen; ++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 < MaxLen; ++i)
    {
        if (-1 == fds[i].fd)
        {
            fds[i].fd = fd;
            fds[i].events = POLLIN; // 读事件
            fds[i].revents = 0;
            break;
        }
    }
}
void fds_del(struct pollfd fds[], int fd)
{
    for (int i = 0; i < MaxLen; ++i)
    {
        if (fd == fds[i].fd)
        {
            fds[i].fd = -1;
            fds[i].events = 0;
            fds[i].revents = 0;
            break;
        }
    }
}
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        exit(1);
    }
    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);                   // htons 将主机字节序转换为网络字节
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址
    int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (-1 == res)
    {
        exit(1);
    }
    res = listen(sockfd, 5);
    if (-1 == res)
    {
        exit(1);
    }

    struct pollfd fds[MaxLen]; //存放套接字描述符
    fds_init(fds);
    fds_add(fds, sockfd);

    while (true)
    {
        int n = poll(fds, MaxLen, 6000); // 阻塞
        if (-1 == n)
        {
            continue;
        }
        else if (0 == n)
        {
            printf("time out\n");
            continue;
        }
        else
        {
            for (int i = 0; i < MaxLen; ++i)
            {
                if (-1 == fds[i].fd)
                {
                    continue;
                }
                if (fds->revents & POLLIN)
                {
                    if (sockfd == fds[i].fd)
                    {
                        struct sockaddr_in caddr;
                        int len = sizeof(caddr);
                        int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
                        if (-1 == c)
                        {
                            continue;
                        }
                        printf("c:%d\n", c);
                        fds_add(c,fds);
                    }
                    else
                    {
                        char buff[256] = {0};
                        int n = recv(fds[i].fd, buff, 255, 0);
                        if (0 >= n)
                        {
                            fds_del(fds[i].fd, fds); //断开连接,清除
                            close(fds[i].fd);
                            printf("one client close"); 
                        }
                        else
                        {
                            printf("client[%d]:%s\n", fds[i], buff);
                            send(fds[i].fd, "ok", 2, 0);
                        }
                    }
                }
            }
        }
    }
}

epoll

  1. epoll 是 Linux 特有的 I/O 复用函数。
  2. epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

1.接口介绍:

1.epoll_create() 用于创建内核事件表
2.epoll_ctl()用于操作内核事件表
3.epoll_wait()用于在一段超时时间内等待一组文件描述符上的事件

2.例子

本地服务器端参考代码:

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

#define MaxLen 20
void epoll_add(int epfd, int fd, int op)
{
    struct epoll_event ep;
    ep.events = op;
    ep.data.fd = fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ep) == -1)
    {
        printf("Add fd error\n");
    }
}
void epoll_del(int epfd, int fd)
{
    if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
    {
        printf("Del fd error\n");
    }
}
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        exit(1);
    }
    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);                   // htons 将主机字节序转换为网络字节
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址
    int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (-1 == res)
    {
        exit(1);
    }
    res = listen(sockfd, 5);
    if (-1 == res)
    {
        exit(1);
    }
    int epfd = epoll_create(MaxLen); // 内核事件表的文件描述符
    if (-1 == epfd)
    {
        exit(1);
    }
    struct epoll_event evs[MaxLen]; // 存放就绪的文件描述符
    epoll_add(epfd,sockfd,EPOLL_CTL_ADD);
    while (true)
    {
        int n = epoll_wait(epfd, evs, MaxLen, 5000);
        if (-1 == n)
        {
            continue;
        }
        else if (0 == n)
        {
            printf("time out\n");
            continue;
        }
        else
        {
            for (int i = 0; i < n; ++i) // 区别于select和poll
            {
                int fd = evs[i].data.fd;
                if (evs[i].events & EPOLLIN) //读事件发生
                {
                    if (sockfd == fd)
                    {
                        struct sockaddr_in caddr;
                        int len = sizeof(caddr);
                        int c = accept(fd, (struct sockaddr *)&caddr, &len);
                        if (0 >= c)
                        {
                            continue;
                        }
                        printf("accept client:%d",c);
                        epoll_add(epfd, c, EPOLL_CTL_ADD);
                    }
                    else
                    {
                        char buff[256] = {0};
                        int c = recv(fd, buff, 255, 0);
                        if (0 >= c)
                        {
                            printf("one client close\n");
                            epoll_del(epfd, fd);
                            close(fd);
                            continue;
                        }
                        printf("client(%d):%s\n", fd, buff);
                        send(fd, "OK", 2, 0);
                    }
                }
            }
        }
    }
}

3.LT 和 ET 模式

1.LT模式

以读事件为例,当缓冲区有数据准备好的时候,此时会触发读事件,**如果我们一直不去读取缓冲区里的数据,epoll模型就会一直通知我们有事件就绪。**LT模式也是epoll模型的默认模式。

2.ET模式

对于读事件 EPOLLIN,当该描述符对应的接受缓冲区的数据准备好的时候,也会触发读事件,但是只会触发一次,如果我们这次没有调用read/recv 读取 或者 没有一次读完,后面就不会通知有读事件就绪了。简单来说,只有当该描述符对应的接受缓冲区里的数据量发生变化的时候,才会通知我们一次,不会像LT模式那样一直通知。

示例图:

在这里插入图片描述
在这里插入图片描述

3.ET模式下的非阻塞编程:
阻塞状态下epoll存在问题:

只有接收缓冲区的数据变化时才会通知,通知的次数少了自然也会引发一些问题,比如触发读事件后必须把数据收取干净,因为你不一定有下一次机会再收取数据了,即使不采用一次读取干净的方式,也要把这个激活状态记下来,后续接着处理,否则如果数据残留到下一次消息来到时就会造成延迟现象。

解决方法:
1.为避免接收缓冲区中的数据一次取不完,我们采用循环来将缓冲区读取干净,当recv发现缓冲区里没有数据了,此时会默认进入阻塞状态,等待数据就绪,这就严重影响到后面的文件描述符读取/写入内容了。所以ET模式下必须要设为非阻塞。
2.当非阻塞状态下时,当缓冲区没有数据时,会返回error,并且将全局变量( errno ) 置为 EAGAIN 或 EWOULDBLOCK;
置为非阻塞的两种方法:
1.fnctl函数设定
    int flag = fcntl(client_fd, F_GETFL);
    flag |= O_NONBLOCK;
    int ret = fcntl(client_fd, F_SETFL, flag);
    if (ret == -1)
    {
        printf("Set Wait error\n");
    }
2.recv的参数设定
int c = recv(fd,buff,1,MSG_DONTWAIT);
if(0 >= c)
{
  if(errno != EAGAIN && errno != EWOULDBLOCK ) 
  {
    epoll_del(epfd,fd);
    close(fd);
    printf("one client close\n");
  }
}
4.例子

本地服务器端参考代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <fcntl.h> //新增
#include <errno.h> //新增

#define MaxLen 20
void set_NoWait(int fd)
{
    int oldfl = fcntl(fd,F_GETFL);  //获取描述符的状态
    int newfl = oldfl | O_NONBLOCK;
    int n = fcntl(fd,F_SETFL,newfl); //新增非阻塞状态
    if(-1 == n)
    {
        printf("Set NoWait error\n");
    }
}
void epoll_add(int epfd, int fd, int op)
{
    struct epoll_event ep;
    ep.events = op | EPOLLET; //开启ET模式
    ep.data.fd = fd;
    set_NoWait(fd); //置为非阻塞
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ep) == -1)
    {
        printf("Add fd error\n");
    }
}
void epoll_del(int epfd, int fd)
{
    if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
    {
        printf("Del fd error\n");
    }
}
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        exit(1);
    }
    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);                   // htons 将主机字节序转换为网络字节
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址
    int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (-1 == res)
    {
        exit(1);
    }
    res = listen(sockfd, 5);
    if (-1 == res)
    {
        exit(1);
    }
    int epfd = epoll_create(MaxLen); // 内核事件表的文件描述符
    if (-1 == epfd)
    {
        exit(1);
    }
    struct epoll_event evs[MaxLen]; // 存放就绪的文件描述符
    epoll_add(epfd,sockfd,EPOLL_CTL_ADD);
    while (true)
    {
        int n = epoll_wait(epfd, evs, MaxLen, 5000);
        if (-1 == n)
        {
            continue;
        }
        else if (0 == n)
        {
            printf("time out\n");
            continue;
        }
        else
        {
            for (int i = 0; i < n; ++i) // 区别于select和poll
            {
                int fd = evs[i].data.fd;
                if (evs[i].events & EPOLLIN) //读事件发生
                {
                    if (sockfd == fd)
                    {
                        struct sockaddr_in caddr;
                        int len = sizeof(caddr);
                        int c = accept(fd, (struct sockaddr *)&caddr, &len);
                        if (0 >= c)
                        {
                            continue;
                        }
                        printf("accept client:%d",c);
                        epoll_add(epfd, c, EPOLL_CTL_ADD);
                    }
                    else
                    {
                        char buff[256] = {0};
                        while(true) //采用循环清空接受缓冲区
                        {
                            int c = recv(fd,buff,1,0);
                            if(0 >= c)
                            {
                                if(errno != EAGAIN && errno != EWOULDBLOCK ) 
                                {
                                    epoll_del(epfd,fd);
                                    close(fd);
                                    printf("one client close\n");
                                }
                                break;
                            }
                            printf("client(%d):%s\n",fd,buff);
                            send(fd,"OK",2,0);
                        }
                    }
                }
            }
        }
    }
}

客户端参考代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd != -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 (-1 == res)
    { 
        exit(1);
    }
    while (1)
    {
        char buff[128] = {0};
        printf("input:\n");
        fgets(buff, 128, stdin); // 会取得\n
        buff[strlen(buff)-1] = 0; //删除\n
        if (strncmp(buff, "end", 3) == 0)
        {
            break;
        }
        send(sockfd, buff, strlen(buff), 0);
        memset(buff, 0, 128);
        recv(sockfd, buff, 127, 0);
        printf("buff=%s\n", buff);
    }
    close(sockfd);
    exit(0);
}

运行结果:

在这里插入图片描述

select/poll/epoll的区别

select

poll

epoll

总结:

I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

举报

相关推荐

0 条评论