socket网络编程(三)——select多路复用问题

目录

1、select诞生的原因

2、具体实现

2.1、服务端代码:

2.1、客户端代码:

3、select结构刨析

4、新的问题,千万级的并发


1、select诞生的原因

在上文《socket网络编程(二)——实现持续发送》我们提到了多客户端的时候,多台客户端发送数据到服务端的话,只能有一台客户端可以正常发送和接受数据,另外一台完全没有反应,那这个问题怎么解决呢?很多人可能第一反应想到利用多线程技术,线程多的话用线程池来维护。的确,多线程确实可以实现这个效果,但是,可能很多看见这个但是就不怎么开心了,却不知很多科学科技的进步都是这个但是引发的。但是一个多线程编程很麻烦又容易出错,二是如果连接有几千个的话,线程间切换的开销确实是很大。如果能够在一个线程里就实现这个效果的话,那该多好啊!

于是select就横空出世!

这个又叫做非阻塞IO多路复用,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高可能很多人会说,现在都是用epoll,都不用select了,还讲这个干嘛?可我想说,因为我这个是socket网络编程的一系列教程,一定要一步步的推进,历史上是有诞生了select,然后epoll是为了完善select的缺陷的,做为学习,我们必须先了解select,然后才能知道epoll的特别之处。

2、具体实现

首先,还是先不扯其他的,我先扔出代码,然后结合代码讲解select,我本人是比较喜欢这种学习方式,带着疑问去学习,如果大家不习惯的话,可以先跳过以下的代码,先看代码下方的讲解部分。

2.1、服务端代码:

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

#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);
        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;
}

2.1、客户端代码:

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

#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");
    }

    //服务器的ip为本地,端口号
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
    server_addr.sin_port = htons(39002);

    //向服务器发送连接请求
    int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (m_connectfd < 0)
    {
        ERR_EXIT("connect server fail");
    }

    //发送并接收数据
    char buffer[BUF_SIZE];
    while (1)
    {
        memset(buffer, 0, sizeof(buffer)); //重置缓冲区
        printf("client send:");
        scanf("%s", buffer);
        send(m_sockfd, buffer, sizeof(buffer) - 1, MSG_NOSIGNAL);
        recv(m_sockfd, buffer, sizeof(buffer) - 1, 0);
        printf("client recv:%s\n", buffer);
    }

    //断开连接
    close(m_sockfd);

    printf("client socket closed!!!\n");

    return 0;
}

3、select结构刨析

说到select的IO多路复用就不得不提fd_set这个变量类型,首先我们打开Linux的fd_set数据结构的源码我们可以看到,就是一个长度为32的long int类型的数组(要注意,windows的源码和Linux的不一样)。每一位可以代表一个文件描述符,所以fd_set最多表示1024个文件描述符!

这里为啥是1024个描述符呢?long int长度是32bit,数组长度是32,32*32=1024!

如果知道epoll用法的童鞋,可能就会知道,最多只能表示1024个文件描述符恰恰也成为了select的缺陷!

言归正传,fd_set中的每一bit可以对应一个文件描述符fd,则1字节长的fd_set最大可以对应8个fd。现在我们来看看fd_set定义的几个宏

#include <sys/select.h>   
int FD_ZERO(fd_set *fdset);    
int FD_SET(int fd, fd_set *fd_set);   
int FD_ISSET(int fd, fd_set *fdset);
int FD_CLR(int fd, fd_set *fdset);  

我们假设fdset就一个字节,就是8位,那么

(1)执行FD_ZERO(&fdset),则set用位表示是00000000,就是所有位都清空成0,一般刚开始的时候就需要清空。
(2)执行FD_SET(fd,&fdset),若fd=5,后set变为00010000,第5位置为1,就是将客户端连接的描述字(一般就是一个整数啦)放入到set当中。
(3)执行FD_ISSET(fd,&fdset),若fd=5,则就是判断set的第5位是否是1,一般用来判断是否客户端的连接。
(4)执行FD_CLR(fd,&fdset),若fd=5,则就是将第5位置成0,在断开客户端连接的时候,一定要记得调用这个。

以上的铺垫都做完之后,我们将要引出重量级的选手select,首先我们先来看下select的函数定义。

int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

参数说明:

maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的;

fd_set *readset: 该参数是我们所关心的文件是否可读的文件描述符的集合,如果这个集合中有个文件可读了,那select返回一个大于0的数,表示有文件可读了,比如说服务端接收到客户端的数据,服务端都是读的状态,所以正常读的文件都放在这里。

fd_set *writeset:那这个大家就比较好理解了,服务端发到客户端的数据,要写入到缓冲区,那么所有正常写的文件都放在这里。

fd_set *exceptset:在所有正常读和正常写的时候,产生了异常情况,那么异常文件就放在这里。

