C语言socket编程-select


前言

在早期的Unix系统中,每个进程都有一个单独的I/O流,这种方式效率低下,因为每个I/O操作都需要等待数据的到来。为了提高效率,Unix引入了select函数,它可以同时监听多个文件描述符,随着网络通信的发展,select函数也被广泛应用于网络编程中,它可以同时监听多个网络连接,当其中任意一个连接有数据到来时,就可以进行相应的处理,从而实现高效的网络通信。


一、简介

在socket编程中,select是一种多路复用 I/O 模型,它可以同时监视多个文件描述符的读写状态,当某个文件描述符就绪时,select会返回该文件描述符,从而实现异步 I/O。

二、select的工作原理

1.工作原理:

  • 服务端初始化socket,绑定端口,监听端口
  • 将sockfd和clientpool中有效的句柄添加到fdset中,调用select等待句柄就绪
  • 客户端初始化socket,发送建链请求
  • 服务端select中感知到sockfd已就绪,函数返回,调用accept与客户端完成建链,并生成connfd,再将connfd添加到clientpool中
  • 客户端向服务端发送数据
  • 服务端遍历clientpool,接收数据,并向客户端返回响应
  • 客户端传输数据完成后断开连接
  • 服务端收到断链事件后,将句柄从clientpool中移除
    在这里插入图片描述

2.fd_set数据模型

  • 假设fd_set长度为1字节(实际使用时根据操作系统而定),fd_set中每一位对应一个句柄fd,1字节长度的fd_set可以表示8个句柄
  • 初始化fd_set set; FD_ZERO(&set),set可以表示为0000 0000
  • 若有fd = 1;执行FD_SET(fd, &set)后,set可以表示为0000 0010
  • 若再加入fd = 3, fd = 5; 执行FD_SET后,set可以表示为0010 1010
  • 执行select(sockFd, &set, NULL, NULL, NULL)后代码会阻塞在这里等待句柄就绪
  • 若此时句柄1和句柄5都发生可读事件,则select函数返回,set此时为0010 0010。没有事件发生的句柄3被清空

三、常用函数及说明

void FD_ZERO(fd_set *set);  		// 初始化集合,使所有位都置为0
void FD_SET(int fd, fd_set *set);   // 将句柄fd添加到集合中
void FD_CLR(int fd, fd_set *set);   // 从集合中删除句柄fd
int FD_ISSET(int fd, fd_set *set);  // 判断句柄fd是否在集合中

四、代码示例

#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <errno.h>

#define ERR 1
#define OK 0
#define ADDR "127.0.0.1"        // 本地IP
#define PORT 23                 // 本地端口
#define BACKLOG 1024            // 监听队列长度
#define MAX_SIZE 1024           // 接收数据缓冲区长度
#define CLIENT_NUM 10           // 可以同时连接的客户端数量

int g_clientFds[CLIENT_NUM] = {0};

/**
* @brief 输出fd_set当前存储状态
*/
void PrintFdSet(fd_set *fdset)
{
    for (int i = 0; i < FD_SETSIZE; i++) {
        if (FD_ISSET(i, fdset)) {
            printf("1");
        } else {
            printf("0");
        }
    }

    printf("\n");
}

