最新资讯

  • 解码服务器IO模型

解码服务器IO模型

2026-01-29 12:26:25 栏目:最新资讯 3 阅读

IO 模型

服务器 IO 模型是服务端网络程序同时处理多个套接字的核心方案,无论是 UDP 还是 TCP 服务器,都需通过合理的 IO 模型应对多客户端请求场景。以下从核心概念到具体模型,结合代码示例与详细解释展开说明。

UDP 与 TCP 服务器的基础特性

UDP 服务器特性

UDP 无需连接,服务端仅需一个套接字即可与任意客户端通信。但默认套接字为阻塞型,调用recvfrom()时若客户端无消息,会导致服务器无限期阻塞,无法执行其他操作。

TCP 服务器特性

TCP 为面向连接协议,客户端连接成功后,服务端会新增一个已连接套接字与之对应。随着客户端数量增加,套接字数量同步增多,需高效管理多个套接字的 IO 操作。

核心 IO 模型详解

非阻塞轮询模型

核心逻辑

将所有套接字设为非阻塞模式,避免因客户端无数据导致服务器卡死;通过持续轮询套接字,检测是否有数据到达并处理。

关键技术:fcntl 函数(设置套接字非阻塞属性)

套接字在 Linux 系统中属于文件,通过fcntl()函数可修改文件(套接字)属性,实现非阻塞模式配置。

/**
 * fcntl函数:修改或获取文件描述符属性,用于配置套接字非阻塞模式
 * @brief 对打开的文件描述符执行指定操作,此处用于设置套接字为非阻塞
 * @param fd 目标文件描述符(此处为套接字描述符)
 * @param cmd 操作命令(F_GETFL获取属性,F_SETFL设置属性)
 * @param arg 命令参数(F_SETFL时传入包含O_NONBLOCK的属性值)
 * @return int 成功返回值取决于cmd:F_GETFL返回文件状态标志;F_SETFL返回0;失败返回-1
 * @note 必须先获取原有属性,通过位或运算添加非阻塞属性,避免覆盖原有配置
 */
int fcntl(int fd, int cmd, ... /* arg */ );
// UDP非阻塞轮询服务器示例
#include 
#include 
#include 
#include 
#include 
#include 
int set_nonblocking(int sockfd) {
    // 获取套接字原有属性
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL failed");
        return -1;
    }
    // 位或运算添加非阻塞属性(O_NONBLOCK)
    flags |= O_NONBLOCK;
    // 重新设置套接字属性
    if (fcntl(sockfd, F_SETFL, flags) == -1) {
        perror("fcntl F_SETFL failed");
        return -1;
    }
    return 0;
}

int main() {
    // 创建UDP套接字
    int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_sock == -1) {
        perror("socket create failed");
        return -1;
    }

    // 设置套接字为非阻塞模式
    if (set_nonblocking(udp_sock) == -1) {
        close(udp_sock);
        return -1;
    }

    // 绑定端口和地址
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(udp_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(udp_sock);
        return -1;
    }

    char buf[1024];
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    // 轮询读取客户端数据
    while (1) {
        memset(buf, 0, sizeof(buf));
        // 非阻塞模式下,无数据时返回-1,errno设为EAGAIN或EWOULDBLOCK
        ssize_t recv_len = recvfrom(udp_sock, buf, sizeof(buf)-1, 0, 
                                   (struct sockaddr*)&client_addr, &client_len);
        if (recv_len > 0) {
            printf("收到客户端数据:%s
", buf);
            // 回复客户端(可选)
            sendto(udp_sock, "已收到", 5, 0, (struct sockaddr*)&client_addr, client_len);
        } else if (recv_len == -1) {
            // 无数据可读,可执行其他操作(如日志打印)
            usleep(10000); // 减少CPU占用
        }
    }

    close(udp_sock);
    return 0;
}

注意事项

  • 配置非阻塞属性时,必须先通过F_GETFL获取原有属性,再用位或运算添加O_NONBLOCK,避免覆盖套接字原有配置。
  • 轮询会持续占用 CPU 资源,可通过usleep()等函数适当延时,降低资源消耗。

多任务并发模型

核心逻辑

通过多进程或多线程同时处理多个套接字,每个套接字分配独立的执行单元,实现并行 IO 操作。通常处理网络套接字数据优先使用多线程(资源开销低于多进程)。

分类实现

  • UDP 多线程实现:单个 UDP 套接字交由专门线程管理,负责收发所有客户端数据。

  • TCP 多线程实现:独立线程监听连接请求,每成功建立一个连接,创建新线程对应已连接套接字。

#include 
#include 
#include 
#include 
#include 
#include 
// 客户端信息结构体(用于链表存储)
typedef struct ClientInfo {
    int conn_fd; // 已连接套接字描述符
    struct sockaddr_in addr; // 客户端地址
    struct ClientInfo* next; // 链表节点指针
} ClientInfo;

// 客户端链表头节点(全局变量,需加锁保护)
ClientInfo* client_list = NULL;
pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER;

/**
 * 客户端处理线程函数:负责单个TCP客户端的通信
 * @brief 循环读取客户端数据,处理后回复,断开连接时移除链表节点
 * @param arg 传入ClientInfo结构体指针
 * @return void* 线程返回值(NULL)
 */
