0
点赞
收藏
分享

微信扫一扫

C++后端开发——五种网络io模型

言诗把酒 2022-01-16 阅读 63

# 网络io与select poll epoll

=====

## 简介

    一个对话框实验,主要用于理解网络io,使用了阻塞,多线程,select,poll,epoll等多种方式


## 五种网络IO模型与其实现

### 1.阻塞——单线程与多线程

  1. accept位于whlie循环之前:只能连接一个客户端
  2. accept位于whlie循环之中:能连接多个客户端,但只能接收一条消息
  3. thread 多线程​
    1. 每个请求对应一个线程,但是多线程需要进行CPU的上下文切换​​​​​​
    2. 优点:结构简单
    3. 缺点:无法支持大量客户端
// 4G / 8m = 512 假设运行内存只有4g,每个客户端连接占8m,只能支持512个连接
// C10K
void *client_routine(void *arg)
{
    int connfd = *(int *)arg;

    char buff[MAXLNE];

    while (1)
    {
        int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0)
        {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
            send(connfd, buff, n, 0);
        }
        else if (n == 0)
        {
            close(connfd);
            break;
        }
    }
    return NULL;
}

int main(int argc, char **argv)
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
    //使用socket套接字创建listenfd
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    //设置sockaddr结构体
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    //绑定地址
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    //开启listen
    if (listen(listenfd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

#if 0
    //首先是最基本的方案,在while循环之前accept
    //只能实现单个客户端的连接
    //定义客户端地址
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
    {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    printf("=================wait for client===================\n");
    while (1)
    {
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0)
        {
            //加上字符串的尾部,以便显示和转发
            buff[n] = '\0';
            printf("recv msg from clientL %s \n", buff);
            send(connfd, buff, n, 0);
        }
        else if (n == 0)
        {
            close(connfd);
        }
    }

#elif 0
    //将accept放入while循环,可以实现多个客户端连接,
    //但是每次只能进行对话一次
    printf("========waiting for client's request========\n");
    while (1)
    {

        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
        {
            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0)
        {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

            send(connfd, buff, n, 0);
        }
        else if (n == 0)
        {
            close(connfd);
        }

        // close(connfd);
    }
#elif 0
    //开线程,可以实现多个客户端连接,
    //但是数量有限,开销大
    while (1)
    {

        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
        {
            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }

        pthread_t threadid;
        pthread_create(&threadid, NULL, client_routine, (void *)&connfd);
    }

#elif 0
    //这份代码有问题
    //使用select实现多路复用
    //分别声明select中的读集合,写集合,读操作,写操作
    fd_set rfds, wfds, rset, wset;

    //全都置0
    FD_ZERO(&rfds);
    //将listen_fd加入读集合
    FD_SET(listenfd, &rfds);

    FD_ZERO(&wfds);

    int max_fd = listenfd;

    while (1)
    {
        //将新的读写操作更新读写集合
        rset = rfds;
        wset = wfds;
        //登记读写集合
        int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);

        //监听到新的套接字
        if (FD_ISSET(listenfd, &rset))
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
            {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }
            //加入到写集合中
            FD_SET(connfd, &rfds);
            //更新max_fd
            if (connfd > max_fd)
                max_fd = connfd;
            //返回的套接字里面啥也没有
            if (--nready == 0)
                continue;
        }

        //循环读操作里面的
        int i = 0;
        for (i = listenfd + 1; i <= max_fd; i++)
        {
            //在读操作里
            if (FD_ISSET(i, &rset))
            {
                n = recv(i, buff, MAXLNE, 0);
                if (n > 0)
                {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);
                    //设置为可写
                    FD_SET(i, &wfds);
                    // reactor
                    // send(i, buff, n, 0);
                }
                else if (n == 0)
                {
                    FD_CLR(i, &rfds);
                    close(i);
                }
                if (--nready == 0)
                    break;
            }

            //在写操作里
            else if (FD_ISSET(i, &wset))
            {
                send(i, buff, n, 0);
                //设置为可读
                FD_SET(i, &rfds);
            }
        }
    }
