unix网络编程2.3——高并发服务器(三)多路IO复用之select

前置知识

阅读本文需要先阅读下面的文章:

unix网络编程1.1——TCP协议详解(一)

unix网络编程2.1——高并发服务器(一)基础——io与文件描述符、socket编程与单进程服务端客户端实现

unix网络编程2.2——高并发服务器(二)多进程与多线程实现

前言

unix网络编程2.2——高并发服务器(二)多进程与多线程实现中实现了多进程和多线程的服务器模型,但是实际开发中往往使用IO复用的方式,避免开多进程/线程的资源浪费,并且可以避免多进程/线程由于资源受限带来的并发量不足的情况,为此在本文中学习多路IO复用的入门接口select,一种基于轮询来借助内核帮助实现高并发服务器的思想。

  • 多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。

select函数参数与返回值

man select查看函数:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数:
nfds: 		监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds:	监控有读数据到达文件描述符集合,传入传出参数
writefds:	监控写数据到达文件描述符集合,传入传出参数
exceptfds:	监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
fd_set类型:	位图
timeout:	定时阻塞监控时间,3种情况
				1.NULL,永远等下去
				2.设置timeval,等待固定时间
				3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
    long tv_sec; /* seconds */
    long tv_usec; /* microseconds */
};

返回值:
	成功:所监听的所有 监听集合(readfds + writefds + exceptfds)中,满足条件的总数
	失败:-1

配套使用的接口:
	void FD_CLR(int fd, fd_set *set); 	// 把文件描述符集合里fd清0
	int FD_ISSET(int fd, fd_set *set); 	// 测试文件描述符集合里fd是否置1
	void FD_SET(int fd, fd_set *set); 	// 把文件描述符集合里fd位置1
	void FD_ZERO(fd_set *set); 		// 把文件描述符集合里所有位清0

select模型图与单进程服务端对比

单进程服务端模型图

  • 单进程服务端基于阻塞IO的设计思想如下:
    image

select模型图

  • IO多路复用会使用非阻塞IO
    image

与单进程服务端模型比较

  • accept和read对应IO不会阻塞,全部交由select统一管理,select阻塞等待IO读写事件就绪
  • 在select监听的事件集合中,有n个IO读写事件就绪,则返回n+1的整型数据,记为n_ready,后续的建立连接、读写数据依赖n_ready和FD_ISSET等接口进行判断后处理,这个过程需要循环遍历(轮询方式),效率较低,因此select不适用大规模客户端的场景,但比起单进程服务端的效率与资源占用率已经好很多

select参数分析 r_fds + w_fds + e_fds 三个集合

  • r_fds + w_fds + e_fds 三个集合对应三个事件,分别是:读事件、写事件、异常事件,发生这三个事件的前提是三次握手完成,server和client已经建立了连接,但在此之前呢?
    请看
    建立请求——accept不会再阻塞**

建立请求——accept不会再阻塞

  • 还有一个在server的事件需要关注:listen_fd的读事件,如果listen_fd发送了读事件,说明由client来发送SYN请求建立连接, 此时select会将listen_fd在r_fds中的bit位置1,标记listen_fd读事件就绪,此时再调用accept

读事件——read不会再阻塞

  • 与listen_fd读事件之于accept不会阻塞同理,conn_fd的读写事件被select阻塞监听后,服务端read也不会阻塞了

基于select实现server.c

  • 版本1
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>

#include <unistd.h>

#include <pthread.h>
#include <sys/select.h>
#include <string.h>

#define PORT 9999
#define START_FD 3
#define BUFFER_LENGTH 128

int main(void)
{
    int listen_fd, conn_fd;
    // block
	listen_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_fd == -1) return -1;
// listen_fd
	struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len;
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(PORT);

	if (-1 == bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) {
		return -2;
	}

#if 1 // nonblock
	int flag = fcntl(listen_fd, F_GETFL, 0);
	flag |= O_NONBLOCK;
	fcntl(listen_fd, F_SETFL, flag);
#endif

	listen(listen_fd, 128);

    // 由于每次有读写事件会调用FD_SET改变r_fds, w_fds,因此需要r_set, w_set作备份
    fd_set r_fds, w_fds;
    fd_set r_set, w_set; 

    FD_ZERO(&r_fds);
    FD_SET(listen_fd, &r_fds); // 监听listen_fd的读事件

    FD_ZERO(&w_fds);
    int max_fd = listen_fd;

    unsigned char buf[BUFFER_LENGTH] = {0};
    int n_ready, ret = 0;


    while (1) {
        r_set = r_fds;
        w_set = w_fds;
        n_ready = select(max_fd + 1, &r_set, &w_set, NULL, NULL); // 阻塞等待IO读写事件就绪
        
        // 走到这里说明有IO事件就绪
        if (FD_ISSET(listen_fd, &r_set)) {
            printf("listen_fd --> \n");
            client_len = sizeof(client_addr);
            conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); // 不会阻塞

            FD_SET(conn_fd, &r_fds); // 将conn_fd添加到r_fds中监听
            
            if (conn_fd > max_fd) {
                max_fd = conn_fd;
            }
        }

        for (int i = START_FD; i <= max_fd; i++) {
            if (FD_ISSET(i, &r_set)) {
                ret = recv(i, buf, sizeof(buf), 0);
                // 说明客户端断开了连接
                if (ret == 0) {
                    close(i);
                    FD_CLR(i, &r_fds);
                } else if (ret > 0) {
                    printf("buf : %s, ret : %d\n", buf, ret);
                    FD_SET(i, &w_fds); // 将fd添加到写集合中
                } else {
                    close(i);
                    FD_CLR(i, &r_fds);
                }
            } else if (FD_ISSET(i, &w_set)) {
                ret = send(i, buf, ret, 0);

                FD_CLR(i, &w_fds); // 将fd从写集合中删除
                FD_SET(i, &r_fds);
            }
        }
    }

    return 0;
}
  • 版本2
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <ctype.h>