int main()
{
    int sockFd, newSockFd, maxFd, iRet, flag;
    unsigned int iLocakAddr;
    struct sockaddr_in localAddr, remoteAddr;
    socklen_t addrlen = 0;
    char buf[MAX_SIZE] = {0};
    fd_set serFdset;

    // 创建socket
    sockFd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockFd < 0) {
        printf("create socket failed, errno: %d\n", errno);
        return ERR;
    }

    printf("socket fd: %d\n", sockFd);

    // 配置地址信息
    localAddr.sin_family = AF_INET;
    localAddr.sin_port = htons(PORT);
    inet_pton(AF_INET, ADDR, &iLocakAddr);
    localAddr.sin_addr.s_addr = iLocakAddr;

    // 绑定地址和socket
    iRet = bind(sockFd, &localAddr, sizeof(localAddr));
    if (iRet < 0) {
        printf("bind socket failed, errno: %d\n", errno);
        return ERR;
    }

    // 监听socket链接请求
    iRet = listen(sockFd, BACKLOG);
    if (iRet < 0) {
        printf("listen failed, errno: %d\n", errno);
        return ERR;
    }

    // 循环处理消息
    while (1) {

        // 重置fdset
        FD_ZERO(&serFdset);

        // 添加标准输入句柄
        FD_SET(0, &serFdset);
        if (maxFd < 0) {
            maxFd = 0;
        }

        // 添加server监听句柄
        FD_SET(sockFd, &serFdset);
        if (maxFd < sockFd) {
            maxFd = sockFd;
        }

        // 添加已经建链的通信句柄
        for (int i = 0; i < CLIENT_NUM; i++) {
            printf("client[%d] fd[%d]\n", i, g_clientFds[i]);
            if (g_clientFds[i] != 0) {
                FD_SET(g_clientFds[i], &serFdset);
                if (maxFd < g_clientFds[i]) {
                    maxFd = g_clientFds;
                }
            }
        }

        PrintFdSet(&serFdset);

        // 等待消息
        iRet = select(maxFd + 1, &serFdset, NULL, NULL, NULL);
        if (iRet < 0) {
            printf("select failed, errno: %d\n", errno);
            continue;
        } else {
            if (FD_ISSET(sockFd, &serFdset)) {
                // 有新的客户端连接
                newSockFd = accept(sockFd, &remoteAddr, &addrlen);
                flag = -1;
                if (newSockFd < 0) {
                    printf("accept failed, errno: %d\n", errno);
                } else {
                    // 将新的连接句柄存储在client池中
                    for (int i = 0; i < CLIENT_NUM; i++) {
                        if (g_clientFds[i] <= 0) {
                            g_clientFds[i] = newSockFd;
                            flag = i;
                            break;
                        }
                    }

                    if (flag >= 0) {
                        printf("new client[%d] fd[%d] connect succ\n", flag, newSockFd);
                    } else {
                        char *fullMsg = "the client pool is full";
                        iRet = write(newSockFd, fullMsg, strlen(fullMsg));
                        if (iRet < 0) {
                            printf("write msg failed, errno: %d\n", errno);
                        }
                    }
                }
            }
        }

        // 读取client发送的消息
        for (int i = 0; i < CLIENT_NUM; i++) {
            if (g_clientFds[i] != 0 && FD_ISSET(g_clientFds[i], &serFdset)) {
                iRet = read(g_clientFds[i], buf, MAX_SIZE);
                if (iRet < 0) {
                    printf("read msg failed, fd: %d, errno: %d\n", g_clientFds[i], errno);
                } else if (iRet > 0) {
                    printf("recv msg: %s\n", buf);
                    write(g_clientFds[i], "this is server msg!", strlen("this is server msg!"));
                } else {
                    // 客户端主动断链后,将句柄移除
                    printf("client[%d] fd[%d] disconnection\n", i, g_clientFds[i]);
                    FD_CLR(g_clientFds[i], &serFdset);
                    close(g_clientFds[i]);
                    g_clientFds[i] = 0;
                }
            }
        }
    }

    close(sockFd);

    return OK;
}

五、问题总结

问:为什么select函数的第一个参数需要是fdmax+1
答:这个参数可以理解为fd_set集合的下标,如果当前fdmax为8,那么fd_set应表示为1 0000 0000,此时如果只遍历8位,会导致最高位被忽略,所以需要给fdamx+1,保证所有句柄都可以被检测到


总结

select函数是c语言网络编程实现io多路复用的方式之一,它是将已连接的socket都放到一个​句柄集合(fd_set)中​,然后调用 select 函数将句柄集合拷贝到内核里,让内核来检查是否有网络事件产生,检查是通过遍历集合的方式,当检查到有事件产生后,将此socket标记为可读或可写,接着再把整个句柄集合拷贝回用户态里,然后用户态还需要再通过遍历集合的方式找到就绪的句柄,做后续处理。这种方式的优缺点如下:

优点:

  1. 可以同时处理多个I/O操作,可以大大提高程序的并发性能
  2. select函数可以监视多个文件描述符,可以实现同时监视多个网络连接
  3. select函数可以设置超时时间,可以避免网络连接的阻塞
  4. select函数可以处理异常事件,如对端关闭连接等

缺点:

  1. select函数需要维护一个大的文件描述符集合,当文件描述符集合很大时,会导致效率下降
  2. select函数每次调用都需要将文件描述符集合从用户空间拷贝到内核空间,当文件描述符集合很大时,会导致系统开销较大
  3. select函数不能直接处理大量的并发连接,因为每次调用select函数都需要扫描整个文件描述符集合,效率较低
  4. select函数的可移植性较差,不同操作系统的实现方式不同,需要编写不同的代码。

基于以上影响,select在当前网络编程中使用的场景相对较小,但对于初学者来说,select是最直观可以了解到io多路复用机制的一种模型

  • 25
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值