#elif 1

    fd_set rfds, rset;

    FD_ZERO(&rfds);
    FD_SET(listenfd, &rfds);

    int max_fd = listenfd;

    while (1)
    {
        //每次都会被清空,需要重新放入
        rset = rfds;

        int nready = select(max_fd + 1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(listenfd, &rset))
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
            {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }

            FD_SET(connfd, &rfds);

            if (connfd > max_fd)
                max_fd = connfd;

            if (--nready == 0)
                continue;
        }

        int i = 0;
        for (i = listenfd + 1; i <= max_fd; i++)
        {
            if (FD_ISSET(i, &rset))
            {
                n = recv(i, buff, MAXLNE, 0);
                if (n > 0)
                {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);

                    send(i, buff, n, 0);
                }
                else if (n == 0)
                {
                    FD_CLR(i, &rfds);
                    close(i);
                }
                if (--nready == 0)
                    break;
            }
        }
    }

#endif
    close(listenfd);
    return 0;
}

### 2.阻塞——select

  • 执行流程
    1. select 是一个阻塞函数,当没有数据时,会一直阻塞在select这一行
    2. 当有数据时会将reset中对应的那一位置1
    3. select函数返回不再阻塞
    4. 遍历文件描述符数组,判断哪个fd被置1了
    5. 读取数据,然后处理
  • 缺点
    1. 所使用的bitmap默认大小是1024,虽然可以调整但还是有限
    2. rset每次循环都必须重新置1,不可以重复使用
    3. 尽管将rset从用户态拷贝到内核态,由内核态判断是否有数据,但是还是有拷贝的开销
    4. 当有数据时select就会返回,但是select不知道哪个文件描述符发生变化,还需要进行遍历,效率比较低

    //这份代码有问题
    //使用select实现多路复用
    //分别声明select中的读集合,写集合,读操作,写操作
    fd_set rfds, wfds, rset, wset;

    //全都置0
    FD_ZERO(&rfds);
    //将listen_fd加入读集合
    FD_SET(listenfd, &rfds);

    FD_ZERO(&wfds);

    int max_fd = listenfd;

    while (1)
    {
        //将新的读写操作更新读写集合
        rset = rfds;
        wset = wfds;
        //登记读写集合
        int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);

        //监听到新的套接字
        if (FD_ISSET(listenfd, &rset))
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
            {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }
            //加入到写集合中
            FD_SET(connfd, &rfds);
            //更新max_fd
            if (connfd > max_fd)
                max_fd = connfd;
            //返回的套接字里面啥也没有
            if (--nready == 0)
                continue;
        }

        //循环读操作里面的
        int i = 0;
        for (i = listenfd + 1; i <= max_fd; i++)
        {
            //在读操作里
            if (FD_ISSET(i, &rset))
            {
                n = recv(i, buff, MAXLNE, 0);
                if (n > 0)
                {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);
                    //设置为可写
                    FD_SET(i, &wfds);
                    // reactor
                    // send(i, buff, n, 0);
                }
                else if (n == 0)
                {
                    FD_CLR(i, &rfds);
                    close(i);
                }
                if (--nready == 0)
                    break;
            }

            //在写操作里
            else if (FD_ISSET(i, &wset))
            {
                send(i, buff, n, 0);
                //设置为可读
                FD_SET(i, &rfds);
            }
        }
    }

### 3.阻塞——poll

  • pollfd结构体
    • fd:文件描述符
    • events:对应的时间,如果是读事件就是POLLIN,如果是写事件就是POLLOUT
    • revents:对events的反馈,开始时为0,当有数据可读时就为POLLIN,类似select的rset
/* Data structure describing a polling request.  */
struct pollfd
  {
    int fd;			/* File descriptor to poll.  */
    short int events;		/* Types of events poller cares about.  */
    short int revents;		/* Types of events that actually occurred.  */
  };