void* handle_client(void* arg) {
    ClientInfo* client = (ClientInfo*)arg;
    char buf[1024];
    ssize_t recv_len;

    while (1) {
        memset(buf, 0, sizeof(buf));
        // 读取客户端数据
        recv_len = recv(client->conn_fd, buf, sizeof(buf)-1, 0);
        if (recv_len <= 0) {
            // 客户端断开连接或读取失败
            printf("客户端断开连接
");
            break;
        }
        printf("收到客户端数据:%s
", buf);
        // 回复客户端
        send(client->conn_fd, "处理完成", 8, 0);
    }

    // 关闭套接字
    close(client->conn_fd);
    // 从链表中移除客户端信息(加锁保护)
    pthread_mutex_lock(&client_mutex);
    ClientInfo* prev = NULL;
    ClientInfo* curr = client_list;
    while (curr != NULL) {
        if (curr == client) {
            if (prev == NULL) {
                client_list = curr->next;
            } else {
                prev->next = curr->next;
            }
            break;
        }
        prev = curr;
        curr = curr->next;
    }
    pthread_mutex_unlock(&client_mutex);
    // 释放内存
    free(client);
    pthread_exit(NULL);
}

/**
 * 监听线程函数:负责监听TCP连接请求
 * @brief 循环accept客户端连接,创建新线程处理,将客户端信息加入链表
 * @param arg 传入监听套接字描述符
 * @return void* 线程返回值(NULL)
 */
void* listen_thread(void* arg) {
    int listen_fd = *(int*)arg;
    ClientInfo* new_client;
    pthread_t tid;
    socklen_t client_len = sizeof(struct sockaddr_in);

    while (1) {
        // 分配客户端信息内存
        new_client = (ClientInfo*)malloc(sizeof(ClientInfo));
        if (new_client == NULL) {
            perror("malloc failed");
            continue;
        }
        // 接受客户端连接
        new_client->conn_fd = accept(listen_fd, (struct sockaddr*)&new_client->addr, &client_len);
        if (new_client->conn_fd == -1) {
            perror("accept failed");
            free(new_client);
            continue;
        }
        new_client->next = NULL;

        // 将客户端信息加入链表(加锁保护)
        pthread_mutex_lock(&client_mutex);
        new_client->next = client_list;
        client_list = new_client;
        pthread_mutex_unlock(&client_mutex);

        // 创建线程处理客户端通信
        if (pthread_create(&tid, NULL, handle_client, (void*)new_client) != 0) {
            perror("pthread_create failed");
            close(new_client->conn_fd);
            free(new_client);
        }
        // 分离线程,自动释放资源
        pthread_detach(tid);
    }
}

// TCP多线程并发服务器主函数
int main() {
    int listen_fd;
    struct sockaddr_in serv_addr;
    pthread_t listen_tid;

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听(backlog设为10,允许10个等待连接)
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 创建监听线程
    if (pthread_create(&listen_tid, NULL, listen_thread, (void*)&listen_fd) != 0) {
        perror("pthread_create listen thread failed");
        close(listen_fd);
        return -1;
    }

    // 主线程等待(可添加信号处理等逻辑)
    pthread_join(listen_tid, NULL);
    close(listen_fd);
    pthread_mutex_destroy(&client_mutex);
    return 0;
}

注意事项

  • 用链表存储客户端信息,断开连接时需从链表中移除,操作链表需加互斥锁(pthread_mutex_t),避免线程安全问题。
  • 线程创建后可通过pthread_detach()设置分离属性,自动释放线程资源,无需主线程等待。

异步信号模型

核心逻辑

利用信号的异步特性处理套接字 IO,当套接字有数据到达时,内核触发指定信号,服务器通过信号处理函数处理数据。

关键信号:SIGIO

SIGIO 是内核针对套接字数据到达触发的信号,默认会杀死目标进程,需自定义信号处理函数,且需指定信号宿主(避免多进程 / 线程间信号混乱)。

服务器代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int udp_sock; // 全局UDP套接字描述符(信号处理函数中使用)
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);

/**
 * SIGIO信号处理函数:处理UDP套接字的数据接收
 * @brief 当套接字有数据到达时触发,读取数据并回复客户端
 * @param sig 信号编号(此处为SIGIO)
 */
void sigio_handler(int sig) {
    char buf[1024];
    memset(buf, 0, sizeof(buf));
    // 读取客户端数据
    ssize_t recv_len = recvfrom(udp_sock, buf, sizeof(buf)-1, 0, 
                               (struct sockaddr*)&client_addr, &client_len);
    if (recv_len > 0) {
        printf("收到客户端数据:%s
", buf);
        // 回复客户端
        sendto(udp_sock, "已处理", 9, 0, (struct sockaddr*)&client_addr, client_len);
    }
}

// UDP异步信号驱动服务器
int main() {
    struct sockaddr_in serv_addr;

    // 创建UDP套接字
    udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_sock == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(udp_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(udp_sock);
        return -1;
    }

    // 设置SIGIO信号处理函数
    signal(SIGIO,sigio_handler);

    // 指定SIGIO信号宿主(当前进程)
    if (fcntl(udp_sock, F_SETOWN, getpid()) == -1) {
        perror("fcntl F_SETOWN failed");
        close(udp_sock);
        return -1;
    }

    // 开启套接字异步模式(触发SIGIO信号)
    int flags = fcntl(udp_sock, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL failed");
        close(udp_sock);
        return -1;
    }
    flags |= O_ASYNC; // 开启异步模式
    if (fcntl(udp_sock, F_SETFL, flags) == -1) {
        perror("fcntl F_SETFL failed");
        close(udp_sock);
        return -1;
    }

    // 主线程阻塞等待信号(可执行其他逻辑)
    while (1) {
        sleep(1); // 避免进程退出
    }

    close(udp_sock);
    return 0;
}

客户端代码

#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024
#define DEFAULT_PORT 8888
#define DEFAULT_IP "127.0.0.1"

int main(int argc, char *argv[]) {
    int client_sock;
    struct sockaddr_in serv_addr;
    char send_buf[BUF_SIZE];
    char recv_buf[BUF_SIZE];
    ssize_t send_len, recv_len;
    
    // 解析命令行参数(可选指定服务器IP和端口)
    const char *serv_ip = DEFAULT_IP;
    int serv_port = DEFAULT_PORT;
    if (argc == 2) {
        serv_ip = argv[1];
    } else if (argc == 3) {
        serv_ip = argv[1];
        serv_port = atoi(argv[2]);
    } else if (argc > 3) {
        fprintf(stderr, "用法:%s [服务器IP] [端口]
", argv[0]);
        fprintf(stderr, "示例:%s 127.0.0.1 8888(默认)
", argv[0]);
        return -1;
    }

    // 创建UDP套接字
    client_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (client_sock == -1) {
        perror("socket create failed");
        return -1;
    }

    // 配置服务器地址结构
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;                  // IPv4协议
    serv_addr.sin_port = htons(serv_port);           // 服务器端口(主机字节序转网络字节序)
    if (inet_pton(AF_INET, serv_ip, &serv_addr.sin_addr) <= 0) {
        perror("invalid server IP");
        close(client_sock);
        return -1;
    }

    printf("=== UDP客户端启动 ===
");
    printf("服务器地址:%s:%d
", serv_ip, serv_port);
    printf("输入消息发送(输入 quit 退出)
");

    // 循环读取输入并发送数据
    while (1) {
        // 读取用户输入
        printf("> ");
        fflush(stdout);  // 刷新输出缓冲区,确保提示符号正常显示
        if (fgets(send_buf, BUF_SIZE, stdin) == NULL) {
            perror("read input failed");
            break;
        }

        // 去除换行符(fgets会读取回车符)
        send_buf[strcspn(send_buf, "
")] = '';

        // 退出条件(输入quit/exit)
        if (strcmp(send_buf, "quit") == 0 || strcmp(send_buf, "exit") == 0) {
            printf("客户端主动退出
");
            break;
        }

        // 发送数据到服务器
        send_len = sendto(client_sock, send_buf, strlen(send_buf), 0,
                         (struct sockaddr*)&serv_addr, sizeof(serv_addr));
        if (send_len == -1) {
            perror("sendto failed");
            continue;
        }

        // 接收服务器回复(设置超时避免无限阻塞,可选)
        struct timeval timeout = {3, 0};  // 3秒超时
        setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
        
        memset(recv_buf, 0, sizeof(recv_buf));
        recv_len = recvfrom(client_sock, recv_buf, BUF_SIZE-1, 0, NULL, NULL);
        if (recv_len > 0) {
            printf("服务器回复:%s
", recv_buf);
        } else if (recv_len == 0) {
            printf("服务器关闭连接
");
        } else {
            printf("接收超时或失败
");
        }
    }

    // 关闭套接字
    close(client_sock);
    return 0;
}

注意事项

  • 必须自定义 SIGIO 信号处理函数,否则信号触发时会终止进程。
  • 需通过fcntl(udp_sock, F_SETOWN, getpid())指定信号宿主,确保信号发送到当前进程。
  • 仅适用于 UDP 协议:TCP 中连接请求、数据传输、断开连接等操作都会触发 SIGIO,无法通过单一信号区分操作类型,无法精准处理。
  • 信号处理函数中应避免复杂操作,优先采用简单的数据读取和回复逻辑,避免信号重入问题。

多路复用模型(select/poll)

核心逻辑

通过特定接口(select/poll 函数)同时监控多个阻塞套接字,当某个 / 某些套接字状态就绪(可读 / 可写)时,再进行对应 IO 操作。无需多进程 / 线程,即可高效处理多个阻塞套接字。

select 函数实现

#include 
/**
 * select函数:同步IO多路复用,监控多个文件描述符状态
 * @brief 阻塞等待监控的文件描述符就绪,就绪后返回并标记就绪描述符
 * @param nfds 监控的最大文件描述符+1(内核遍历范围)
 * @param readfds 可读状态监控集合(NULL表示不监控)
 * @param writefds 可写状态监控集合(NULL表示不监控)
 * @param exceptfds 异常状态监控集合(NULL表示不监控)
 * @param timeout 超时时间:NULL(无限阻塞)、{0,0}(立即返回)、具体时间(秒+微秒)
 * @return int 成功返回就绪描述符总数;失败返回-1;超时返回0
 * @note 返回后未就绪的描述符会被自动清零,需重新初始化集合
 *       文件描述符集合大小受FD_SETSIZE限制(默认1024)
 */
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

/**
 * FD_ZERO:清空文件描述符集合
 * @brief 将fd_set中的所有位设为0,初始化集合(必须在使用前调用)
 * @param set 指向要清空的fd_set结构体的指针(不可为NULL)
 * @return void 无返回值
 * @note 若不初始化,集合中会残留随机值,导致select监控异常
 */
void FD_ZERO(fd_set *set);

/**
 * FD_SET:将指定文件描述符添加到集合中
 * @brief 把fd对应的位图位设为1,让select监控该fd
 * @param fd 要添加的文件描述符(必须是有效的非负整数,且小于FD_SETSIZE)
 * @param set 指向目标fd_set结构体的指针(不可为NULL)
 * @return void 无返回值
 * @note 1. fd不能超过FD_SETSIZE(默认1024),否则会越界导致内存错误
 *       2. 重复添加同一fd无效果(位图位已为1),不会报错
 */
void FD_SET(int fd, fd_set *set);

/**
 * FD_CLR:将指定文件描述符从集合中移除
 * @brief 把fd对应的位图位设为0,让select停止监控该fd
 * @param fd 要移除的文件描述符(需是已添加到集合中的有效fd)
 * @param set 指向目标fd_set结构体的指针(不可为NULL)
 * @return void 无返回值
 * @note 移除不存在的fd无效果,不会报错
 */
void FD_CLR(int fd, fd_set *set);

/**
 * FD_ISSET:检查文件描述符是否在就绪集合中
 * @brief 判断fd对应的位图位是否为1,用于select返回后检测哪个fd就绪
 * @param fd 要检查的文件描述符(非负整数)
 * @param set 指向select返回后的fd_set结构体的指针(不可为NULL)
 * @return int 就绪返回非零值(通常为1),未就绪返回0
 * @note select会修改原集合,仅保留就绪fd的位为1,未就绪fd的位会被清0
 */
int FD_ISSET(int fd, fd_set *set);
#include 
#include 
#include 
#include 
#include 
#include 

// TCP多路复用服务器(select实现)
int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    fd_set read_fds, temp_fds; // 监控集合(temp_fds用于临时备份)
    int max_fd; // 最大文件描述符

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 初始化文件描述符集合
    FD_ZERO(&read_fds); // 清空集合
    FD_SET(listen_fd, &read_fds); // 添加监听套接字到监控集合
    max_fd = listen_fd; // 初始最大描述符为监听套接字

    struct timeval timeout = {3, 0}; // 超时时间3秒
    char buf[1024];
    ssize_t recv_len;

    while (1) {
        // 备份监控集合(select会修改原集合,需重新初始化)
        temp_fds = read_fds;

        // 调用select监控可读状态
        int ret = select(max_fd + 1, &temp_fds, NULL, NULL, &timeout);
        if (ret == -1) {
            perror("select failed");
            continue;
        } else if (ret == 0) {
            // 超时,无就绪描述符
            printf("select timeout...
");
            continue;
        }

        // 遍历所有可能的文件描述符,检查就绪状态
        for (int fd = 0; fd <= max_fd; fd++) {
            if (FD_ISSET(fd, &temp_fds)) { // 检查fd是否在就绪集合中
                if (fd == listen_fd) {
                    // 监听套接字就绪,接受新连接
                    conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                    if (conn_fd == -1) {
                        perror("accept failed");
                        continue;
                    }
                    printf("新客户端连接,fd=%d
", conn_fd);
                    FD_SET(conn_fd, &read_fds); // 将新连接加入监控集合
                    if (conn_fd > max_fd) {
                        max_fd = conn_fd; // 更新最大描述符
                    }
                } else {
                    // 已连接套接字就绪,读取数据
                    memset(buf, 0, sizeof(buf));
                    recv_len = recv(fd, buf, sizeof(buf)-1, 0);
                    if (recv_len <= 0) {
                        // 客户端断开连接
                        printf("客户端断开,fd=%d
", fd);
                        close(fd);
                        FD_CLR(fd, &read_fds); // 从监控集合中移除
                        // 更新max_fd(可选,优化遍历效率)
                        if (fd == max_fd) {
                            while (!FD_ISSET(max_fd, &read_fds) && max_fd > 0) {
                                max_fd--;
                            }
                        }
                    } else {
                        printf("收到数据(fd=%d):%s
", fd, buf);
                        send(fd, "已处理", 5, 0); // 回复客户端
                    }
                }
            }
        }
    }

    close(listen_fd);
    return 0;
}

poll 函数实现

/**
 * poll函数:同步IO多路复用,监控多个文件描述符状态(select增强版)
 * @brief 阻塞等待监控的文件描述符就绪,通过结构体数组管理监控对象
 * @param fds 监控结构体数组(每个元素对应一个描述符及监控事件)
 * @param nfds 监控的结构体数组长度
 * @param timeout 超时时间(毫秒):-1(无限阻塞)、0(立即返回)、>0(具体毫秒数)
 * @return int 成功返回就绪描述符总数;失败返回-1;超时返回0
 * @note 无需重新初始化监控集合,就绪事件会在结构体中标记
 *       监控数量无FD_SETSIZE限制,仅受系统资源约束
 */
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#include 
#include 
#include 
#include 
#include 
#include 

// TCP多路复用服务器(poll实现)
#define MAX_CLIENT 1024 // 最大客户端数量
int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    struct pollfd fds[MAX_CLIENT]; // poll监控结构体数组
    int nfds = 1; // 初始监控数量(仅监听套接字)
    char buf[1024];
    ssize_t recv_len;

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 初始化poll监控数组(监听套接字,监控可读事件)
    fds[0].fd = listen_fd;
    fds[0].events = POLLIN; // 监控可读事件
    fds[0].revents = 0; // 就绪事件初始化为0

    while (1) {
        // 调用poll监控事件,超时时间3000毫秒(3秒)
        int ret = poll(fds, nfds, 3000);
        if (ret == -1) {
            perror("poll failed");
            continue;
        } else if (ret == 0) {
            printf("poll timeout...
");
            continue;
        }

        // 遍历监控数组,处理就绪事件
        for (int i = 0; i < nfds; i++) {
            if (fds[i].revents & POLLIN) { // 可读事件就绪
                if (fds[i].fd == listen_fd) {
                    // 监听套接字就绪,接受新连接
                    conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                    if (conn_fd == -1) {
                        perror("accept failed");
                        continue;
                    }
                    if (nfds >= MAX_CLIENT) {
                        printf("客户端数量已满,拒绝连接
");
                        close(conn_fd);
                        continue;
                    }
                    // 添加新连接到poll监控数组
                    fds[nfds].fd = conn_fd;
                    fds[nfds].events = POLLIN;
                    fds[nfds].revents = 0;
                    nfds++;
                    printf("新客户端连接,fd=%d,当前连接数=%d
", conn_fd, nfds-1);
                } else {
                    // 已连接套接字就绪,读取数据
                    memset(buf, 0, sizeof(buf));
                    recv_len = recv(fds[i].fd, buf, sizeof(buf)-1, 0);
                    if (recv_len <= 0) {
                        // 客户端断开连接
                        printf("客户端断开,fd=%d
", fds[i].fd);
                        close(fds[i].fd);
                        // 移除该客户端(用最后一个元素覆盖,减少遍历)
                        fds[i] = fds[nfds - 1];
                        nfds--;
                        i--; // 重新检查当前位置(已被替换为原最后一个元素)
                    } else {
                        printf("收到数据(fd=%d):%s
", fds[i].fd, buf);
                        send(fds[i].fd, "已处理", 5, 0);
                    }
                }
                // 清除就绪事件标记
                fds[i].revents = 0;
            }
        }
    }

    close(listen_fd);
    return 0;
}

高效 IO 多路复用 ——epoll 模型

epoll 是 Linux 内核 2.6 版本后引入的高级 IO 多路复用机制,专门针对 select/poll 在高并发场景下的效率瓶颈设计。它通过红黑树管理监控的文件描述符、就绪链表存储就绪事件,实现了 “只关心就绪 fd” 的高效通知机制,是高并发服务器(如 Nginx、Redis)的核心 IO 模型。

epoll 核心原理

epoll 的高效源于其底层两种关键数据结构:

  • 红黑树:用于管理所有需要监控的文件描述符(fd),支持高效的添加、删除、查询操作(时间复杂度 O (log n)),解决了 select/poll “遍历所有 fd” 的效率问题。
  • 就绪链表:内核会主动将就绪的 fd 加入该链表,epoll_wait 只需从链表中获取就绪 fd,无需遍历全部监控对象,大幅提升高并发场景下的性能。

epoll 核心函数详解

epoll 通过 3 个核心函数完成监控流程,以下结合代码注释详细说明:

epoll_create—— 创建 epoll 实例

#include 
/**
 * epoll_create:创建epoll实例,返回用于操作epoll的文件描述符
 * @brief 初始化epoll的红黑树和就绪链表,后续通过该fd管理监控事件
 * @param size 早期版本用于指定监控fd的最大数量,Linux 2.6.8后被忽略(仅需传>0的值)
 * @return int 成功返回epoll实例fd(非负整数),失败返回-1(errno标记错误)
 * @note 每个epoll实例对应一个独立的监控集合,需用close()关闭
 *       size参数仅为兼容性保留,实际监控数量由系统资源决定(无硬限制)
 */
int epoll_create(int size);

epoll_ctl—— 管理监控事件(添加 / 修改 / 删除)

/**
 * epoll_ctl:向epoll实例添加、修改或删除监控的文件描述符及事件
 * @brief 操作epoll红黑树,维护监控的fd和对应的事件类型
 * @param epfd epoll实例的文件描述符(由epoll_create返回)
 * @param op 操作类型:
 *           - EPOLL_CTL_ADD:添加新fd到监控集合
 *           - EPOLL_CTL_MOD:修改已监控fd的事件类型
 *           - EPOLL_CTL_DEL:从监控集合中删除fd(event参数可设为NULL)
 * @param fd 要操作的目标文件描述符(如套接字fd)
 * @param event 指向epoll_event结构体的指针,描述监控的事件类型及附加数据
 * @return int 成功返回0,失败返回-1(errno标记错误,如fd已存在、权限不足)
 * @note 添加fd时,需确保fd非阻塞(尤其ET模式下)
 *       删除fd时,无需指定event的具体内容,仅需传入非NULL指针即可
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

// epoll_event结构体:描述监控的事件类型和附加数据
struct epoll_event {
    uint32_t events;  // 监控的事件类型(位图)
    epoll_data_t data;// 附加数据(可存储fd、指针等,用于区分不同fd)
};

// epoll_data_t联合体:存储附加数据的灵活结构
typedef union epoll_data {
    void    *ptr;     // 指向自定义数据的指针(如客户端信息结构体)
    int      fd;      // 直接存储目标fd(最常用)
    uint32_t u32;     // 32位无符号整数
    uint64_t u64;     // 64位无符号整数
} epoll_data_t;

// 常用事件类型(events参数可选值)
#define EPOLLIN      0x001  // 可读事件(fd有数据可读取)
#define EPOLLOUT     0x004  // 可写事件(fd可写入数据,不阻塞)
#define EPOLLPRI     0x002  // 紧急数据可读事件(如TCP带外数据)
#define EPOLLERR     0x008  // 错误事件(fd发生错误,会自动触发,无需手动设置)
#define EPOLLHUP     0x010  // 挂起事件(fd连接断开,会自动触发,无需手动设置)
#define EPOLLET      0x80000000  // 边缘触发模式(默认是水平触发LT)
#define EPOLLONESHOT 0x40000000  // 一次性事件(触发后自动删除监控,需重新添加)

epoll_wait—— 等待就绪事件

/**
 * epoll_wait:等待epoll实例中监控的fd就绪,获取就绪事件
 * @brief 阻塞等待就绪事件,从就绪链表中复制就绪fd到events数组
 * @param epfd epoll实例的文件描述符
 * @param events 输出参数:用于存储就绪事件的数组(需提前分配内存)
 * @param maxevents events数组的最大容量(必须>0,且不超过监控的fd总数)
 * @param timeout 超时时间(毫秒):
 *                - -1:无限阻塞,直到有fd就绪或被信号中断
 *                - 0:立即返回,无论是否有fd就绪(轮询模式)
 *                - >0:等待指定毫秒数,超时后返回0
 * @return int 成功返回就绪事件的数量(0表示超时),失败返回-1(errno标记错误)
 * @note 就绪事件会被复制到events数组,数组中仅包含就绪的fd,无需遍历全部监控对象
 *       同一fd的多个事件(如EPOLLIN+EPOLLOUT)会被合并为一个就绪项
 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll 的两种触发模式(关键特性)

epoll 支持水平触发(LT)边缘触发(ET) 两种事件通知方式,直接影响程序逻辑设计,是其与 select/poll 的核心区别之一。

水平触发(LT,Level Trigger)—— 默认模式

  • 触发逻辑:只要 fd 处于就绪状态(如可读),每次调用 epoll_wait 都会返回该 fd,直到 fd 的就绪状态消失(如数据被读完)。
  • 特点
    • 逻辑简单,兼容性好(类似 select/poll 的触发逻辑);
    • 无需一次性读完所有数据,可分多次读取;
    • 允许使用阻塞或非阻塞 fd(推荐非阻塞,避免单个 fd 阻塞影响其他 fd)。
  • 适用场景:新手入门、对性能要求不极致的场景,或需要兼容旧逻辑的代码。

边缘触发(ET,Edge Trigger)—— 高效模式

  • 触发逻辑:仅在 fd 的状态从 “未就绪” 变为 “就绪” 时触发一次,后续即使 fd 仍处于就绪状态(如还有数据未读),也不会再触发,直到状态再次变化(如又有新数据到达)。
  • 特点
    • 效率极高,仅通知状态变化,减少 epoll_wait 的返回次数;
    • 必须使用非阻塞 fd(否则若数据未读完,fd 会阻塞在 read/write,导致其他 fd 无法处理);
    • 必须一次性读完 / 写完所有数据(需循环调用 read/write,直到返回 - 1 且 errno 为 EAGAIN/EWOULDBLOCK)。
  • 适用场景:高并发场景(如万级以上连接),追求极致性能的服务器(如 Nginx)。

epoll 服务器代码示例(TCP)

水平触发(LT)模式示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define MAX_EVENTS 1024  // epoll_wait返回的最大就绪事件数
#define PORT 8888
int main() {
    int listen_fd, conn_fd, epfd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    struct epoll_event ev, events[MAX_EVENTS]; // ev用于添加事件,events存储就绪事件
    char buf[1024];
    ssize_t recv_len;

    // 创建TCP监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听(允许10个等待连接)
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 创建epoll实例
    epfd = epoll_create(1); // size参数忽略,传>0即可
    if (epfd == -1) {
        perror("epoll_create failed");
        close(listen_fd);
        return -1;
    }

    // 将监听套接字添加到epoll监控(LT模式,监控可读事件)
    ev.events = EPOLLIN;       // 可读事件
    ev.data.fd = listen_fd;    // 附加数据:存储监听fd
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
        perror("epoll_ctl add listen_fd failed");
        close(listen_fd);
        close(epfd);
        return -1;
    }

    printf("LT模式epoll服务器启动,端口:%d
", PORT);

    // 循环等待就绪事件
    while (1) {
        // 调用epoll_wait,超时时间-1(无限阻塞)
        int ready_num = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (ready_num == -1) {
            perror("epoll_wait failed");
            continue;
        }

        // 遍历所有就绪事件
        for (int i = 0; i < ready_num; i++) {
            // 情况1:监听套接字就绪(有新客户端连接)
            if (events[i].data.fd == listen_fd) {
                conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                if (conn_fd == -1) {
                    perror("accept failed");
                    continue;
                }
                printf("新客户端连接,fd=%d
", conn_fd);

                // 将新连接的fd添加到epoll监控(LT模式,可读事件)
                ev.events = EPOLLIN;
                ev.data.fd = conn_fd;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
                    perror("epoll_ctl add conn_fd failed");
                    close(conn_fd);
                    continue;
                }
            }
            // 情况2:已连接套接字就绪(有数据可读)
            else if (events[i].events & EPOLLIN) {
                int client_fd = events[i].data.fd;
                memset(buf, 0, sizeof(buf));
                // LT模式:无需循环读,一次读部分数据也可(下次epoll_wait仍会触发)
                recv_len = read(client_fd, buf, sizeof(buf)-1);
                if (recv_len <= 0) {
                    // 客户端断开连接或读取错误
                    if (recv_len < 0) perror("read failed");
                    printf("客户端断开,fd=%d
", client_fd);
                    // 从epoll中删除该fd,并关闭
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
                    close(client_fd);
                } else {
                    printf("收到数据(fd=%d):%s
", client_fd, buf);
                    // 回复客户端(LT模式可直接写,无需监控可写事件)
                    write(client_fd, "LT模式:已收到数据", 18);
                }
            }
        }
    }

    // 关闭资源(实际不会执行到这里)
    close(listen_fd);
    close(epfd);
    return 0;
}

边缘触发(ET)模式示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define MAX_EVENTS 1024
#define PORT 8888
// 工具函数:设置fd为非阻塞模式(ET模式必须)
int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL failed");
        return -1;
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL failed");
        return -1;
    }
    return 0;
}

int main() {
    int listen_fd, conn_fd, epfd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    struct epoll_event ev, events[MAX_EVENTS];
    char buf[1024];
    ssize_t recv_len;

    // 创建监听套接字(ET模式下,监听fd可阻塞,也可设为非阻塞,不影响)
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 监听连接
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 创建epoll实例
    epfd = epoll_create(1);
    if (epfd == -1) {
        perror("epoll_create failed");
        close(listen_fd);
        return -1;
    }

    // 添加监听fd到epoll(ET模式需加EPOLLET标志)
    ev.events = EPOLLIN | EPOLLET; // 可读事件 + 边缘触发
    ev.data.fd = listen_fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
        perror("epoll_ctl add listen_fd failed");
        close(listen_fd);
        close(epfd);
        return -1;
    }

    printf("ET模式epoll服务器启动,端口:%d
", PORT);

    // 循环等待就绪事件
    while (1) {
        int ready_num = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (ready_num == -1) {
            perror("epoll_wait failed");
            continue;
        }

        for (int i = 0; i < ready_num; i++) {
            // 情况1:监听fd就绪(新连接)
            if (events[i].data.fd == listen_fd) {
                // ET模式:需循环accept,避免漏接连接(可能同时有多个连接到达)
                while (1) {
                    conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                    if (conn_fd == -1) {
                        // 没有更多连接时,errno为EAGAIN/EWOULDBLOCK(因listen_fd非阻塞)
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;
                        } else {
                            perror("accept failed");
                            break;
                        }
                    }
                    printf("新客户端连接,fd=%d
", conn_fd);

                    // ET模式:必须将客户端fd设为非阻塞
                    if (set_nonblocking(conn_fd) == -1) {
                        close(conn_fd);
                        continue;
                    }

                    // 添加客户端fd到epoll(ET模式 + 可读事件)
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = conn_fd;
                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
                        perror("epoll_ctl add conn_fd failed");
                        close(conn_fd);
                        continue;
                    }
                }
            }
            // 情况2:客户端fd就绪(有数据可读)
            else if (events[i].events & EPOLLIN) {
                int client_fd = events[i].data.fd;
                // ET模式:必须循环读,直到数据读完(返回-1且errno为EAGAIN)
                while (1) {
                    memset(buf, 0, sizeof(buf));
                    recv_len = read(client_fd, buf, sizeof(buf)-1);
                    if (recv_len > 0) {
                        printf("收到数据(fd=%d):%s
", client_fd, buf);
                        // 若需回复,可添加EPOLLOUT事件监控(避免写阻塞)
                        // 此处简化:直接写(因fd非阻塞,写不完会返回EAGAIN,后续监控EPOLLOUT再写)
                        write(client_fd, "ET模式:已收到数据", 18);
                    } else if (recv_len == 0) {
                        // 客户端正常断开
                        printf("客户端断开,fd=%d
", client_fd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
                        close(client_fd);
                        break;
                    } else {
                        // 读取错误:仅当errno为EAGAIN时表示数据已读完,其他为真错误
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break; // 数据读完,退出循环
                        } else {
                            perror("read failed");
                            epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
                            close(client_fd);
                            break;
                        }
                    }
                }
            }
        }
    }

    close(listen_fd);
    close(epfd);
    return 0;
}

epoll 与 select/poll 的核心区别

特性selectpollepoll
监控数量限制受 FD_SETSIZE(默认 1024)限制无硬限制,仅受系统资源约束无硬限制,支持万级以上连接
底层数据结构位图(固定大小)数组(动态管理)红黑树(管理监控 fd)+ 就绪链表(存储就绪 fd)
效率遍历所有 fd(O (n)),高并发低效遍历所有监控 fd(O (n)),低效仅处理就绪 fd(O (1)),高并发高效
触发模式仅水平触发(LT)仅水平触发(LT)支持 LT 和 ET(边缘触发,更高效)
fd 重复添加重复添加无效果(位图覆盖)重复添加无效果重复添加会返回错误(EEXIST)
内存拷贝每次调用拷贝全部 fd 集合到内核每次调用拷贝全部 fd 集合到内核仅初始化时拷贝,后续复用(零拷贝)
适用场景低并发(<1000 连接)中低并发(<1 万连接)高并发(万级以上连接,如 Web 服务器)

epoll 使用注意事项

  • ET 模式必须用非阻塞 fd:若 fd 阻塞,read/write 未完成时会阻塞进程,导致其他 fd 无法处理。
  • ET 模式需循环读写:仅触发一次状态变化,需循环调用 read/write 直到返回1errno=EAGAIN/EWOULDBLOCK,避免数据残留。
  • 避免监控不必要的事件:如仅需读数据时,不要监控 EPOLLOUT(否则 fd 可写时会频繁触发,浪费资源)。
  • 及时删除无效 fd:客户端断开后,需通过epoll_ctl(EPOLL_CTL_DEL)删除 fd,避免内存泄漏和错误触发。
  • epoll 实例的关闭:不再使用时需调用close(epfd),释放内核资源(红黑树、就绪链表)。

各 IO 模型对比与适用场景

模型优点缺点适用场景
非阻塞轮询实现简单,无多线程开销CPU 占用高,轮询效率低客户端数量少(<100)、数据频繁传输
多任务并发逻辑清晰,并行处理能力强线程 / 进程资源开销大,高并发性能下降客户端数量适中(<1000)、需独立处理连接
异步信号无需轮询,CPU 利用率高仅支持 UDP,信号处理逻辑受限UDP 协议、数据量少且突发的场景
select/poll单进程处理多连接,资源开销低高并发效率低,select 有连接数限制中低并发(<1 万连接)、兼容性要求高
epoll高并发效率极高,支持 ET 模式,无连接限制仅支持 Linux 系统,ET 模式逻辑复杂高并发场景(万级以上连接,如 Nginx、Redis)

  • 套接字本质:Linux 系统中属于特殊文件,通过文件描述符管理,所有文件操作函数(fcntl、read、write 等)均可用于套接字。
  • 阻塞与非阻塞区别:阻塞模式下 IO 操作未完成时,进程 / 线程会挂起等待;非阻塞模式下会立即返回,通过 errno 标记未就绪状态(EAGAIN/EWOULDBLOCK)。
  • 信号安全:信号处理函数中应避免调用非信号安全函数(如 printf、malloc),优先使用异步信号安全函数(如 write),避免程序异常。
  • 高并发设计原则:高并发服务器(如百万连接)通常结合 “epoll + 非阻塞 IO + 线程池” 设计,epoll 负责高效监控 fd,线程池处理 IO 逻辑,平衡性能与资源开销。

综合示例

服务器

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 8889
#define BUF_SIZE 1024
#define MAX_CLIENTS 10  // 最大支持客户端数

// 客户端连接结构体(存储每个客户端的信息)
typedef struct Client {
    int conn_fd;                // 客户端连接套接字
    char ip[INET_ADDRSTRLEN];   // 客户端IP
    int port;                   // 客户端端口
    pthread_t tid;              // 处理该客户端的线程ID
    struct Client *next;        // 链表节点(用于管理多个客户端)
} Client;

// 全局客户端链表(头节点)+ 互斥锁(线程安全操作链表)
Client *client_list = NULL;
pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER;

// 安全发送函数:确保数据完整发送
ssize_t safe_send(int fd, const char *buf, size_t len, int flags) {
    size_t total_sent = 0;
    while (total_sent < len) {
        ssize_t sent = send(fd, buf + total_sent, len - total_sent, flags);
        if (sent == -1) {
            if (errno == EINTR) continue;
            perror("[发送失败]");
            return -1;
        }
        total_sent += sent;
    }
    return total_sent;
}

// 从客户端链表中移除指定conn_fd的客户端(线程安全)
void remove_client(int conn_fd) {
    pthread_mutex_lock(&client_mutex);
    Client *prev = NULL, *curr = client_list;

    while (curr != NULL) {
        if (curr->conn_fd == conn_fd) {
            // 找到目标客户端,从链表中移除
            if (prev == NULL) {
                client_list = curr->next;  // 头节点
            } else {
                prev->next = curr->next;   // 中间/尾节点
            }
            printf("
[客户端管理] 移除客户端:IP=%s:%d(conn_fd=%d)
",
                   curr->ip, curr->port, curr->conn_fd);
            free(curr);  // 释放节点内存
            break;
        }
        prev = curr;
        curr = curr->next;
    }
    pthread_mutex_unlock(&client_mutex);
}

// 打印当前所有在线客户端(线程安全)
void print_clients() {
    pthread_mutex_lock(&client_mutex);
    Client *curr = client_list;
    if (curr == NULL) {
        printf("[客户端管理] 暂无在线客户端
");
        pthread_mutex_unlock(&client_mutex);
        return;
    }

    printf("
[客户端管理] 在线客户端列表:
");
    int count = 0;
    while (curr != NULL) {
        count++;
        printf("  %d. IP=%s:%d | conn_fd=%d
",
               count, curr->ip, curr->port, curr->conn_fd);
        curr = curr->next;
    }
    printf("  总计在线:%d 个
", count);
    pthread_mutex_unlock(&client_mutex);
}

// 客户端处理线程:每个客户端独立线程,核心修复OOB处理逻辑
void *client_handler(void *arg) {
    Client *client = (Client *)arg;
    int conn_fd = client->conn_fd;
    char recv_buf[BUF_SIZE], oob_buf[BUF_SIZE];
    fd_set read_fds, except_fds;  // 读事件集合 + 异常事件集合(关键!)
    int max_fd = conn_fd;
    struct timeval timeout = {3, 0};  // select超时3秒

    printf("
[线程启动] 开始处理客户端:IP=%s:%d(conn_fd=%d)
",
           client->ip, client->port, conn_fd);

    // 线程分离:自动释放资源,无需主线程join
    pthread_detach(pthread_self());

    while (1) {
        // 初始化读事件和异常事件集合(每次select前必须重置)
        FD_ZERO(&read_fds);
        FD_ZERO(&except_fds);
        FD_SET(conn_fd, &read_fds);    // 监听普通数据(读事件)
        FD_SET(conn_fd, &except_fds);  // 监听OOB数据(异常事件)【核心修复】

        // 等待事件:读事件(普通数据)或异常事件(OOB数据)
        int ret = select(max_fd + 1, &read_fds, NULL, &except_fds, &timeout);
        if (ret == -1) {
            if (errno == EINTR) continue;
            perror("[select失败]");
            break;
        } else if (ret == 0) {
            continue;  // 超时,继续监听
        }

        // ------------- 处理OOB带外数据(异常事件触发)-------------
        if (FD_ISSET(conn_fd, &except_fds)) {
            memset(oob_buf, 0, BUF_SIZE);
            // 必须用MSG_OOB标志接收,仅1字节有效
            ssize_t oob_len = recv(conn_fd, oob_buf, BUF_SIZE - 1, MSG_OOB);
            if (oob_len == -1) {
                if (errno == EINTR) continue;
                perror("[接收OOB数据失败]");
                continue;
            }
            // 打印OOB数据(明确标记客户端信息)
            printf("
📢【紧急通知】客户端(%s:%d,conn_fd=%d)发送带外数据:%c(有效字节数:%ld)
",
                   client->ip, client->port, conn_fd, oob_buf[0], oob_len);
            fflush(stdout);  // 刷新缓冲区,避免日志被覆盖
            continue;
        }

        // ------------- 处理普通数据(读事件触发)-------------
        if (FD_ISSET(conn_fd, &read_fds)) {
            memset(recv_buf, 0, BUF_SIZE);
            ssize_t len = recv(conn_fd, recv_buf, BUF_SIZE - 1, 0);
            if (len == -1) {
                if (errno == EINTR) continue;
                perror("[接收普通数据失败]");
                break;
            } else if (len == 0) {
                printf("
[客户端断开] 客户端主动断开:IP=%s:%d(conn_fd=%d)
",
                       client->ip, client->port, conn_fd);
                break;
            }

            // 打印接收的普通数据
            printf("
[接收数据] 客户端(%s:%d,conn_fd=%d):%s(字节数:%ld)
",
                   client->ip, client->port, conn_fd, recv_buf, len);
            fflush(stdout);

            // 收到quit指令,客户端主动退出
            if (strcmp(recv_buf, "quit") == 0) {
                printf("[客户端退出] 收到客户端(%s:%d)退出指令
",
                       client->ip, client->port);
                break;
            }
        }
    }

    // 线程退出:关闭连接,从链表移除客户端
    close(conn_fd);
    remove_client(conn_fd);
    printf("[线程退出] 客户端处理线程结束:IP=%s:%d(conn_fd=%d)
",
           client->ip, client->port, conn_fd);
    return NULL;
}

// 服务器主线程输入处理:支持管理客户端、发送数据给指定客户端
void *server_input_handler(void *arg) {
    char input_buf[BUF_SIZE];
    printf("
=== 服务器管理命令 ===
");
    printf("  list → 查看在线客户端
");
    printf("  send [conn_fd] [内容] → 给指定客户端发送普通数据(如send 4 hello)
");
    printf("  oob [conn_fd] [字符] → 给指定客户端发送带外数据(如oob 4 !)
");
    printf("  help → 查看命令说明
");
    printf("======================
");

    while (1) {
        printf("
服务器命令(输入help查看说明):");
        memset(input_buf, 0, BUF_SIZE);
        if (fgets(input_buf, BUF_SIZE, stdin) == NULL) {
            perror("[读取命令失败]");
            continue;
        }
        input_buf[strcspn(input_buf, "
")] = '';

        // 处理空输入
        if (strlen(input_buf) == 0) continue;

        // 查看在线客户端(list)
        if (strcmp(input_buf, "list") == 0) {
            print_clients();
            continue;
        }

        // 查看帮助(help)
        if (strcmp(input_buf, "help") == 0) {
            printf("
=== 服务器管理命令 ===
");
            printf("  list → 查看在线客户端
");
            printf("  send [conn_fd] [内容] → 给指定客户端发送普通数据(如send 4 hello)
");
            printf("  oob [conn_fd] [字符] → 给指定客户端发送带外数据(如oob 4 !)
");
            printf("  help → 查看命令说明
");
            printf("======================
");
            continue;
        }

        // 发送普通数据(send [conn_fd] [内容])
        if (strncmp(input_buf, "send ", 5) == 0) {
            int target_fd;
            char content[BUF_SIZE];
            if (sscanf(input_buf, "send %d %[^
]", &target_fd, content) != 2) {
                printf("错误:命令格式无效!正确格式:send [conn_fd] [内容](如send 4 hello)
");
                continue;
            }

            // 查找目标客户端是否在线
            pthread_mutex_lock(&client_mutex);
            Client *curr = client_list;
            int found = 0;
            while (curr != NULL) {
                if (curr->conn_fd == target_fd) {
                    found = 1;
                    break;
                }
                curr = curr->next;
            }
            pthread_mutex_unlock(&client_mutex);

            if (!found) {
                printf("错误:conn_fd=%d 的客户端不在线!(输入list查看在线客户端)
", target_fd);
                continue;
            }

            // 发送普通数据
            if (safe_send(target_fd, content, strlen(content), 0) != -1) {
                printf("✅ 已发送普通数据给客户端(conn_fd=%d):%s(字节数:%zu)
",
                       target_fd, content, strlen(content));
            }
            continue;
        }

        // 发送带外数据(oob [conn_fd] [字符])
        if (strncmp(input_buf, "oob ", 4) == 0) {
            int target_fd;
            char oob_char;
            if (sscanf(input_buf, "oob %d %c", &target_fd, &oob_char) != 2) {
                printf("错误:命令格式无效!正确格式:oob [conn_fd] [单个字符](如oob 4 !)
");
                continue;
            }

            // 查找目标客户端是否在线
            pthread_mutex_lock(&client_mutex);
            Client *curr = client_list;
            int found = 0;
            while (curr != NULL) {
                if (curr->conn_fd == target_fd) {
                    found = 1;
                    break;
                }
                curr = curr->next;
            }
            pthread_mutex_unlock(&client_mutex);

            if (!found) {
                printf("错误:conn_fd=%d 的客户端不在线!(输入list查看在线客户端)
", target_fd);
                continue;
            }

            // 发送带外数据(仅1字节有效)
            if (safe_send(target_fd, &oob_char, 1, MSG_OOB) != -1) {
                printf("✅ 已发送带外数据给客户端(conn_fd=%d):%c(仅1字节有效)
",
                       target_fd, oob_char);
            }
            continue;
        }

        // 无效命令
        printf("错误:无效命令!输入help查看支持的命令
");
    }
}

// 服务器Ctrl+C优雅退出处理
void sigint_handler(int sig) {
    printf("

收到Ctrl+C,服务器准备优雅退出...
");
    // 关闭所有客户端连接
    pthread_mutex_lock(&client_mutex);
    Client *curr = client_list;
    while (curr != NULL) {
        Client *tmp = curr;
        curr = curr->next;
        printf("关闭客户端连接:IP=%s:%d(conn_fd=%d)
", tmp->ip, tmp->port, tmp->conn_fd);
        close(tmp->conn_fd);
        free(tmp);
    }
    client_list = NULL;
    pthread_mutex_unlock(&client_mutex);
    // 销毁互斥锁
    pthread_mutex_destroy(&client_mutex);
    printf("服务器已退出,所有资源已释放!
");
    exit(EXIT_SUCCESS);
}

int main() {
    int listen_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    pthread_t input_tid;  // 服务器输入处理线程(管理命令)

    // 信号处理:忽略SIGPIPE,捕获SIGINT(Ctrl+C)
    signal(SIGPIPE, SIG_IGN);
    signal(SIGINT, sigint_handler);

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket创建失败");
        exit(EXIT_FAILURE);
    }

    // 地址复用+绑定+监听
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("绑定失败");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(listen_fd, MAX_CLIENTS) == -1) {
        perror("监听失败");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }

    // 启动服务器输入处理线程(管理客户端、发送数据)
    if (pthread_create(&input_tid, NULL, server_input_handler, NULL) != 0) {
        perror("创建输入处理线程失败");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }

    printf("=== TCP OOB 多客户端服务器启动 ===
");
    printf("监听端口:%d
", PORT);
    printf("最大支持客户端数:%d
", MAX_CLIENTS);
    printf("服务器PID:%d(Ctrl+C优雅退出)
", getpid());

    // 主循环:持续接受新客户端连接(不退出)
    while (1) {
        // 接受新连接
        int new_conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len);
        if (new_conn_fd == -1) {
            if (errno == EINTR) continue;
            perror("接受连接失败");
            continue;
        }

        // 检查客户端数量是否达到上限
        pthread_mutex_lock(&client_mutex);
        int client_count = 0;
        Client *curr = client_list;
        while (curr != NULL) {
            client_count++;
            curr = curr->next;
        }
        if (client_count >= MAX_CLIENTS) {
            printf("[连接拒绝] 客户端数量已达上限(%d个),拒绝新连接:IP=%s:%d
",
                   MAX_CLIENTS, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
            close(new_conn_fd);
            pthread_mutex_unlock(&client_mutex);
            continue;
        }
        pthread_mutex_unlock(&client_mutex);

        // 创建新客户端节点,加入链表
        Client *new_client = (Client *)malloc(sizeof(Client));
        new_client->conn_fd = new_conn_fd;
        strcpy(new_client->ip, inet_ntoa(client_addr.sin_addr));
        new_client->port = ntohs(client_addr.sin_port);
        new_client->next = NULL;

        pthread_mutex_lock(&client_mutex);
        if (client_list == NULL) {
            client_list = new_client;  // 链表为空,作为头节点
        } else {
            curr = client_list;
            while (curr->next != NULL) curr = curr->next;
            curr->next = new_client;  // 加入链表尾部
        }
        printf("
[新连接] 客户端上线:IP=%s:%d(conn_fd=%d),当前在线数:%d
",
               new_client->ip, new_client->port, new_client->conn_fd, client_count + 1);
        pthread_mutex_unlock(&client_mutex);

        // 为新客户端创建处理线程
        if (pthread_create(&new_client->tid, NULL, client_handler, new_client) != 0) {
            perror("创建客户端处理线程失败");
            pthread_mutex_lock(&client_mutex);
            free(new_client);  // 释放节点
            pthread_mutex_unlock(&client_mutex);
            close(new_conn_fd);
            continue;
        }
    }

    // 理论上不会执行到这里
    close(listen_fd);
    pthread_mutex_destroy(&client_mutex);
    return 0;
}

客户端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8889
#define BUF_SIZE 1024

int sock_fd = -1;  // 与服务器的连接套接字
fd_set read_fds;   // select读集合
int max_fd;        // 最大文件描述符

// SIGURG信号处理函数:接收服务器OOB带外数据
void sigurg_handler(int sig) {
    if (sock_fd < 0) {
        fprintf(stderr, "
[SIGURG] 无有效连接,忽略
");
        return;
    }

    char oob_buf[BUF_SIZE];
    memset(oob_buf, 0, BUF_SIZE);

    ssize_t len = recv(sock_fd, oob_buf, BUF_SIZE - 1, MSG_OOB);
    if (len == -1) {
        if (errno == EINTR) return;
        perror("
[接收OOB失败]");
        return;
    }

    printf("
📢【紧急通知】收到服务器带外数据:%c(有效字节数:%ld)
", oob_buf[0], len);
    printf("请输入数据(普通输入/OOB格式:oob:X/quit退出):");
    fflush(stdout);
}

// 安全发送函数:确保数据完整发送
ssize_t safe_send(int fd, const char *buf, size_t len, int flags) {
    size_t total_sent = 0;
    while (total_sent < len) {
        ssize_t sent = send(fd, buf + total_sent, len - total_sent, flags);
        if (sent == -1) {
            if (errno == EINTR) continue;
            perror("[发送失败]");
            return -1;
        }
        total_sent += sent;
    }
    return total_sent;
}

// 初始化select:添加监听的文件描述符(标准输入+套接字)
void init_select() {
    FD_ZERO(&read_fds);
    FD_SET(STDIN_FILENO, &read_fds);
    FD_SET(sock_fd, &read_fds);
    max_fd = (sock_fd > STDIN_FILENO) ? sock_fd : STDIN_FILENO;
}

// 信号处理函数:Ctrl+C优雅退出
void sigint_handler(int sig) {
    printf("
收到Ctrl+C,客户端优雅退出...
");
    if (sock_fd > 0) {
        safe_send(sock_fd, "quit", strlen("quit"), 0);
        close(sock_fd);
    }
    exit(EXIT_SUCCESS);
}

int main() {
    struct sockaddr_in server_addr;
    char recv_buf[BUF_SIZE], send_buf[BUF_SIZE];
    struct timeval timeout = {3, 0};  // select超时3秒

    // 注册信号处理函数
    signal(SIGINT, sigint_handler);
    signal(SIGPIPE, SIG_IGN);  // 忽略SIGPIPE(服务器断开后send不崩溃)

    // 创建套接字
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket创建失败");
        exit(EXIT_FAILURE);
    }

    // 配置服务器地址并连接
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("服务器IP无效");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    server_addr.sin_port = htons(SERVER_PORT);

    if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("连接服务器失败");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    printf("=== 已连接服务器:%s:%d ===
", SERVER_IP, SERVER_PORT);

    // 配置SIGURG信号(接收OOB数据)
    signal(SIGURG, sigurg_handler);
    fcntl(sock_fd, F_SETOWN, getpid());  // 设置套接字所有者

    // 初始化select,开始双向通信
    init_select();
    printf("
=== 双向通信已就绪 ===
");
    printf("输入说明:
");
    printf("  直接输入内容 → 发送普通数据
");
    printf("  输入格式 'oob:X'(X为单个字符)→ 发送带外数据(如oob:!)
");
    printf("  输入 'quit' → 退出程序
");
    printf("===========================
");
    printf("请输入数据:");
    fflush(stdout);

    while (1) {
        fd_set tmp_fds = read_fds;
        int ret = select(max_fd + 1, &tmp_fds, NULL, NULL, &timeout);
        if (ret == -1) {
            if (errno == EINTR) continue;
            perror("select失败");
            break;
        } else if (ret == 0) {
            init_select();
            continue;
        }

        // ------------- 处理服务器发送的数据(套接字可读)-------------
        if (FD_ISSET(sock_fd, &tmp_fds)) {
            memset(recv_buf, 0, BUF_SIZE);
            ssize_t len = recv(sock_fd, recv_buf, BUF_SIZE - 1, 0);
            if (len == -1) {
                if (errno == EINTR) continue;
                if (errno == ECONNRESET || errno == EBADF) {
                    printf("
❌ 服务器主动断开连接
");
                    break;
                }
                perror("[接收普通数据失败]");
                break;
            } else if (len == 0) {
                printf("
❌ 服务器正常关闭连接
");
                break;
            }

            printf("
[接收服务器普通数据]:%s(字节数:%ld)
", recv_buf, len);
            if (strcmp(recv_buf, "quit") == 0) {
                printf("
❌ 收到服务器退出指令,客户端准备关闭
");
                break;
            }
            printf("请输入数据:");
            fflush(stdout);
        }

        // ------------- 处理用户输入(标准输入可读)-------------
        if (FD_ISSET(STDIN_FILENO, &tmp_fds)) {
            memset(send_buf, 0, BUF_SIZE);
            if (fgets(send_buf, BUF_SIZE, stdin) == NULL) {
                perror("[读取输入失败]");
                break;
            }
            send_buf[strcspn(send_buf, "
")] = '';

            // 处理空输入
            if (strlen(send_buf) == 0) {
                printf("提示:输入不能为空,请重新输入:");
                fflush(stdout);
                continue;
            }

            // 处理退出指令
            if (strcmp(send_buf, "quit") == 0) {
                safe_send(sock_fd, send_buf, strlen(send_buf), 0);
                printf("
✅ 已发送退出指令,客户端准备关闭...
");
                break;
            }

            // 处理带外数据
            if (strncmp(send_buf, "oob:", 4) == 0) {
                if (strlen(send_buf) != 5) {
                    printf("错误:OOB格式无效!正确格式:oob:X(X为单个字符)
");
                    printf("请输入数据:");
                    fflush(stdout);
                    continue;
                }
                char oob_char = send_buf[4];
                if (safe_send(sock_fd, &oob_char, 1, MSG_OOB) != -1) {
                    printf("✅ 已发送带外数据:%c(仅1字节有效)
", oob_char);
                }
                printf("请输入数据:");
                fflush(stdout);
                continue;
            }

            // 发送普通数据
            if (safe_send(sock_fd, send_buf, strlen(send_buf), 0) != -1) {
                printf("✅ 已发送普通数据:%s(字节数:%zu)
", send_buf, strlen(send_buf));
            }
            printf("请输入数据:");
            fflush(stdout);
        }

        // 重新初始化select
        init_select();
    }

    // 释放资源
    close(sock_fd);
    printf("客户端已退出,所有资源已释放!
");
    return 0;
}

本文地址:https://www.yitenyun.com/1860.html

搜索文章

Tags

#ios面试 #ios弱网 #断点续传 #ios开发 #objective-c #ios #ios缓存 #服务器 #python #pip #conda #远程工作 #kubernetes #笔记 #平面 #容器 #linux #学习方法 香港站群服务器 多IP服务器 香港站群 站群服务器 #Trae #IDE #AI 原生集成开发环境 #Trae AI #运维 #分阶段策略 #模型协议 #人工智能 #科技 #深度学习 #自然语言处理 #神经网络 #低代码 #爬虫 #音视频 #docker #github #git #华为云 #部署上线 #动静分离 #Nginx #新人首发 #物联网 #websocket #开发语言 #云原生 #iventoy #VmWare #OpenEuler #harmonyos #鸿蒙PC #内网穿透 #网络 #cpolar #开源 #学习 #后端 #数据库 #Conda # 私有索引 # 包管理 #kylin #进程控制 #数信院生信服务器 #Rstudio #生信入门 #生信云服务器 #银河麒麟高级服务器操作系统安装 #银河麒麟高级服务器V11配置 #设置基础软件仓库时出错 #银河麒高级服务器系统的实操教程 #生产级部署银河麒麟服务系统教程 #Linux系统的快速上手教程 #vscode #mobaxterm #计算机视觉 #缓存 #ubuntu #fastapi #html #css #MobaXterm #Dell #PowerEdge620 #内存 #硬盘 #RAID5 #FTP服务器 #node.js #tcp/ip #qt #C++ #unity #c# #游戏引擎 #分布式 #华为 #hadoop #hbase #hive #zookeeper #spark #kafka #flink #算法 #大数据 #mcu #golang #java #redis #flask #多个客户端访问 #IO多路复用 #回显服务器 #TCP相关API #sql #AIGC #langchain #agi #安全 #nginx #RTP over RTSP #RTP over TCP #RTSP服务器 #RTP #TCP发送RTP #vllm #大模型 #Streamlit #Qwen #本地部署 #AI聊天机器人 #pytorch #我的世界 #android #腾讯云 #web安全 #uni-app #小程序 #notepad++ #Harbor #centos #ssh #ModelEngine #ide #vue.js #jvm #微信 #Ubuntu服务器 #硬盘扩容 #命令行操作 #VMware #儿童书籍 #儿童诗歌 #童话故事 #经典好书 #儿童文学 #好书推荐 #经典文学作品 #udp #c++ #Ascend #MindIE #ARM服务器 # GLM-4.6V # 多模态推理 #需求分析 #架构 #ping通服务器 #读不了内网数据库 #bug菌问答团队 #经验分享 #安卓 #epoll #高级IO #计算机网络 #面试 #MCP #MCP服务器 #性能优化 #课程设计 #LoRA # RTX 3090 # lora-scripts #http #fiddler #asp.net #1024程序员节 #php #前端 #gemini #gemini国内访问 #gemini api #gemini中转搭建 #Cloudflare #项目 #高并发 #c语言 #stm32 #jenkins #银河麒麟 #系统升级 #信创 #国产化 #prometheus #AI编程 #mvp #个人开发 #设计模式 #gpu算力 #编辑器 #研发管理 #禅道 #禅道云端部署 #svn #游戏 #MC #RAID #RAID技术 #磁盘 #存储 #数据结构 #链表 #链表的销毁 #链表的排序 #链表倒置 #判断链表是否有环 #elasticsearch #Android #Bluedroid #云计算 #凤希AI伴侣 #网络安全 #VS Code调试配置 #AI #大模型学习 #AI论文写作工具 #学术论文创作 #论文效率提升 #MBA论文写作 #SSH反向隧道 # Miniconda # Jupyter远程访问 #json #jmeter #功能测试 #软件测试 #自动化测试 #职场和发展 #电脑 #自动化 #grafana #spring boot #asp.net大文件上传 #asp.net大文件上传下载 #asp.net大文件上传源码 #ASP.NET断点续传 #asp.net上传文件夹 #jar #sqlserver #密码学 #flutter #数码相机 #SSH #X11转发 #Miniconda #apache #ansible #时序数据库 #鸭科夫 #逃离鸭科夫 #鸭科夫联机 #鸭科夫异地联机 #开服 #debian #改行学it #创业创新 #程序员创富 #ms-swift # 一锤定音 # 大模型微调 #deepseek #cpp #nas #企业开发 #ERP #项目实践 #.NET开发 #C#编程 #编程与数学 #claude #screen 命令 #arm开发 #macos #windows #mysql #ssl #iphone #阿里云 #远程桌面 #远程控制 #模版 #函数 #类 #笔试 #WEB #bash #DisM++ # 系统维护 #金融 #mcp #金融投资Agent #Agent #京东云 #YOLO #目标检测 #lua #语音识别 #测试工具 #压力测试 #laravel #ida #diskinfo # TensorFlow # 磁盘健康 #流量监控 #中间件 #shell #CPU利用率 #流媒体 #NAS #飞牛NAS #监控 #NVR #EasyNVR #版本控制 #Git入门 #开发工具 #代码托管 #深度优先 #DFS #毕设 #STUN # TURN # NAT穿透 #制造 #Ansible # 自动化部署 # VibeThinker #个人博客 #n8n #进程 #操作系统 #进程创建与终止 #嵌入式编译 #ccache #distcc #unity3d #服务器框架 #Fantasy #ollama #ai #llm #web #webdav #RustDesk #IndexTTS 2.0 #本地化部署 #智能路由器 #毕业设计 #车辆排放 #散列表 #哈希算法 #leetcode #pycharm #Spring AI #STDIO协议 #Streamable-HTTP #McpTool注解 #服务器能力 #javascript #react.js #程序人生 #蓝桥杯 #django #生信 #microsoft #LLM #oracle #SA-PEKS # 关键词猜测攻击 # 盲签名 # 限速机制 #pencil #pencil.dev #设计 #树莓派4b安装系统 #智能手机 #journalctl #我的世界服务器搭建 #minecraft #openresty #LobeChat #vLLM #GPU加速 #数据仓库 #RAG #全链路优化 #实战教程 #everything #Playbook #AI服务器 #openlayers #bmap #tile #server #vue #机器学习 #推荐算法 #网络协议 #openEuler #Hadoop #log #SSH Agent Forwarding # PyTorch # 容器化 #spring #信息可视化 #claude code #codex #code cli #ccusage #vuejs #信令服务器 #Janus #MediaSoup #eBPF #单片机 #todesk #建筑缺陷 #红外 #数据集 #wsl #微服务 #微信小程序 #健身房预约系统 #健身房管理系统 #健身管理系统 #p2p #Windows #gitea #jupyter #网站 #截图工具 #批量处理图片 #图片格式转换 #图片裁剪 #结构体 #北京百思可瑞教育 #百思可瑞教育 #北京百思教育 #Android16 #音频性能实战 #音频进阶 #agent #ai大模型 #SMTP # 内容安全 # Qwen3Guard #SSE # AI翻译机 # 实时翻译 #risc-v #嵌入式硬件 #Host #渗透测试 #SSRF #adb #UOS #海光K100 #统信 #SSH公钥认证 # 安全加固 #r-tree #聊天小程序 #Fun-ASR # 语音识别 # WebUI #PyTorch #CUDA #Triton #无人机 #Deepoc #具身模型 #开发板 #未来 #部署 #tdengine #涛思数据 #语言模型 #DeepSeek #昇腾300I DUO #es安装 #NFC #智能公交 #服务器计费 #数据挖掘 #FP-增长 #ddos #ui #分类 #dify #内存治理 #opencv #Qwen3-14B # 大模型部署 # 私有化AI #交互 #c++20 #搜索引擎 #Proxmox VE #虚拟化 #文心一言 #AI智能体 #C# # REST API # GLM-4.6V-Flash-WEB #transformer #chatgpt #运维开发 #GPU服务器 #8U #硬件架构 #opc ua #opc #NPU #CANN #fpga开发 #LVDS #高速ADC #DDR #东方仙盟 # GLM-TTS # 数据安全 #视频去字幕 #API限流 # 频率限制 # 令牌桶算法 #iBMC #UltraISO #黑群晖 #虚拟机 #无U盘 #纯小白 #支付 #cosmic #aws #H5 #跨域 #发布上线后跨域报错 #请求接口跨域问题解决 #跨域请求代理配置 #request浏览器跨域 #蓝湖 #Axure原型发布 #处理器 #esp32教程 #azure #游戏机 #llama #ceph #JumpServer #堡垒机 #银河麒麟操作系统 #openssh #华为交换机 #信创终端 #OPCUA #ambari #arm #单元测试 #集成测试 #muduo库 #uv #uvx #uv pip #npx #Ruff #pytest #振镜 #振镜焊接 #teamviewer #青少年编程 #蓝耘智算 #910B #昇腾 #rocketmq #Linux #TCP #Socket网络编程 #算力一体机 #ai算力服务器 # Triton # 目标检测 #科研 #博士 #Anaconda配置云虚拟环境 #MQTT协议 #milvus #springboot #知识库 #https #web server #请求处理流程 #ONLYOFFICE #MCP 服务器 #SRS #直播 #tomcat #集成学习 # 双因素认证 #服务器繁忙 #前端框架 #reactjs #web3 #chrome #rustdesk #政务 #postgresql #连接数据库报错 #测试用例 #selenium #守护进程 #复用 #screen #cursor #系统架构 #powerbi #Clawdbot #个人助理 #数字员工 #1panel #vmware #YOLOFuse # Base64编码 # 多模态检测 #企业微信 #SPA #单页应用 #web3.py #visual studio code #源码 #闲置物品交易系统 #umeditor粘贴word #ueditor粘贴word #ueditor复制word #ueditor上传word图片 #IPv6 #DNS #麒麟OS #LangGraph #模型上下文协议 #MultiServerMCPC #load_mcp_tools #load_mcp_prompt #swagger #embedding #硬件工程 #ci/cd #gitlab #mariadb #prompt #YOLOv8 # Docker镜像 #CLI #Python #JavaScript #langgraph.json #jetty #MS #Materials #java-ee #mamba #java大文件上传 #java大文件秒传 #java大文件上传下载 #java文件传输解决方案 #计算机 #CMake #Make #C/C++ #sqlite #PyCharm # 远程调试 # YOLOFuse #扩展屏应用开发 #android runtime # 高并发部署 #wordpress #雨云 # CUDA #vps #电气工程 #PLC #翻译 #开源工具 #libosinfo #tensorflow #webpack #ComfyUI # 推理服务器 #学术写作辅助 #论文创作效率提升 #AI写论文实测 #负载均衡 #maven #intellij-idea #客户端 #嵌入式 #DIY机器人工房 #nacos #银河麒麟aarch64 #windows11 #系统修复 #.net #性能 #优化 #RAM #SSH免密登录 #其他 #select #上下文工程 #langgraph #意图识别 #说话人验证 #声纹识别 #CAM++ #万悟 #联通元景 #智能体 #镜像 #scala #webrtc #idm #PTP_1588 #gPTP #rust #web服务器 #信创国产化 #达梦数据库 # 公钥认证 #Reactor # GPU租赁 # 自建服务器 #Nacos #gRPC #注册中心 #win11 #c #gateway #Comate #遛狗 #GPU #AutoDL ##租显卡 #进程等待 #wait #waitpid #MinIO服务器启动与配置详解 #pdf #clickhouse #程序员 #大模型教程 #AI大模型 #代理 #SSH复用 # 远程开发 #实时音视频 #业界资讯 #UDP套接字编程 #UDP协议 #网络测试 #DHCP #scrapy #勒索病毒 #勒索软件 #加密算法 #.bixi勒索病毒 #数据加密 #eclipse #spring cloud #servlet #arm64 #nvidia #VMWare Tool #wpf #串口服务器 #Modbus #MOXA #GATT服务器 #蓝牙低功耗 #5G #平板 #零售 #交通物流 #智能硬件 #H5网页 #网页白屏 #H5页面空白 #资源加载问题 #打包部署后网页打不开 #HBuilderX #CTF #插件 #开源软件 #硬件 #心理健康服务平台 #心理健康系统 #心理服务平台 #心理健康小程序 #firefox #safari #hibernate #memory mcp #Cursor #PowerBI #企业 #信号处理 #模型训练 #星图GPU #googlecloud #数据分析 #vnstat # 远程连接 #论文笔记 #idea #intellij idea #vp9 # keep-alive #攻防演练 #Java web #漏洞 #红队 #机器人 #驱动开发 #飞牛nas #fnos #指针 #GB28181 #SIP信令 #SpringBoot #视频监控 #WT-2026-0001 #QVD-2026-4572 #smartermail #restful #ajax #Llama-Factory # 树莓派 # ARM架构 #系统管理 #服务 #memcache #Modbus-TCP #大剑师 #nodejs面试题 #screen命令 #C2000 #TI #实时控制MCU #AI服务器电源 #源代码管理 #管道Pipe #system V #ai编程 #ranger #MySQL8.0 #UDP的API使用 # 高并发 #serverless #数据恢复 #视频恢复 #视频修复 #RAID5恢复 #流媒体服务器恢复 #智能体来了 #智能体对传统行业冲击 #行业转型 #AI赋能 #SAP #ebs #metaerp #oracle ebs #chat #国产化OS #SSH跳转 #容器化 #cesium #可视化 #elk # 批量管理 #RSO #机器人操作系统 #glibc #YOLO26 #muduo #TcpServer #accept #高并发服务器 #C语言 #远程开发 #vivado license #CVE-2025-68143 #CVE-2025-68144 #CVE-2025-68145 #html5 #计算几何 #斜率 #方向归一化 #叉积 #samba #fabric #可信计算技术 #winscp #postman #openHiTLS #TLCP #DTLCP #商用密码算法 #excel #Aluminium #Google #copilot #CPU #测评 #CCE #Dify-LLM #Flexus #微PE #硬盘克隆 #DiskGenius #媒体 #AI技术 #ArkUI #ArkTS #鸿蒙开发 #手机h5网页浏览器 #安卓app #苹果ios APP #手机电脑开启摄像头并排查 #IO #智能家居 #go #mybatis #spine #宝塔面板部署RustDesk #RustDesk远程控制手机 #手机远程控制 #C #bootstrap #移动端h5网页 #调用浏览器摄像头并拍照 #开启摄像头权限 #拍照后查看与上传服务器端 #摄像头黑屏打不开问题 #puppeteer #KMS #slmgr #kmeans #聚类 #文件IO #输入输出流 #智慧校园解决方案 #智慧校园一体化平台 #智慧校园选型 #智慧校园采购 #智慧校园软件 #智慧校园专项资金 #智慧校园定制开发 #POC #问答 #交付 #xlwings #Excel #信息与通信 #tcpdump # 大模型 # 模型训练 #VMware Workstation16 #服务器操作系统 #nfs #iscsi #Java #ShaderGraph #图形 #Smokeping #策略模式 #pve #文件管理 #文件服务器 #paddleocr #企业级存储 #网络设备 #大模型应用 #API调用 #PyInstaller打包运行 #服务端部署 #raid #raid阵列 #大语言模型 #能源 #markdown #建站 #zotero #WebDAV #同步失败 #代理模式 #Anything-LLM #IDC服务器 #私有化部署 #工具集 #scanf #printf #getchar #putchar #cin #cout #ue5 #欧拉 #儿童AI #图像生成 #排序算法 #jdk #排序 #麒麟 # 水冷服务器 # 风冷服务器 # IndexTTS 2.0 # 自动化运维 #VoxCPM-1.5-TTS # 云端GPU # PyCharm宕机 #数学建模 #2026年美赛C题代码 #2026年美赛 #rdp #软件 #本地生活 #电商系统 #商城 #海外服务器安装宝塔面板 #aiohttp #asyncio #异步 #SSH保活 #n8n解惑 #大模型开发 #rabbitmq #.netcore #HistoryServer #Spark #YARN #jobhistory # 模型微调 #大模型部署 #mindie #大模型推理 #WinDbg #Windows调试 #内存转储分析 #模拟退火算法 #简单数论 #埃氏筛法 #系统安全 #AB包 #net core #kestrel #web-server #asp.net-core #重构 #yum #uvicorn #uvloop #asgi #event #Zabbix #CosyVoice3 #语音合成 #homelab #Lattepanda #Jellyfin #Plex #Emby #Kodi #yolov12 #研究生life #浏览器自动化 #python #eureka #Go并发 #高并发架构 #Goroutine #系统设计 #mongodb #Dify #ARM架构 #鲲鹏 #rtsp #转发 #TensorRT # 推理优化 #EMC存储 #存储维护 #NetApp存储 #zabbix #三维 #3D #三维重建 #IntelliJ IDEA #Spring Boot #neo4j #NoSQL #SQL # 大模型推理 #智能一卡通 #门禁一卡通 #梯控一卡通 #电梯一卡通 #消费一卡通 #一卡通 #考勤一卡通 #统信UOS #win10 #qemu #Jetty # CosyVoice3 # 嵌入式服务器 #CVE-2025-61686 #路径遍历高危漏洞 #视觉检测 #visual studio #vim #gcc #ESP32 #传感器 #MicroPython # 代理转发 # 跳板机 #RK3576 #瑞芯微 #硬件设计 #echarts #Termux #Samba #HeyGem # 服务器IP # 端口7860 #k8s #戴尔服务器 #戴尔730 #装系统 #junit #iot #ThingsBoard MCP # ARM服务器 # 鲲鹏 #LangFlow # 智能运维 # 性能瓶颈分析 #空间计算 #原型模式 #VibeVoice # 语音合成 # 云服务器 #uip #devops #数据访问 # 服务器IP访问 # 端口映射 #bug #树莓派 #温湿度监控 #WhatsApp通知 #IoT #MySQL #智慧城市 #跳槽 #黑客技术 #文件上传漏洞 #word #Kylin-Server #国产操作系统 #服务器安装 #自动化运维 #nosql #CA证书 #数据安全 #注入漏洞 #区块链 #生活 #vncdotool #链接VNC服务器 #如何隐藏光标 #机器视觉 #6D位姿 #A2A #GenAI # ControlMaster #FHSS #Keycloak #Quarkus #AI编程需求分析 #b树 #服务器解析漏洞 #nodejs #outlook #错误代码2603 #无网络连接 #2603 #算力建设 #windbg分析蓝屏教程 #le audio #蓝牙 #低功耗音频 #通信 #连接 #esb接口 #走处理类报异常 #ffmpeg #Ubuntu #ESP32编译服务器 #Ping #DNS域名解析 #SSH密钥 #练习 #基础练习 #数组 #循环 #九九乘法表 #计算机实现 #dynadot #域名 #ETL管道 #向量存储 #数据预处理 #DocumentReader #Buck #NVIDIA #算力 #交错并联 #DGX #银河麒麟部署 #银河麒麟部署文档 #银河麒麟linux #银河麒麟linux部署教程 #安全架构 #网路编程 #百万并发 #smtp #smtp服务器 #PHP #TTS私有化 # IndexTTS # 音色克隆 #设备驱动 #芯片资料 #网卡 #anaconda #虚拟环境 #SSH跳板机 # Python3.11 #SFTP #Gunicorn #WSGI #Flask #并发模型 #性能调优 #视频 #AI 推理 #NV #ip #ServBay #数模美赛 #matlab #ecmascript #elementui #门禁 #梯控 #智能梯控 # OTA升级 # 黄山派 #超时设置 #客户端/服务器 #网络编程 #挖矿 #Linux病毒 #ansys #ansys问题解决办法 #网安应急响应 # 网络延迟 # GLM # 服务连通性 #duckdb #全能视频处理软件 #视频裁剪工具 #视频合并工具 #视频压缩工具 #视频字幕提取 #视频处理工具 # Connection refused #ipv6 #框架搭建 #状态模式 #AI-native #dba #Tokio #雨云服务器 #Minecraft服务器 #教程 #MCSM面板 #react native # GPU集群 #Gateway #认证服务器集成详解 #sql注入 #ASR #SenseVoice #coffeescript #鸿蒙 #weston #x11 #x11显示服务器 # 服务器配置 # GPU #证书 #后端框架 #远程连接 # 数字人系统 # 远程部署 #工程设计 #预混 #扩散 #燃烧知识 #层流 #湍流 #WinSCP 下载安装教程 #FTP工具 #服务器文件传输 # 批量部署 # TTS服务器 # 键鼠锁定 #sentinel #服务器线程 # SSL通信 # 动态结构体 #node #语音生成 #TTS #Docker #AI写作 #LE Audio #BAP #网络攻击模型 #pyqt #r语言 #参数估计 #矩估计 #概率论 #lvs #运维工具 #ipmitool #BMC # 黑屏模式 #领域驱动 #麦克风权限 #访问麦克风并录制音频 #麦克风录制音频后在线播放 #用户拒绝访问麦克风权限怎么办 #uniapp 安卓 苹果ios #将音频保存本地或上传服务器 #STDIO传输 #SSE传输 #WebMVC #WebFlux #Node.js # child_process #服务器IO模型 #非阻塞轮询模型 #多任务并发模型 #异步信号模型 #多路复用模型 #IndexTTS2 # 阿里云安骑士 # 木马查杀 #dlms #dlms协议 #逻辑设备 #逻辑设置间权限 #入侵 #日志排查 #scikit-learn #随机森林 #typescript #npm #安全威胁分析 #VPS #云服务器 #搭建 #仙盟创梦IDE #GLM-4.6V-Flash-WEB # AI视觉 # 本地部署 #动态规划 #Spring AOP #3d #Minecraft #PaperMC #我的世界服务器 #前端开发 #人大金仓 #Kingbase #租显卡 #训练推理 # 远程访问 # 服务器IP配置 #多进程 #python技巧 #kong #Kong Audio #Kong Audio3 #KongAudio3 #空音3 #空音 #中国民乐 #小艺 #搜索 #游戏美术 #技术美术 #游戏策划 #游戏程序 #用户体验 #TLS协议 #HTTPS #漏洞修复 #运维安全 #Langchain-Chatchat # 国产化服务器 # 信创 #多模态 #微调 #超参 #LLamafactory #numpy #产品经理 #就业 #pjsip #Java程序员 #Java面试 #后端开发 #Spring源码 #Spring #bond #服务器链路聚合 #网卡绑定 # 远程运维 #Autodl私有云 #深度服务器配置 #V11 #kylinos #KMS激活 #程序开发 #程序设计 #计算机毕业设计 #大作业 #database #人脸识别sdk #视频编解码 #人脸识别 #AI生成 # outputs目录 # 自动化 #CSDN #性能测试 #LoadRunner #论文阅读 #软件工程 #智能制造 #供应链管理 #工业工程 #库存管理 #esp32 arduino #ZooKeeper #ZooKeeper面试题 #面试宝典 #深入解析 #blender #warp # 显卡驱动备份 #数字化转型 #实体经济 #商业模式 #软件开发 #数智红包 #商业变革 #创业干货 #广播 #组播 #并发服务器 #x86_64 #数字人系统 #AI视频创作系统 #AI视频创作 #AI创作系统 #AI视频生成 #AI工具 #文生视频 #AI创作工具 #FASTMCP #大模型入门 #计组 #数电 #文件传输 #电脑文件传输 #电脑传输文件 #电脑怎么传输文件到另一台电脑 #电脑传输文件到另一台电脑 #Tracker 服务器 #响应最快 #torrent 下载 #2026年 #Aria2 可用 #迅雷可用 #BT工具通用 #Puppet # IndexTTS2 # TTS #联机教程 #局域网联机 #局域网联机教程 #局域网游戏 #Xshell #Finalshell #生物信息学 #组学 #交换机 #三层交换机 #企业存储 #RustFS #对象存储 #高可用 #高斯溅射 #gpu #nvcc #cuda #产品运营 #SQL注入主机 #个人电脑 #log4j #MC群组服务器 #RXT4090显卡 #RTX4090 #深度学习服务器 #硬件选型 #CS2 #debian13 #数据采集 #浏览器指纹 #asp.net上传大文件 #WRF #WRFDA #unix #编程 #c++高并发 #edge #迭代器模式 #观察者模式 #机器人学习 # IP配置 # 0.0.0.0 # 权限修复 #turn #ICE #群晖 #http头信息 #银河麒麟服务器系统 #视觉理解 #Moondream2 #多模态AI #I/O模型 #并发 #水平触发、边缘触发 #多路复用 # HiChatBox # 离线AI #TCP服务器 #开发实战 #全文检索 #可撤销IBE #服务器辅助 #私钥更新 #安全性证明 #双线性Diffie-Hellman #磁盘配额 #存储管理 #形考作业 #国家开放大学 #系统运维 #短剧 #短剧小程序 #短剧系统 #微剧 #C++ UA Server #SDK #跨平台开发 #汽车 #mssql #知识 #lucene #webgl #wireshark #网络安全大赛 #protobuf #密码 #CNAS #CMA #程序文件 #音乐分类 #音频分析 #ViT模型 #Gradio应用 #鼠大侠网络验证系统源码 #实时检测 #卷积神经网络 #AITechLab #cpp-python #CUDA版本 #DAG #nmodbus4类库使用教程 #docker-compose #目标跟踪 #云服务器选购 #Saas #线程 #测速 #iperf #iperf3 #HarmonyOS APP #ARM64 # DDColor # ComfyUI #IFix #具身智能 #LabVIEW知识 #LabVIEW程序 #LabVIEW功能 #labview #小智 #声源定位 #MUSIC #matplotlib #AI电商客服 #期刊 #SCI #面向对象 #基础语法 #标识符 #常量与变量 #数据类型 #运算符与表达式 #地理 #遥感 #spring ai #oauth2 #Fluentd #Sonic #日志采集 #gerrit #数据可视化 #rtmp #taro # 环境迁移 # 高温监控 #xshell #host key #Linly-Talker # 数字人 # 服务器稳定性 #fs7TF #主板 #总体设计 #电源树 #框图 #ROS # 局域网访问 # 批量处理 #传统行业 #npu #编程助手 #内网 #远程软件 #电子电气架构 #系统工程与系统架构的内涵 #自动驾驶 #Routine #OSS #代理服务器 #rsync # 数据同步 #设计师 #图像处理 #分布式数据库 #集中式数据库 #业务需求 #选型误 #HarmonyOS #华为od #华为机试 #Apple AI #Apple 人工智能 #FoundationModel #Summarize #SwiftUI #多线程 #claudeCode #content7 #工作 #服务器开启 TLS v1.2 #IISCrypto 使用教程 #TLS 协议配置 #IIS 安全设置 #服务器运维工具 #uniapp #合法域名校验出错 #服务器域名配置不生效 #request域名配置 #已经配置好了但还是报错 #uniapp微信小程序 #odoo # 串口服务器 # NPort5630 #appche #mtgsig #美团医药 #美团医药mtgsig #美团医药mtgsig1.2 #SMP(软件制作平台) #EOM(企业经营模型) #应用系统 #Socket #套接字 #I/O多路复用 #字节序 #寄存器 #Python办公自动化 #Python办公 #项目申报系统 #项目申报管理 #项目申报 #企业项目申报 #ftp #sftp #H3C #YOLO识别 #YOLO环境搭建Windows #YOLO环境搭建Ubuntu # 轻量化镜像 # 边缘计算 #OpenHarmony #cpu #MCP服务器注解 #异步支持 #方法筛选 #声明式编程 #自动筛选机制 #量子计算 #JNI #pxe #opc模拟服务器 #AI部署 # ms-swift #free #vmstat #sar #PN 结 #Shiro #反序列化漏洞 #CVE-2016-4437 #RWK35xx #语音流 #实时传输 #超算中心 #PBS #lsf #反向代理 #报表制作 #职场 #用数据讲故事 #MinIO #tcp/ip #网络 #Discord机器人 #云部署 #程序那些事 #adobe #TRO #TRO侵权 #TRO和解 #数据迁移 #系统安装 #铁路桥梁 #DIC技术 #箱梁试验 #裂纹监测 #四点弯曲 #边缘计算 #可再生能源 #绿色算力 #风电 #express #cherry studio #高仿永硕E盘的个人网盘系统源码 #gmssh #宝塔 #漏洞挖掘 #Exchange #汇编 #AI应用编程 #若依 #xss #工业级串口服务器 #串口转以太网 #串口设备联网通讯模块 #串口服务器选型 #支持向量机 #自由表达演说平台 #演说 #音诺ai翻译机 #AI翻译机 # Ampere Altra Max #AI Agent #开发者工具 #考研 #WAN2.2 #EN4FE #VSCode # SSH #Karalon #AI Test #国产PLM #瑞华丽PLM #瑞华丽 #PLM #流程图 #图论 #国产开源制品管理工具 #Hadess #一文上手 #okhttp #范式 #计算机外设 #阻塞队列 #生产者消费者模型 #服务器崩坏原因 #xml #ET模式 #非阻塞 #统信操作系统 #remote-ssh #bigtop #hdp #hue #kerberos #健康医疗 #轻量化 #低配服务器 #docker安装seata #AI应用 #图像识别 #大学生 #高考 #电梯 #电梯运力 #电梯门禁 #工程实践 #gpt #API #idc #wps #Linux多线程 #Beidou #北斗 #SSR #Syslog #系统日志 #日志分析 #日志监控 #生产服务器问题查询 #日志过滤 #数据报系统 #信息安全 #信息收集 #poll #vue上传解决方案 #vue断点续传 #vue分片上传下载 #vue分块上传下载 #stl #IIS Crypto #simulink #海外短剧 #海外短剧app开发 #海外短剧系统开发 #短剧APP #短剧APP开发 #短剧系统开发 #海外短剧项目 #决策树 #RK3588 #RK3588J #评估板 #核心板 #嵌入式开发 #数字孪生 #三维可视化 #sglang # Qwen3Guard-Gen-8B #VMware创建虚拟机 #远程更新 #缓存更新 #多指令适配 #物料关联计划 #晶振 #挖漏洞 #攻击溯源 #内存接口 # 澜起科技 # 服务器主板 #程序定制 #毕设代做 #课设 # AI部署 #材料工程 #智能电视 #m3u8 #HLS #移动端H5网页 #APP安卓苹果ios #监控画面 直播视频流 #Prometheus # 服务器迁移 # 回滚方案 #DooTask #华为od机试 #华为od机考 #华为od最新上机考试题库 #华为OD题库 #华为OD机试双机位C卷 #od机考题库 #开关电源 #热敏电阻 #PTC热敏电阻 #防毒面罩 #防尘面罩 #运维 #UEFI #BIOS #Legacy BIOS #音乐 #clawdbot #Coturn #TURN #身体实验室 #健康认知重构 #系统思维 #微行动 #NEAT效应 #亚健康自救 #ICT人 #云开发 #KMS 激活 #AI智能棋盘 #Rock Pi S #模块 #Matrox MIL #二次开发 #BoringSSL #SMARC #ARM #云计算运维 #vertx #vert.x #vertx4 #runOnContext #SSH别名 #网络配置实战 #Web/FTP 服务访问 #计算机网络实验 #外网访问内网服务器 #Cisco 路由器配置 #静态端口映射 #网络运维 #自动化巡检 #0day漏洞 #DDoS攻击 #漏洞排查 #Rust #异步编程 #系统编程 #Pin #http服务器 #路由器 #galeweather.cn #高精度天气预报数据 #光伏功率预测 #风电功率预测 #高精度气象 #CS336 #Assignment #Experiments #TinyStories #Ablation #星际航行 #论文复现 #ossinsight #娱乐 #敏捷流程 #AE #rag #AI赋能盾构隧道巡检 #开启基建安全新篇章 #以注意力为核心 #YOLOv12 #AI隧道盾构场景 #盾构管壁缺陷病害异常检测预警 #隧道病害缺陷检测 #jquery #canvas层级太高 #canvas遮挡问题 #盖住其他元素 #苹果ios手机 #安卓手机 #调整画布层级 #学术生涯规划 #CCF目录 #基金申请 #职称评定 #论文发表 #科研评价 #顶会顶刊 #节日 #fork函数 #进程创建 #进程终止 #分子动力学 #化工仿真 #游戏服务器断线 #session #clamav #外卖配送 #JADX-AI 插件 #Archcraft #命令模式 #语义检索 #向量嵌入 #boltbot #starrocks #人脸活体检测 #live-pusher #动作引导 #张嘴眨眼摇头 #苹果ios安卓完美兼容 #环境搭建 #百度 #ueditor导入word #L6 #L10 #L9 #阿里云RDS # 硬件配置 #composer #symfony #java-zookeeper #软件需求 #dubbo #个性化推荐 #BERT模型 #因果学习 #Qwen3-VL # 服务状态监控 # 视觉语言模型 #新浪微博 #传媒 #隐函数 #常微分方程 #偏微分方程 #线性微分方程 #线性方程组 #非线性方程组 #复变函数 #DuckDB #协议 #React安全 #漏洞分析 #Next.js #思爱普 #SAP S/4HANA #ABAP #NetWeaver #静脉曲张 #腿部健康 #运动 #土地承包延包 #领码SPARK #aPaaS+iPaaS #智能审核 #档案数字化 #农产品物流管理 #物流管理系统 #农产品物流系统 #农产品物流 #Ward #4U8卡 AI 服务器 ##AI 服务器选型指南 #GPU 互联 #GPU算力 #日志模块 #边缘AI # Kontron # SMARC-sAMX8 #Arduino BLDC #核辐射区域探测机器人 #DDD #tdd #OpenAI #故障 #风控模型 #决策盲区 #esp32 #mosquito # GPU服务器 # tmux #NSP #下一状态预测 #aigc #效率神器 #办公技巧 #自动化工具 #Windows技巧 #打工人必备 #cascadeur #二值化 #Canny边缘检测 #轮廓检测 #透视变换 #AI+ #coze #AI入门 #resnet50 #分类识别训练 #Python3.11 #Spire.Office #隐私合规 #网络安全保险 #法律风险 #风险管理 #AI工具集成 #容器化部署 #分布式架构 #2025年 #FRP #AI教程 #pandas