timeval *timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。这个参数使select处于三种状态:(1)timeout传入NULL,则select一直等到文件状态有变化时才返回,这段时间一直处于阻塞状态。(2):timeout传入0,则select会立即返回(非阻塞),如果文件状态有变化则返回一个大于0的值没有变化则返回0;(3)timeout传入一个大于0的数,则select在timeout时间内阻塞,一旦文件状态有变化就会返回,超时后不管怎样都会返回值同样是文件状态右边话就返回一个大于0的值,无变化则返回0;

timeval结构体定义如下:

struct timeval
{      
    long tv_sec;   /*秒 */
    long tv_usec;  /*微秒 */   
};

说完了用到的知识点,我来解释一下代码的部分实现。首先定义了一个array_fd的整形数据,数组的最长长度是MAX_FD_NUM,数组中存放的就是客户端连接的描述符,说白了就是客户端的连接,初始化的时候置成-1,只要客户端连接上了,就是描述符插入数组(实际上就是将其中的一个-1置成描述符)。最后遍历所有的客户端连接,找到发送数据的那个客户端描述符。

刚开始看这些代码的时候可能有点难,但是只要掌握了以上知识点,那么再看这些代码就很简单了。

4、新的问题,千万级的并发

我们刚才说到了select的IO多路复用最多只能支持1024个连接,超过了就不支持了,这个最大值用宏FD_SETSIZE定义的。可是我们实际的有些大的应用,连接数很容易超过这个数字,那如果还用这个就需要更改linux内核的select.h文件,然后重新编译内核了,这是一个问题。另一个是我们select的原理是遍历所有的连接,找到需要的那个,一旦连接数越多,就耗费资源,这是一对相互的矛盾体。最后假设我们的服务器需要支持100万的并发连接,则在FD_SETSIZE为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

那么在高并发下,我们又该怎么做呢?你能想的到解决方法吗?