// poll 先把listenfd放进去,关注读事件
    struct pollfd fds[POLL_SIZE] = {0};
    fds[0].fd = listenfd;
    fds[0].events = POLLIN;

    int max_fd = listenfd;
    int i = 0;
    for (i = 1; i < POLL_SIZE; i++)
    {
        fds[i].fd = -1;
    }

    while (1)
    {
        int nready = poll(fds, max_fd + 1, -1);
        // listenfd发生读事件
        if (fds[0].revents & POLLIN)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
            {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }
            //接收此次收到的客户端文件描述符
            printf("accept \n");
            fds[connfd].fd = connfd;
            fds[connfd].events = POLLIN;

            if (connfd > max_fd)
                max_fd = connfd;

            if (--nready == 0)
                continue;
        }
        //遍历fds内部,读取revent确定是否收到数据
        for (i = listenfd + 1; i <= max_fd; i++)
        {
            if (fds[i].revents & POLLIN)
            {
                n = recv(i, buff, MAXLNE, 0);
                if (n > 0)
                {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);
                    //把刚刚收到的发出去
                    send(i, buff, n, 0);
                }
                //客户端断开连接,回收资源
                else if (n == 0)
                {
                    fds[i].fd = -1;
                    close(i);
                }
                if (--nready == 0)
                    break;
            }
        }
    }
  • 执行流程
    1. 将需要用到的fd从用户态拷贝到内核态
    2. poll为阻塞方法,执行poll方法,如果有数据会将fd对应的revents置为POLLIN
    3. poll方法返回
    4. 循环遍历,查找哪个fd被置为POLLIN
    5. 将revents重新置0,便于复用
    6. 将置位的fd进行读取和处理
  • 对于select提升的地方
    1. 解决了bitmap的1024大小限制
    2. 解决了rset不可重用的情况
  • 弊端
    1. 每次poll都仍要重新遍历全量的fds
    2. 服务程序也要遍历全量的fds,查看每个文件描述符的revents字段是否需要读写操作

### 4.非阻塞——epoll

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epoll_create() 返回一个文件描述符,该文件描述符“描述”的是内核中的一块内存区域,size现在不起任何作用。
  • epoll_ctl()用来操作内核事件表,
    • int epfd表示epoll_create() 返回的事件表
    • int fd:新创建的socket文件描述符
    • int op
      • EPOLL_CTL_ADD: 事件表中添加一个文件描述符,内核应该关注的socket的事件在epoll_event结构体中,添加到事件表中的文件描述符以红黑树的形式存在,防止重复添加
      • EPOLL_CTL_MOD:修改fd上注册的事件
      • EPOLL_CTL_DEL:删除fd上注册的事件
  • struct epoll_event *event
struct epoll_event{
    _uint32_t events; //epoll事件,读、写、异常三种
    epoll_data_t data; //用户数据
}

struct epoll_data{
    void* prt;
    int fd;
    _uint32_t u32;
    _uint64_t u64;
}epoll_data_t;
  • epoll_wait()该函数返回就绪文件描述符的个数

   // poll/select -->
    //  epoll_create
    //  epoll_ctl(ADD, DEL, MOD)
    //  epoll_wait

    //创建一个空间,假设是一个大房子
    int epfd = epoll_create(1);

    struct epoll_event events[POLL_SIZE] = {0};
    struct epoll_event ev;
    //先把listenfd先塞进去
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;
    // add
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    while (1)
    {
        //非阻塞,最后一个是超时时间
        int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
        if (nready == -1)
        {
            continue;
        }

        int i = 0;
        for (i = 0; i < nready; i++)
        {
            //读取获取的fd
            int clientfd = events[i].data.fd;
            //如果是listenfd
            if (clientfd == listenfd)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
                {
                    printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                    return 0;
                }
                printf("accept\n");
                //添加到ev里
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }
            // 获取到的消息
            else if (events[i].events & EPOLLIN)
            {
                n = recv(clientfd, buff, MAXLNE, 0);
                if (n > 0)
                {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);

                    send(clientfd, buff, n, 0);
                }
                else if (n == 0)
                {
                    ev.events = EPOLLIN;
                    ev.data.fd = clientfd;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
                    close(clientfd);
                }
            }
        }
    }
  • 执行流程

 图片转载自https://file.cfanz.cn/uploads/jpeg/2022/01/15/19/291353M663.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1h1ZXlpbkd1bw==,size_16,color_FFFFFF,t_70https://file.cfanz.cn/uploads/jpeg/2022/01/15/19/291353M663.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1h1ZXlpbkd1bw==,size_16,color_FFFFFF,t_70

举报

相关推荐

0 条评论