#define SERVER_PORT 8888

/*  
统称:
文件描述符:   fd 
下标(索引): id
*/
int main(void)
{
    /* init */
    int listen_fd, max_fd, conn_fd, sock_fd;
    char buf[BUFSIZ], ip_str[INET_ADDRSTRLEN];                                 
    
    int i, j, max_i, n;
    fd_set r_set, all_set;                                                          /* 位图,r_set:监听读事件fd集合,all_set存r_set上一次的状态 */
    int n_ready, client[FD_SETSIZE];                                                /* 自定义数组client[],防止每次都遍历1024个fd,FD_SETSIZE:1024 */    

    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);

    listen_fd = socket(AF_INET, SOCK_STREAM, 0);                                    /* 创建socket,return listen_fd */
    bind(listen_fd, (struct sockaddr *)&server_addr,                                /* bind listen_fd */
         sizeof(server_addr));    
    listen(listen_fd, 128);                                                         /* start listen */

    /************************************************ begin select /************************************************/
    max_fd = listen_fd;                                                             /* 初始化时,listen_fd作为最大fd */         
    max_i = -1;                                                                     /* max_i作为client[]的id,初始值指向第0个元素之前的id */
    memset(client, -1, sizeof(client));                                             /* client[]所有id初始化为-1 */

    FD_ZERO(&all_set);
    FD_SET(listen_fd, &all_set);                                                    /* 构造select监控的fd信号集 */

    while (1) {
        r_set = all_set;
        n_ready = select(max_fd + 1, &r_set, NULL, NULL, NULL);                     /* n_ready: 所监听的所有 监听集合(readfds + writefds + exceptfds)中,满足条件的总数 */
        if (n_ready < 0) {
            perror("select error");
            exit(-1);
        }
        if (FD_ISSET(listen_fd, &r_set)) {                                          /* 说明由新的客户端连接请求(监听到了listen_fd的读事件) */
            client_addr_len = sizeof(client_addr);
            conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr,            /* accept不会再阻塞(listen已经ok) */
                             &client_addr_len);
            printf("accept done, client ip:%s, port:%d\n", 
                    inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str)),
                    ntohs(client_addr.sin_port));

            for (i = 0; i < FD_SETSIZE; i++) {                                  
                if (client[i] == -1) {                                              /* 找到client[]中没有使用的id */
                    client[i] = conn_fd;                                            /* 保存新连接的conn_fd到client[]中 */
                    break;
                }
            }

            if (i == FD_SETSIZE) {                                                  /* 达到select能监控的fd最大上限数 1024 */
                fputs("too many clients\n", stderr);
                exit(-1);
            }

            FD_SET(conn_fd, &all_set);                                              /* 更新conn_fd到select监控的fd信号集中 */

            if (conn_fd > max_fd) {
                max_fd = conn_fd;                                                   /* 更新max_fd */
            }
            if (i > max_i) {
                max_i = i;                                                          /* 保证max_i总是指向client[]最后一个元素下标 */
            }
            if (--n_ready == 0) {                                                   /* 其实这里的n_ready,是为了处理多个client可能同时发数据并且有新client连接的情况 */
                continue;                                                           /* 如果没有更多的就绪fd继续回到上面select阻塞监听 */
            }
        }

        for (i = 0; i <= max_i; i++) {                                              /* 轮询检测有哪个clients 有数据就绪(max_i指向的是client[]中最后一个有数据的fd) */
            if ((sock_fd = client[i]) < 0) {
                continue;
            }
            if (FD_ISSET(sock_fd, &r_set)) {
                if ((n = read(sock_fd, buf, sizeof(buf))) == 0) {                   /* 当client关闭连接时,server也应关闭对应连接 */
                    close(sock_fd);
                    FD_CLR(sock_fd, &all_set);                                      /* 清除select对此fd的监听 */
                    client[i] = -1;
                } else if (n > 0) {
                    for (j = 0; j < n; j++) {
                        buf[j] = toupper(buf[j]);
                    }
                    write(sock_fd, buf, n);
                }
                if (--n_ready == 0) {
                    break;                                                          /* 跳出for,但还在while中 */
                }
            }
        }
    }
    return 0;
}

总结

参考1

  • 1、select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
  • 2、解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力

参考2

posted @ 2022-11-22 22:05  胖白白  阅读(194)  评论(0编辑  收藏  举报