更多精彩内容,请关注同名公众:一点月光(alittle-moon)

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Linux网络编程(总共41集) 讲解Linux网络编程知识,分以下四个篇章。 Linux网络编程之TCP/IP基础篇 Linux网络编程socket编程篇 Linux网络编程之进程间通信篇 Linux网络编程之线程篇 Linux网络编程之TCP/IP基础篇 01TCPIP基础(一) ISO/OSI参考模型 TCP/IP四层模型 基本概念(对等通信、封装、分用、端口) 02TCPIP基础(二) 最大传输单元(MTU)/路径MTU 以太网帧格式 ICMP ARP RARP 03TCPIP基础() IP数据报格式 网际校验和 路由 04TCPIP基础(四) TCP特点 TCP报文格式 连接建立次握手 连接终止四次握手 TCP如何保证可靠性 05TCPIP基础(五) 滑动窗口协议 UDP特点 UDP报文格式 Linux网络编程socket编程篇 06socket编程(一) 什么是socket IPv4套接口地址结构 网络字节序 字节序转换函数 地址转换函数 套接字类型 07socket编程(二) TCP客户/服务器模型 回射客户 /服务器 socket、bind、listen、accept、connect 08socket编程() SO_REUSEADDR 处理多客户连接(process-per-conection) 点对点聊天程序实现 09socket编程(四) 流协议与粘包 粘包产生的原因 粘包处理方案 readn writen 回射客户/服务器 10socket编程(五) read、write与recv、send readline实现 用readline实现回射客户/服务器 getsockname、getpeername gethostname、gethostbyname、gethostbyaddr 11socket编程(六) TCP回射客户/服务器 TCP是个流协议 僵进程与SIGCHLD信号 12socket编程(七) TCP 11种状态 连接建立次握手、连接终止四次握手 TIME_WAIT与SO_REUSEADDR SIGPIPE 13socket编程(八) 五种I/O模型 selectselect改进回射客户端程序 14socket编程(九) select 读、写、异常事件发生条件 用select改进回射服务器程序。 15socket编程(十) 用select改进第八章点对点聊天程序 16socket编程(十一) 套接字I/O超时设置方法 用select实现超时 read_timeout函数封装 write_timeout函数封装 accept_timeout函数封装 connect_timeout函数封装 17socket编程(十二) select限制 poll 18socket编程(十) epoll使用 epoll与select、poll区别 epoll LT/ET模式 19socket编程(十四) UDP特点 UDP客户/服务基本模型 UDP回射客户/服务器 UDP注意点 20socket编程(十五) udp聊天室实现 21socket编程(十六) UNIX域协议特点 UNIX域地址结构 UNIX域字节流回射客户/服务 UNIX域套接字编程注意点 22socket编程(十七) socketpair sendmsg/recvmsg UNIX域套接字传递描述符字 Linux网络编程之进程间通信篇 23进程间通信介绍(一) 进程同步与进程互斥 进程间通信目的 进程间通信发展 进程间通信分类 进程间共享信息的种方式 IPC对象的持续性 24进程间通信介绍(二) 死锁 信号量 PV原语 用PV原语解决司机与售票员问题 用PV原语解决民航售票问题 用PV原语解决汽车租赁问题 25System V消息队列(一) 消息队列 IPC对象数据结构 消息队列结构 消息队列在内核中的表示 消息队列函数 26System V消息队列(二) msgsnd函数 msgrcv函数 27System V消息队列() 消息队列实现回射客户/服务器 28共享内存介绍 共享内存 共享内存示意图 管道、消息队列与共享内存传递数据对比 mmap函数 munmap函数 msync函数 29System V共享内存 共享内存数据结构 共享内存函数 共享内存示例 30System V信号量(一) 信号量 信号量集结构 信号量集函数 信号量示例 31System V信号量(二) 用信号量实现进程互斥示例 32System V信号量() 用信号集解决哲学家就餐问题 33System V共享内存与信号量综合 用信号量解决生产者消费者问题 实现shmfifo 34POSIX消息队列 POSIX消息队列相关函数 POSIX消息队列示例 35POSIX共享内存 POSIX共享内存相关函数 POSIX共享内存示例 Linux网络编程之线程篇 36线程介绍 什么是线程 进程与线程 线程优缺点 线程模型 N:1用户线程模型 1:1核心线程模型 N:M混合线程模型 37POSIX线程(一) POSIX线程库相关函数 用线程实现回射客户/服务器 38POSIX线程(二) 线程属性 线程特定数据 39POSIX信号量与互斥锁 POSIX信号量相关函数 POSIX互斥锁相关函数 生产者消费者问题 自旋锁与读写锁介绍 40POSIX条件变量 条件变量 条件变量函数 条件变量使用规范 使用条件变量解决生产者消费者问题 41一个简单的线程池实现 线程池性能分析 线程池实现 网络编程, Linux
socket编程中的多路复用是指通过一种机制,使一个进程可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读写操作准备就绪),能够通知程序进行相应的读写操作。在Linux中,常用的多路复用机制有select、poll和epoll。其中,select是最古老的多路复用机制,poll是select的改进版,而epoll是最新、最高效的多路复用机制。多路复用机制可以大大提高程序的并发性能,使得程序可以同时处理多个客户端请求。 下面是一个简单的使用select实现多路复用的流程图和代码示例: 流程图: ``` 1. 创建socket并绑定端口 2. 将socket设置为非阻塞模式 3. 创建fd_set集合,并将socket加入集合 4. 进入循环,调用select函数,等待文件描述符就绪 5. 如果socket就绪,表示有新的客户端连接请求,调用accept函数接受连接 6. 如果其他文件描述符就绪,表示有客户端发送数据,调用recv函数接收数据并处理 7. 回到步骤4,继续等待文件描述符就绪 ``` 代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/select.h> #define MAX_CLIENTS 10 #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { int server_fd, client_fd, max_fd, activity, i, valread, sd; struct sockaddr_in address; char buffer[BUFFER_SIZE] = {0}; fd_set readfds; // 创建socket并绑定端口 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(atoi(argv[1])); if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } if (listen(server_fd, MAX_CLIENTS) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } // 将socket设置为非阻塞模式 int flags = fcntl(server_fd, F_GETFL, 0); fcntl(server_fd, F_SETFL, flags | O_NONBLOCK); // 创建fd_set集合,并将socket加入集合 FD_ZERO(&readfds); FD_SET(server_fd, &readfds); max_fd = server_fd; // 进入循环,调用select函数,等待文件描述符就绪 while (1) { activity = select(max_fd + 1, &readfds, NULL, NULL, NULL); if (activity < 0) { perror("select error"); exit(EXIT_FAILURE); } // 如果socket就绪,表示有新的客户端连接请求,调用accept函数接受连接 if (FD_ISSET(server_fd, &readfds)) { if ((client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept error"); exit(EXIT_FAILURE); } printf("New connection, socket fd is %d, ip is : %s, port : %d\n", client_fd, inet_ntoa(address.sin_addr), ntohs(address.sin_port)); // 将新的客户端socket加入集合 FD_SET(client_fd, &readfds); if (client_fd > max_fd) { max_fd = client_fd; } } // 如果其他文件描述符就绪,表示有客户端发送数据,调用recv函数接收数据并处理 for (i = server_fd + 1; i <= max_fd; i++) { sd = i; if (FD_ISSET(sd, &readfds)) { if ((valread = recv(sd, buffer, BUFFER_SIZE, 0)) == 0) { // 客户端关闭连接 printf("Client disconnected, socket fd is %d\n", sd); close(sd); FD_CLR(sd, &readfds); } else { // 处理客户端发送的数据 printf("Received message from client, socket fd is %d, message is %s\n", sd, buffer); memset(buffer, 0, BUFFER_SIZE); } } } } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员一点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值