• 能进行一对多连接的服务器(1994年Linux 1.0支持select)


    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
     
    #define PORT 39002
    #define MAX_FD_NUM 3
    #define BUF_SIZE 512
    #define ERR_EXIT(m)         \
        do                      \
        {                       \
            perror(m);          \
            exit(EXIT_FAILURE); \
        } while (0)
     
    int main()
    {
        //创建套接字
        int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (m_sockfd < 0)
        {
            ERR_EXIT("create socket fail");
        }
     
        //初始化socket元素
        struct sockaddr_in server_addr;
        int server_len = sizeof(server_addr);
        memset(&server_addr, 0, server_len);
     
        server_addr.sin_family = AF_INET;
        //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以
        server_addr.sin_addr.s_addr = INADDR_ANY;
        server_addr.sin_port = htons(PORT);
     
        //绑定文件描述符和服务器的ip和端口号
        int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
        if (m_bindfd < 0)
        {
            ERR_EXIT("bind ip and port fail");
        }
     
        //进入监听状态,等待用户发起请求
        int m_listenfd = listen(m_sockfd, MAX_FD_NUM);
        if (m_listenfd < 0)
        {
            ERR_EXIT("listen client fail");
        }
     
        //定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信
        //struct sockaddr_in client_addr;
        //socklen_t client_len = sizeof(client_addr);
        //int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
     
        printf("client accept success\n");
     
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
     
        //接收客户端数据,并相应
        char buffer[BUF_SIZE];
        int array_fd[MAX_FD_NUM];
        //客户端连接数量
        int client_count = 0;
    
        fd_set tmpfd;
        int max_fd = m_sockfd;
        struct timeval timeout;
     
        for (int i = 0; i < MAX_FD_NUM; i++)
        {
            array_fd[i] = -1;
        }
        //array_fd[0] = m_sockfd;
     
        while (1)
        {
            FD_ZERO(&tmpfd);
            FD_SET(m_sockfd, &tmpfd);   // m_sockfd是服务器的socket,也加入到fd_set中,后面交给select进行监听。
            int i;
     
            //所有在线的客户端加入到fd中,并找出最大的socket
            for (i = 0; i < MAX_FD_NUM; i++)
            {
                if (array_fd[i] > 0)
                {
                    FD_SET(array_fd[i], &tmpfd); //set array_fd in red_set
                    if (max_fd < array_fd[i])
                    {
                        max_fd = array_fd[i]; //get max_fd
                    }
                }
            }
     
            int ret = select(max_fd + 1, &tmpfd, NULL, NULL, NULL);
            if (ret < 0)
            {
                ERR_EXIT("select fail");
            }
            else if (ret == 0)
            {
                //ERR_EXIT("select timeout"); //超时不是错误,不可断掉连接
                printf("select timeout\n");
                continue;
            }
     
            //表示有客户端连接
            if (FD_ISSET(m_sockfd, &tmpfd))
            {
                int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
                if (m_connfd < 0)
                {
                    ERR_EXIT("server accept fail");
                }
     
                //客户端连接数已满
                if (client_count >= MAX_FD_NUM)
                {
                    printf("max connections arrive!!!\n");
                    // char buff[]="max connections arrive!!!";
                    // send(m_connfd, buff, sizeof(buff) - 1, 0);
                    close(m_connfd);
                    continue;
                }
     
                //客户端数量加1
                client_count++;
                printf("we got a new connection, client_socket=%d, client_count=%d, ip=%s, port=%d\n", m_connfd, client_count, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
     
                for (i = 0; i < MAX_FD_NUM; i++)
                {
                    if (array_fd[i] == -1)
                    {
                        array_fd[i] = m_connfd;
                        break;
                    }
                }
            }
     
            //遍历所有的客户端连接,找到发送数据的那个客户端描述符
            for (i = 0; i < MAX_FD_NUM; i++)
            {
                if (array_fd[i] < 0)
                {
                    continue;
                }
                //有客户端发送过来的数据
                else
                {
                    if (FD_ISSET(array_fd[i], &tmpfd))
                    {
                        memset(buffer, 0, sizeof(buffer)); //重置缓冲区
                        int recv_len = recv(array_fd[i], buffer, sizeof(buffer) - 1, 0);
                        if (recv_len < 0)
                        {
                            ERR_EXIT("recv data fail");
                        }
                        //客户端断开连接
                        else if (recv_len == 0)
                        {
                            client_count--;
                            //打印断开的客户端数据
                            printf("client_socket=[%d] close, client_count=[%d], ip=%s, port=%d\n\n", array_fd[i], client_count, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                            close(array_fd[i]);
                            FD_CLR(array_fd[i], &tmpfd);
                            array_fd[i] = -1;
                        }
                        else
                        {
                            printf("server recv:%s\n", buffer);
                            strcat(buffer, "+ACK");
                            send(array_fd[i], buffer, sizeof(buffer) - 1, 0);
                        }
                    }
                }
            }
        }
     
        //关闭套接字
        close(m_sockfd);
     
        printf("server socket closed!!!\n");
     
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187

    上面也是一个简单的echo服务器,它通过select实现I/O复用。下面我简单介绍这个过程的思想(如果你在读这篇文章时觉得写的不清楚,欢迎留言或私信交流)。

    首先,要进行I/O多路复用,则意味着一个服务器可以同时连接多个客户端,那么就需要用数组来记录这些客户端,上面的代码中通过int array_fd[MAX_FD_NUM];来记录。每当服务器用accept接受一个用户连接,就将accept返回这个客户端连接的文件描述符m_connfd并放入到array_fd中。

    但是accept、recv、send函数都是阻塞的,如果没有用户发送连接请求或者发送数据,那整个服务器照样会被阻塞,无法实现一对多的连接。这个时候select就发挥作用了。

    select能够检测到句柄上是否有事件发生(比如客户连接请求、客户的数据到达服务器)。这样,程序就只需要加上一个判断语句,根据select的结果来决定是否调用accept或者recv。

    那么select是怎么做的呢?select有一个配套的数据结构fd_set,它里面定义了一个数组可以记录文件描述符。服务器中通过FD_SET(m_sockfd, &tmpfd)tmpfd中的第m_sockfd(它对应服务器对外开放的套接字)标记为感兴趣的文件描述符(将对应的位置1)。如果没有新的连接请求,select会将m_sockfdtmpfd中的记号置为0。select的思想是这样的:遍历tmpfd中被标记为感兴趣的文件描述符,遍历完后,把有事件产生的文件描述符在tmpfd中的值保持为1,否则置为0。

    所以tempfd在被select函数处理前后表示的意义是不一样的,在传入前,表示的是需要检查的感兴趣的文件描述符集合,在处理后,表示的是感兴趣的文件描述符集合中有事件发生的文件描述符。

    正是由于这个过程中会改变tempfd表示的感兴趣的文件描述符集合,所以在程序中创建了数组array_fd来表示感兴趣的文件描述符集合。然后每次select后,通过array_fd重新设定tempfd上感兴趣的文件描述符集合。

    由于服务器需要频繁的监听是否有新的用户连接请求,所以每次循环都会执行FD_SET(m_sockfd, &tmpfd);,表示对m_sockfd这个文件描述符感兴趣。

    另外,服务器还需要知道是否收到客户端发送的数据,所以在建立服务器-客户端连接后,还需要通过FD_SET(m_connfd, &tmpfd);将用户连接加入到感兴趣的文件描述符集合中。

    每当通过select后,tempfd变为感兴趣的文件描述符集合中有事件发生的文件描述符集合。再通过一个循环函数遍历tempfd,根据FD_ISSET(array_fd[i], &tmpfd)找到有事件发生的文件描述符。并对这个文件描述符执行recv函数,这个时候,recv函数可以直接获得收到的数据,因而不会被阻塞。

    需要注意的是,服务器对外开放的文件描述符sock_fd并不在array_fd中,每次单独的通过FD_SET(m_sockfd, &tmpfd);把在tempfd上的标记置为1。再单独的通过FD_ISSET(m_sockfd, &tmpfd)检查这个套接字上是否有新的连接请求事件。如果有,则由accept产生客户端对应的文件描述符,并加入到感兴趣的文件描述符集合array_fd中。此时由于客户端连接请求已经存在,所以accept函数不会被阻塞。

    这个服务器程序可以同时处理多个用户连接而不会被阻塞,我觉得它最重要的思想是:不直接去执行会导致阻塞的函数(accept、recv、send),而是在执行前先进行判断,如果有对应的事件发生才去执行阻塞函数,否则继续循环监听。这种方式称为事件驱动

    为什么说这种方式实现了I/O多路复用呢?在网上找了一段解释:

    IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
    一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
    多路是指网络连接,复用指的是同一个线程

    这样实现方式存在一些缺点:

    • 首先是fd_set中定义的数组大小是由FD_SETSIZE决定的,最大为1024,所以select的实现方式由连接数量的限制。

    • select会将感兴趣的文件描述符标记集tempfd中有事件发生的文件描述符标记为1,但是并不能直接告诉程序到底是哪些文件描述符上有事件发生,实现时还得通过FD_ISSET函数一个个判断tempfd中的标记,时间复杂度为O(N)

    • select每次处理完tempfd后会改变tempfd中设定的感兴趣的文件描述符标记,程序还得创建一个array_fd来保存这些感兴趣的文件描述符集合,在select之后再通过for循环依次根据array_fd重新设置tempfd,操作繁琐,接口很不友好(不仔细琢磨不容易理解这个设计,差点被select函数挡在了网络编程的门外)。

  • 相关阅读:
    PostgreSQL常用指令
    基于java+SpringBoot+HTML+Mysql宠物医院网站
    百战RHCE(第五十一战:运维工程师必会技-Ansible学习6-编写和执行Playbook)
    基于Python实现的图的同构算法
    2023.10.03
    SQL数据分析之窗口排序函数rank、dense_rank、raw_number与lag、lead窗口偏移函数【用法整理】
    【前端设计模式】之解释器模式
    掌握 SwiftUI 中的 ScrollView
    JSP out对象:向客户端输出数据
    OPC DCOM快速配置
  • 原文地址:https://blog.csdn.net/GodNotAMen/article/details/128081089