MST

星途 面试题库

面试题:C语言中select多路复用模型的基本原理及使用

请简述C语言中select多路复用模型的工作原理,并给出一个简单的使用select实现同时监听多个套接字的代码示例,说明fd_set结构体的作用及相关函数(如FD_SET、FD_CLR等)的使用方法。
14.5万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

select多路复用模型工作原理

select 是一种 I/O 多路复用技术,用于在多个文件描述符(如套接字)上等待事件发生。其原理如下:

  1. 初始化:将需要监听的文件描述符集合(如套接字描述符)添加到 fd_set 结构体中。
  2. 调用 select 函数:该函数会阻塞,直到指定的文件描述符集合中有一个或多个文件描述符上有事件发生(如可读、可写或异常),或者超时。
  3. 检查结果select 函数返回后,通过检查 fd_set 结构体中哪些文件描述符被标记为就绪,来确定哪些文件描述符上发生了事件。

代码示例

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

#define PORT 8080
#define MAX_CLIENTS 10

int main(int argc, char const *argv[]) {
    int server_fd, new_socket, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字
    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");
        exit(EXIT_FAILURE);
    }

    fd_set read_fds;
    fd_set tmp_fds;
    FD_ZERO(&read_fds);
    FD_ZERO(&tmp_fds);
    FD_SET(server_fd, &read_fds);

    int activity, new_socket_fd;
    while (1) {
        tmp_fds = read_fds;
        activity = select(FD_SETSIZE, &tmp_fds, NULL, NULL, NULL);

        if ((activity < 0) && (errno != EINTR)) {
            printf("select error");
        } else if (activity) {
            if (FD_ISSET(server_fd, &tmp_fds)) {
                if ((new_socket_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
                    perror("accept");
                    exit(EXIT_FAILURE);
                }
                printf("New connection, socket fd is %d, ip is : %s, port : %d \n", new_socket_fd, inet_ntoa(address.sin_addr), ntohs(address.sin_port));
                FD_SET(new_socket_fd, &read_fds);
            }
            for (int i = 0; i < FD_SETSIZE; i++) {
                if (FD_ISSET(i, &tmp_fds)) {
                    if (i != server_fd) {
                        valread = read(i, buffer, 1024);
                        if (valread == 0) {
                            getpeername(i, (struct sockaddr *)&address, (socklen_t *)&addrlen);
                            printf("Host disconnected, ip %s, port %d \n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
                            close(i);
                            FD_CLR(i, &read_fds);
                        } else {
                            buffer[valread] = '\0';
                            printf("Message from client %d : %s \n", i, buffer);
                        }
                    }
                }
            }
        }
    }
    return 0;
}

fd_set 结构体及相关函数

  1. fd_set 结构体fd_set 是一个用于存储文件描述符集合的数据结构。它本质上是一个位图,每一位对应一个文件描述符,通过设置或清除相应的位来表示某个文件描述符是否在集合中。
  2. FD_SET 函数:用于将指定的文件描述符添加到 fd_set 集合中。
    • 函数原型void FD_SET(int fd, fd_set *set);
    • 示例FD_SET(server_fd, &read_fds);server_fd 添加到 read_fds 集合中。
  3. FD_CLR 函数:用于从 fd_set 集合中移除指定的文件描述符。
    • 函数原型void FD_CLR(int fd, fd_set *set);
    • 示例FD_CLR(new_socket_fd, &read_fds);read_fds 集合中移除 new_socket_fd
  4. FD_ISSET 函数:用于检查指定的文件描述符是否在 fd_set 集合中。
    • 函数原型int FD_ISSET(int fd, fd_set *set);
    • 示例if (FD_ISSET(server_fd, &tmp_fds)) {... } 检查 server_fd 是否在 tmp_fds 集合中。
  5. FD_ZERO 函数:用于清空 fd_set 集合,即将所有位都设置为 0。
    • 函数原型void FD_ZERO(fd_set *set);
    • 示例FD_ZERO(&read_fds); 清空 read_fds 集合。