最新资讯

  • Socket编程入门:IO多路复用方式实现高并发服务器

Socket编程入门:IO多路复用方式实现高并发服务器

2026-01-29 08:26:30 栏目:最新资讯 3 阅读

Socket编程入门:IO多路复用方式实现高并发服务器

        阅读本文前需要Socket编程基础和理解Socket文件描述符的本质,可以先阅读Socket编程入门。

目录

Socket编程入门:IO多路复用方式实现高并发服务器

IO多路复用方式介绍

IO多路复用方式的优缺点

1.IO多路复用方式——select方式

select方式介绍

select方式使用的核心函数

select方式实现并发服务器的使用流程

select方式实现高并发服务器的示例代码(详细注释)

select实现并发服务器需要注意的几个细节

2.IO多路复用方式——epoll方式

epoll方式介绍

epoll方式的特点

epoll方式使用的核心函数

epoll方式实现高并发服务器的使用流程

epoll方式的LT工作模式和ET工作模式

LI工作模式(水平触发模式)

ET工作模式(边缘触发模式)

两种工作模式的比较和使用方式

epoll方式实现高并发服务器的示例代码(详细注释)

3.IO多路复用方式——poll方式

Poll方式介绍

Poll对比Select/Epoll的优缺点

poll方式的核心函数

poll方式实现高并发器示例代码(详细注释)

3种IO多路复用方式select、epoll、poll的总结对比

各自特点总结

适用场景


IO多路复用方式介绍

        在Socket编程的原始TCP服务器通信流程中(没有Socket网络编程基础可以查看这篇文章:Socket编程入门(全面干货)),单进程或单线程的TCP服务器会阻塞在下面几个IO系统调用中。

  1. 当服务器监听的文件描述符没有客户端发送连接时,该监听的文件描述符的内核接收缓冲区没有数据时,服务器会阻塞在accept函数中,直到有客户端发送连接。
  2. 在服务器与客户端通信的套接字文件描述符的内核接收缓冲区没有数据时,服务器会阻塞在read函数中,直到客户端发送数据。
  3. 在服务器与客户端通信的套接字文件描述符的内核发送缓冲区被写满时,虽然这种情况很小,但在网络负载很大的情况下可能发生,服务器会阻塞在write函数上,直到套接字内核缓冲区发送数据,使内核缓冲区有空闲空间时解除阻塞。

        除服务器会阻塞在上面这几个IO系统调用外,并且单进程或单线程的服务器也只能一个一个按先后连接的客户端顺序去给客户端提供服务,不仅会极大影响客户端体验,表现在客户端加载某些数据一直没反应等情况,也会极大浪费服务器硬件资源,服务器进程由于阻塞无法抢占CPU资源。IO多路复用方式实现在单线程或单进程的情况下,由内核监控指定的文件描述符是否处于不会阻塞的就绪状态,这时直接进行IO系统调用不会阻塞,使单进程或单线程不阻塞于某个特定的 I/O 系统调用,并可以同时并发为多个客户端提供服务。

       select()、poll()和epoll()都是实现I/O多路复用的机制。这些机制允许程序同时监控多个文件描述符,当其中任一描述符就绪(即具备读/写条件)时,系统会通知程序进行相应的I/O操作而不会阻塞。需要注意的是,这三种方法本质上都属于同步I/O。因为它们都要求程序在事件就绪后主动执行实际的读写操作,这个过程是阻塞式的。与之相对的是异步I/O,后者由系统自动完成数据在内核空间和用户空间之间的传输,无需程序主动参与读写操作。

IO多路复用方式的优缺点

优点

  1. 资源占用低:单线程/单进程即可管理大量连接(如n个连接),系统资源(内存、上下文切换)开销远低于多线程/多进程。
  2. 高并发性:适合高并发场景(如万级连接),通过事件驱动机制(如epollkqueue)高效监听多个IO事件。
  3. 无锁简化:避免多线程的锁竞争和同步问题。

缺点

  1. 编程复杂:回调或协程模式可能增加代码逻辑复杂度(如“回调地狱”)。
  2. CPU密集型任务弱:若任务需大量计算,单线程可能阻塞事件循环,需配合线程池使用。
  3. 调试困难:异步代码的调试和异常处理比同步模式更复杂。

1.IO多路复用方式——select方式

select方式介绍

        一种网络通信的手段,通过这种select方式会阻塞并同时监测多个套接字文件描述符的读/写缓冲区,一旦检测到有套接字文件描述符就绪( 可以读数据或者可以写数据)程序的阻塞就会被解除,就可以基于这些(一个或多个)就绪的套接字文件描述符进行通信与客户端的通信

  1. 优点:相比于采用多线程方式,select方式的并发通信使用的系统资源更少,相比于poll/epoll方式,允许跨平台(Windows,Linuxs,mMac)使用。
  2. 缺点:相比于poll/epoll,由于select方式的检测集合fd_set数据结构的检测文件描述符最多1024个的限制,select方式的服务器最多允许与1023个客户端进行通信。相比于epoll,select方式对检测集合的就绪状态套接字文件描述符采用线性方式,造成检测集合套接字文件描述符数量越多效率越低。 相比于epoll,select方式对检测集合到内核采用拷贝方式,返回的就绪集合也采用拷贝方式返回,效率教低。

        将服务器程序并发与多个客户端进行连接通信,并将这些套接字文件描述符读缓冲区(没有数据)/写缓冲区(数据已满)导致套接字函数阻塞程序,采用select方式进行周期检测套接字文件描述符的读缓冲区/写缓冲区是否就绪不阻塞程序,准备就绪的套接字文件描述符,就使用相应的套接字通信函数连接/通信,由于套接字文件描述符已就绪,调用相关连接/通信的套接字函数不会阻塞程序,这样就实现服务器程序能同时与多个客户端连接/通信,但在本质上还是线性一个一个处理套接字文件描述符已就绪的客户端的连接/通信。

select方式使用的核心函数

        select的IO多路转接方式需要调用select函数实现,进行检测设置好的套接字文件描述符的读/写/异常缓冲区是否有数据并进行返回,select函数是一种跨平台的函数(Linuxs,Windows,Mac),Linuxs操作系统中在头文件中。

  • select函数:用于监听获取处于指定的文件描述符是否处于就绪状态
//阻塞程序规定时长或由于3个集合有就绪文件描述符而提前唤醒程序
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval * timeout);

nfds:委托内核检测的这三个集合中最大的文件描述符+1,不知道就使用最大值1024,内核需要线性遍历这些集合中的文件描述符,在Window中这个参数是无效的,指定为-1即可
readfds:传入传出参数,套接字文件描述符的集合, 内核只检测这个集合中文件描述符对应的读缓冲区,要提前设置好要检测读缓冲区的套接字文件描述符,返回这些提前设置好并且已就绪的读缓冲区就绪的套接字文件描述符,一般包括服务器监听的套接字文件描述符/通信的套接字文件描述符。
writefds:传入传出参数,套接字文件描述符的集合, 内核只检测这个集合中文件描述符对应的写缓冲区,要提前设置好要检测写缓冲区的套接字文件描述符,返回这些提前设置好并且已就绪的写缓冲区就绪的套接字文件描述符,一般不使用可以设置为NULL,因为服务器通信的套接字文件描述符极小概率是满的而去阻塞write/send函数,大多数情况下直接采用send/write函数不会阻塞,除非发送的数据量特别大。
exceptfds:传入传出参数,套接字文件描述符的集合, 内核只检测这个集合中文件描述符对应的异常缓冲区,提前设置好要检测异常缓冲区的套接字文件描述符,返回这些提前设置好并且已绪的异常缓冲区就绪的套接字文件描述符,可以包括服务器监听的套接字文件描述符/通信的套接字文件描述符,一般不使用置为NULL
timeout:超时时长,select函数会阻塞服务器程序进行检测3个集合的文件描述符是否就绪,就绪就提前接触阻塞或超时时长后自动解除阻塞,struct timeval结构体

struct timeval {//超时时长timeout的数据类型
    time_t      tv_sec;//秒
    suseconds_t tv_usec;//纳秒,一般不使用初始化为0
};

返回值:>0=>提前返回集合中已就绪的文件描述符的总个数。0=>超时返回,没有检测到就绪的套接字文件描述符。-1=>select函数执行失败。

  • fd_set数据结构及相关操作函数

        fd_set类型,一种用于表示进程最大文件描述符个数1024个的文件描述符中内核所要检测的文件描述符,其中共有1024bit,每1个bit表示一个序号的文件描述符,bit值=1表示该bit对应的文件描述符在fd_se集合中要被内核检测,包括一系列可以操控该fd_set数据结构变量的函数。

void FD_ZERO(fd_set *set);

        FD_ZERO函数用于将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符。

int  FD_ISSET(int fd, fd_set *set);

        FD_ISSET函数用于判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1,即返回值在set集合中返回true,否则返回0。

void FD_SET(int fd, fd_set *set);

        FD_SET函数用于将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1

void FD_CLR(int fd, fd_set *set);

        FD_CLR函数用于将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0

        这些fd_set类型及相关函数用于设置哪些套接字文件描述符需要被监听是否就绪,使用思路如下。先设置fd_set结构的select函数要使用的3个集合,使用FD_ZERO函数初始化所有标志位为0,使用FD_SET函数提前设置好要检测读/写/异常缓冲区的套接字文件描述符,使用FD_CLR清除不需要监听的套接字文件描述符, 采用while循环使用select函数委托内核检测3个集合的套接字文件描述符的就绪状态并返回,使用FD_ISSET函数检查3个集合的套接字文件描述符是否就绪,相应套接字文件描述符就绪就使用相应的套接字函数进行连接/通信。使用举例:

// 创建要监测的事件集合,包括读、写、异常,不需要监测的情况下可以设置为NULL
fd_set recv_sockfd_set,send_sockfd_set,except_sockfd_set; 
// 初始化清空设置的事件集合
FD_ZERO(&recv_sockfd_set); FD_ZERO(&send_sockfd_set); FD_ZERO(&except_sockfd_set); 
// 在相应事件集合中添加要监听观察的套接字文件描述符
FD_SET(connect_sockfd,&recv_sockfd_set); FD_SET(listen_sockfd,&recv_sockfd_set); 
// 使用select函数来监听输出当前准备就绪的套接字文件描述符
struct timeval timeout={5,0}; 
select(1024,&recv_sockfd_set,&send_sockfd_set,&except_sockfd_set,&timeout); 
// 在select函数后,使用FD_ISSET函数检查哪些套接字文件描述符准备就绪
FD_ISSET(connect_sockfd,&recv_sockfd_set); FD_ISSET(listen_sockfd,&recv_sockfd_set); 

select方式实现并发服务器的使用流程

  1. 使用socket()函数创建服务器用于监听的套接字listen_sockfd=socket();
  2. 使用bind()函数将监听的套接字listen_sockfd和服务器本地的IP地址和端口号绑定
  3. 使用listen()给监听的套接字文件描述符listen_sockfd设置监听
  4. 创建一个套接字文件描述符集合fd_set recv_sockfd_set,用于存储需要检测读事件的所有的文件描述符通过FD_ZERO()初始化读缓冲区的套接字文件描述符集合recv_sockfd_set标志位为0,即集合没有任何文件描述符通过FD_SET()将套接字文件描述符listen_sockfd放入检测的读集合recv_sockfd_set中。
  5. 循环调用select(),周期性的对所有的文件描述符进行检测,服务器线程等待select() 解除阻塞返回,得到内核传出的满足条件的就绪的读缓冲区的套接字文件描述符集合。根据select函数的返回值来判断下一步操作:
    • ①等于0时,表示当前事件集合中在超时时间内还没有要监听的文件描述符准备就绪,直接continue,进行循环下次select函数的调用。
    • ②等于-1时,表示select函数发生未知错误,直接break终止,回收资源,退出服务器。
    • ③大于0时,表示当前事件集合中已就绪的文件描述符个数,此时需要从0遍历文件描述符到max_fd+1,采用FD_ISSET函数判断这些文件描述符的事件是否处于就绪状态,并区分判断是否为监听客户端连接的套接字、还是与客户端通信的套接字。
      • 1)如果是用于监听客户端连接的套接字文件描述符listen_sockfd的读缓冲区有数据(有客户端发起连接),调用accept()函数和客户端建立连接,并将accept()得到的新的通信的文件描述符,通过FD_SET()放入到读集合recv_sockfd_set中,并保存与该客户端通信的相关数据,一般考虑使用结构体数组[1023]保存。
      • 2)如果是与该客户端通信的套接字文件描述符connect_sockfd的读缓冲区有数据(该客户端给服务器发送数据),使用recv()/read()函数读取该客户端发送的数据,判断recv()/read()函数的返回值==》
                『1』返回=0,客户端断开连接,关闭与客户端通信的套接字文件描述符,并使用FD_CLR()将该通信的套接字文件描述符connect_sockfd从读集合recv_sockfd_set中删除,并关闭该套接字文件描述符,回收相关保存与该客户端通信的数据.
                『2』返回>0,服务器成功接收客户端发送的数据,服务器处理数据,将结果使用send()/write()函数发回给客户端。
                『3』返回<0,服务器接收客户端发送的数据失败,出现未知错误,服务器直接关闭与客户端通信的套接字文件描述符。
  6. 重复第5步,实现服务器单线程循环接受多个客户端的连接和通信,在退出服务器程序前,关闭监听套接字文件描述符等资源即可。

select方式实现高并发服务器的示例代码(详细注释)

客户端:

实现思路:在客户端进程中继续创建多个子进程启动客户端程序,模拟在高并发环境下,多个客户端几乎同时连接服务器情况,然后在客户端主进程中回收所有子进程。

#include //提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
#include //提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
#include //提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
#include //提供用于处理C风格字符串(即以''结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
// 线程相关
#include //提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
#include  //包括信号量sem_t
// 进程相关
#include  // 进程优先级相关函数
#include  // 进程等待函数
#include  // 类型定义
#include  // 信号函数
// socket网络编程相关
#include 
#include  //Socket编程的数据结构和函数
#include  //Socket编程IO复用的select方式
#include   //Socket编程IO复用的epoll方式

void client(int client_number, int communicate_number) {
    // 主进程回收终止子进程的信号函数
    struct sigaction sigact_chld;
    sigact_chld.sa_handler = [](int signal) {
        switch (signal) {
        case SIGCHLD:
            // 子进程终止信号,非阻塞回收子进程的子进程
            int cpid;
            while ((cpid = waitpid(-1, NULL, WNOHANG)) > 0)
                printf("client son process:%d recycle son process:%d
", getpid(), cpid);
            break;

        default:
            break;
        }
        };
    sigact_chld.sa_flags = SA_RESTART;
    sigemptyset(&sigact_chld.sa_mask); // 信号屏蔽位置空,默认只屏蔽自身
    // 注册信号和信号处理函数
    sigaction(SIGCHLD, &sigact_chld, NULL);

    // 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
    int spid; // 子进程fork的返回值
    for (int i = 0; i < client_number; i++) {
        spid = fork(); // 创建子进程
        if (spid == 0)
            break; // 保证不会使创建的子进程迭代创建,只有父进程进行子进程的创建
    }

    // 子进程作为客户端访问服务器
    printf("son process:%d start client!
", getpid());
    // 1.创建与服务器进行通信的客户端socket
    int communicate_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (communicate_sockfd == -1) {
        printf("client:%d socket() error!
", getpid());
        // 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
        if (spid > 0) {
            // 阻塞回收子进程的子进程
            int cpid;
            while ((cpid = waitpid(-1, NULL, 0)) > 0)
                printf("client son process:%d recycle son process:%d
", getpid(), cpid);
        }
        exit(-1);
    }

    // 2.客户端连接服务器,需要和服务器bind绑定的地址相同
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 查看自己主机网口的所有IP地址的任意一个,其中127.0.0.1是必有的
    server_addr.sin_port = 10086;
    int ret = connect(communicate_sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
    if (ret == -1) {
        printf("client:%d connect() error!
", getpid());;
        close(communicate_sockfd);
        // 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
        if (spid > 0) {
            // 阻塞回收子进程的子进程
            int cpid;
            while ((cpid = waitpid(-1, NULL, 0)) > 0)
                printf("client son process:%d recycle son process:%d
", getpid(), cpid);
        }
        exit(-1);
    }

    // 客户端和服务器进行循环通信
    // 为观察输出,假设客户端和服务器只通信1次,客户端就主动关闭
    for (int i = 0; i < communicate_number; i++) {
        // 3.客户端给服务器发送数据
        char wrbuf[1024];
        sprintf(wrbuf, "Hello World form client:%d", getpid());
        int wrbyte = write(communicate_sockfd, wrbuf, strlen(wrbuf));
        // 根据返回值判断状态,执行相应行为
        if (wrbyte == -1) {
            printf("client:%d connect server error, client disconnect!
", getpid());
            close(communicate_sockfd);
            // 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
            if (spid > 0) {
                // 阻塞回收子进程的子进程
                int cpid;
                while ((cpid = waitpid(-1, NULL, 0)) > 0)
                    printf("client son process:%d recycle son process:%d
", getpid(), cpid);
            }
            exit(-1);
        }
        else
            printf("client:%d send numbers:%d data.
", getpid(), sizeof(wrbuf));

        sleep(2);

        // 4.客户端等待接收服务器的数据
        char rdbuf[1024];
        memset(rdbuf, 0, sizeof(rdbuf)); // 清空接收缓冲区
        int rdbyte = read(communicate_sockfd, rdbuf, sizeof(rdbuf));
        // 根据返回值判断状态,执行相应行为
        if (rdbyte == -1) {
            printf("client:%d connect server error, client disconnect!
", getpid());
            close(communicate_sockfd);
            // 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
            if (spid > 0) {
                // 阻塞回收子进程的子进程
                int cpid;
                while ((cpid = waitpid(-1, NULL, 0)) > 0)
                    printf("client son process:%d recycle son process:%d
", getpid(), cpid);
            }
            exit(-1);
        }
        else if (rdbyte == 0) {
            printf("server disconnect, client:%d disconnect!
", getpid());
            close(communicate_sockfd);
            // 主进程退出前阻塞回收所有以终止子进程,其他子进程交给内核处理
            if (spid > 0) {
                // 阻塞回收子进程的子进程
                int cpid;
                while ((cpid = waitpid(-1, NULL, 0)) > 0)
                    printf("client son process:%d recycle son process:%d
", getpid(), cpid);
            }
            exit(-1);
        }
        else
            printf("client:%d receive server numbers:%d data:%s
", getpid(), rdbyte, rdbuf);
    }

    // 客户端主动关闭套接字,关闭与服务器连接
    close(communicate_sockfd);

    // 主进程退出前非阻塞回收所有以终止子进程,其他子进程交给内核处理
    if (spid > 0) {
        // 阻塞回收子进程的子进程
        int cpid;
        while ((cpid = waitpid(-1, NULL, 0)) > 0)
            printf("client son process:%d recycle son process:%d
", getpid(), cpid);
    }
    exit(0);

    // 客户端子进程运行结束
}
服务器:

        服务器默认绑定地址为0.0.0.0:10086,默认监听本地所有IP地址,包括上述客户端连接的必有的回环地址127.0.0.1,如果只想监听某个特定的外网或内网IP地址可以更改,同时需要更改客户端地址

实现思想:基本遵循上述介绍的select实现并发服务器流程。

以下代码包括完整的服务器和客户端。

// select方式的高并发服务器和客户端
sigjmp_buf env_select; // 保存服务器状态
void select_TCP_server_client() {
    // 创建条件变量
    int semmid = semget(ftok(".", 1), 1, 0666 | IPC_CREAT);
    if (semmid == -1) {
        printf("semget() error.
");
        return;
    }
    // 初始化条件变量
    semctl(semmid, 0, SETVAL, 0); // 条件变量初始化为0

    // 创建子进程,主进程运行服务器,子进程运行并发客户端
    int pid = fork();
    if (pid > 0) {
        // 主进程运行服务器
        printf("main process:%d start server!
", getpid());

        // 注册信号和处理函数,主要包括两个信号:SIGCHLD和SIGINT
        struct sigaction sigact;
        sigact.sa_handler = [](int signal) {
            switch (signal)
            {
            case SIGCHLD:
                // 子进程终止信号,非阻塞回收
                int spid;
                while ((spid = waitpid(-1,NULL,WNOHANG)) > 0)
                    printf("main process:%d recycle son process:%d.
", getpid(), spid);
                break;

            case SIGINT:
                // ctrl+c终止信号
                siglongjmp(env_select, signal);
                break;

            default:
                break;
            }
        };
        // 被本信号打断的系统调用在处理完本信号的函数后会重新启动,不会让原系统调用返回错误,但不是所有系统都支持
        sigact.sa_flags = SA_RESTART; 
        sigemptyset(&sigact.sa_mask); // 默认只屏蔽自身信号
        // 信号和处理函数注册
        sigaction(SIGCHLD, &sigact, NULL);
        sigaction(SIGINT, &sigact, NULL);

        // 错误提示和退出函数
        void (*clear)(char*, int, int) = [](char* tips, int semmid, int sockfd=-1) {
            // 打印错误信息
            printf("%s
", tips);
            // 执行V操作释放信号量
            sembuf sem_buf;
            sem_buf.sem_num = 0;
            sem_buf.sem_flg = 0;
            sem_buf.sem_op = +1; // V操作
            semop(semmid, &sem_buf, 1);

            // 关闭套接字文件描述符
            if (sockfd > 0)
                close(sockfd);

            // 主进程退出前,回收子进程
            int spid;
            int status;
            while ((spid = waitpid(-1, &status, 0)) > 0)
                printf("main process:%d recycle son process:%d.
", getpid(), spid);

            return;
        };
        
        // 1.创建TCP监听socket
        int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_sockfd == -1) {
            // 创建失败直接返回
            clear("server socket() error!", semmid, listen_sockfd);
            return;
        }
        // 设置服务器可以快速重用sock地址重启
        int opt = 1;
        int ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        if (ret == -1) {
            // 创建失败直接返回
            clear("server setsockopt() error!", semmid, listen_sockfd);
            return;
        }

        // 2.绑定本地地址
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");//htonl(INADDR_ANY); // 绑定0.0.0.0
        server_addr.sin_port = 10086;
        ret = bind(listen_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
        if (ret == -1) {
            // 绑定失败直接返回
            clear("server bind() error!", semmid, listen_sockfd);
            return;
        }

        // 3.监听客户端的连接
        ret = listen(listen_sockfd, 16);
        if (ret == -1) {
            // 绑定失败直接返回
            clear("server listen() error!", semmid, listen_sockfd);
            return;
        }
        printf("main process:%d start sever listening(port:%d)..........
", getpid(), server_addr.sin_port);
        // 服务器启动成功,执行V操作释放启动子进程客户端
        sembuf sem_buf;
        sem_buf.sem_num = 0;
        sem_buf.sem_flg = 0; // 表示使用默认阻塞方式
        sem_buf.sem_op = +1;
        semop(semmid, &sem_buf, 1); // 执行V操作

        // 4.创建select需要的事件集合并初始化
        fd_set recvfd_set; // 创建读事件集合
        FD_ZERO(&recvfd_set); // 初始化清空
        FD_SET(listen_sockfd, &recvfd_set); // 将监听套接字加入到读事件集合中
        int max_fd = listen_sockfd; // 保存要监听的最大的文件描述符
        
        // 5.循环采用select监听
        while (true) {
            ret = sigsetjmp(env_select, 1); // 保存服务器堆栈状态
            if (ret)
                break; // 出现错误,直接终止退出循环

            // 使用select函数循环监听要观察的套接字文件描述符状态
            fd_set recvfd_set_clone = recvfd_set; // 监听事件的副本,避免select函数进行修改
            struct timeval timeout = { 1,0 }; // 设置超时时间为1s
            int max_fd_clone = max_fd; // 最大监听文件描述符的副本
            ret = select(max_fd + 1, &recvfd_set_clone, NULL, NULL, &timeout);
            // 根据select函数的返回值判断状态
            if (ret == -1) {
                // select系统调用被其他信号中断后会直接返回-1,errno被设置为EINTR,但select没错误,需要继续循环
                if (errno == EINTR) 
                    continue;  
                printf("server select() error! errno=%d
", errno);
                break;
            }
            else if (ret == 0) 
                continue; // 表示在超时时间内,没有观察的套接字就绪
            else {
                // 有监听的文件描述符就绪,进行遍历寻找就绪文件描述符,然后区分是服务器监听文件描述符或客户端通信文件描述符
                for (int i = 0; i < max_fd_clone + 1; i++) 
                    // 先判断是否是事件就绪文件描述符
                    if (FD_ISSET(i, &recvfd_set_clone)) {
                        // 判断监听套接字、通信套接字
                        if (i == listen_sockfd) {
                            // 监听套接字,说明有客户端发起连接请求
                            struct sockaddr_in client_addr;
                            socklen_t len = sizeof(client_addr);
                            memset(&client_addr, 0, len);
                            // 接受客户端的连接
                            int communicate_sockfd = accept(i, (struct sockaddr*)&client_addr, &len);
                            if (communicate_sockfd == -1)
                                continue; // 连接客户端失败,直接返回
                            else {
                                // 连接客户端成功,将该与客户端通信的文件描述符加入到读事件集合中
                                FD_SET(communicate_sockfd, &recvfd_set);
                                // 判断更新最大观察的文件描述符
                                max_fd = (communicate_sockfd > max_fd_clone) ? communicate_sockfd : max_fd_clone;
                                printf("server connect client:%s:%d success. communicate sockfd:%d.
", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), communicate_sockfd);
                            }
                        }
                        else {
                            // 通信套接字,说明有连接成功的客户端发送信息
                            char rdbuf[1024]; // 读缓冲区
                            memset(rdbuf, 0, sizeof(rdbuf));
                            // 读取客户端的信息
                            int rdbyte = read(i, rdbuf, sizeof(rdbuf));
                            // 根据信息的返回值判断读取状态
                            if (rdbyte == -1) {
                                // 读取信息出现错误,直接在事件集合中去除观察,并关闭与客户端通信套接字
                                FD_CLR(i, &recvfd_set);
                                close(i);
                                printf("server read() error! close communicate:%d.
", i);
                                continue;
                            }
                            else if (rdbyte == 0) {
                                // 客户端断开,服务器断开连接
                                FD_CLR(i, &recvfd_set);
                                close(i);
                                printf("cient disconnect, server disconnect! close communicate:%d.
", i);
                                continue;
                            }
                            else {
                                // 服务器成功读取到客户端数据
                                printf("server read client %d number data:%s
", rdbyte, rdbuf);
                                // 模拟处理数据
                                sleep(1);
                                // 服务器将数据写回发给客户端
                                char wrbuf[] = "Hello server form client.";
                                int wrbyte = write(i, wrbuf, sizeof(wrbuf));
                                // 根据返回值判断发送状态
                                if (wrbyte == -1) {
                                    // 发送信息失败,直接断开连接,关闭套接字并停止监听
                                    printf("server write() error! close communicate:%d.
", i);
                                    FD_CLR(i, &recvfd_set);
                                    close(i);
                                    continue;
                                }
                                else {
                                    // 发送信息给客户端成功
                                    printf("server send client %d number data.
", wrbyte, wrbuf);
                                    continue;
                                }
                            }

                        }
                    }
            }
        }

        // 6.退出服务器,释放资源
        close(listen_sockfd);

        // 由于还是主进程,需要阻塞回收子进程
        int spid;
        while ((spid = waitpid(-1, NULL, 0)) > 0)
            printf("main process:%d recycle son process:%d.", getpid(), spid);
        // 释放信号量
        semctl(semmid, 0, IPC_RMID);

        // 主进程服务器结束
    }
    else if (pid == 0) {
        // 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
        sembuf sem_buf;
        sem_buf.sem_num = 0;
        sem_buf.sem_op = -1; // P操作
        sem_buf.sem_flg = 0; // 默认阻塞方式
        semop(semmid, &sem_buf, 1); // 执行P操作

        // 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
        client(2, 1); // 创建2+1个客户端子进程,每个客户端只发送1次消息就退出,便于观察
    }
    else
        printf("fork() error!
");
}

select实现并发服务器需要注意的几个细节

1.fd_set数据类型本质上是一个位图(bit array)结构,不是文件描述符,但其中每个bit位按顺序对应于1个相应套接字文件描述符的状态。并且这个位图一般最大位数是1024,这也是为什么select最多可以监听1024个的套接字文件描述符的就绪状态。

2.select函数会修改传入的事件集合fd_set变量,其会将fd_set中所关心要监听的套接字文件描述符进行修改,输出事件集合中的要监听的套接字文件描述符,其中已经处于就绪状态的bit位设置为1,表示该bit位对应的文件描述符的读、写、异常事件已经就绪。因此在进行循环监听时,要在传入select函数前使用fd_set的副本,以避免每次select前都要重新设置监听套接字。

3.采用FD_ISSET函数可以观察事件集合fd_set中指定顺序的套接字文件描述符对应的bit位是否为1,由于select函数的特性,在使用select函数前,FD_ISSET函数可以观察指定bit位的套接字文件描述符是否处于我们要观察的套接字文件描述符中,在使用select函数后,FD_ISSET函数可以判断指定bit位的套接字文件描述符的相应事件是否已经处于就绪状态。

4.select函数是采用从0到指定最大文件描述符max_fd-1进行按bit位查找的,要监听的文件描述符必须处于这个范围间才能被select监听到,由于select方式最大可以监听1024个文件描述符,可以直接填1024,但为了提高select的查找效率,往往需要记录当前最大监听套接字文件描述符,在select函数使用时设置为max_fd+1。

5.select函数属于系统调用方式,可以被其他信号打断终止select系统调用,select函数会直接返回-1,并设置全局变量errno为EINTR,因此在判断select函数的返回值为-1时,还需要检查errno是否为EINTR,不是才是真正发生select函数错误。

// 使用select函数循环监听要观察的套接字文件描述符状态
fd_set recvfd_set_clone = recvfd_set; // 监听事件的副本,避免select函数进行修改
struct timeval timeout = { 1,0 }; // 设置超时时间为1s
int max_fd_clone = max_fd; // 最大监听文件描述符的副本
ret = select(max_fd + 1, &recvfd_set_clone, NULL, NULL, &timeout);
// 根据select函数的返回值判断状态
if (ret == -1) {
    // select系统调用被其他信号中断后会直接返回-1,errno被设置为EINTR,但select没错误,需要继续循环
    if (errno == EINTR) 
        continue;  
    printf("server select() error! errno=%d
", errno);
    break;
}

2.IO多路复用方式——epoll方式

epoll方式介绍

        类似于select方式,epoll方式同样委托内核完成对要检测的套接字文件描述符的读缓冲区/写缓冲区进行检测,检测就绪后调用相关函数进行非阻塞的连接或通信,epoll方式使用思路和select方式相同,不同在于函数及实现方式。

epoll方式的特点

  1. 对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合。

  2. select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降。
  3. select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现)。
  4. 程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测。
  5. 使用epoll没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制,select方式只允许最多1023个客户端的连接。

epoll方式使用的核心函数

epoll方式的操作函数:#include

  • 创建epoll实例红黑树:
int epoll_create(int size);

        通过一棵epoll实例红黑树管理套接字文件描述符的待检测集合,每一个待检测的套接字文件描述符都是一个结点
size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了。
返回值:失败返回-1,成功返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的epoll实例了。

  • 管理(添加、修改、删除)epoll实例红黑树上的套接字文件描述符结点
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

fd和event会一起保存在epoll实例红黑树的某个结点上,event用于修饰fd的属性
epfd:
epoll实例红黑树的套接字文件描述符,即epoll_create() 函数的返回值,通过这个参数可以找到epoll实例红黑树
op:枚举值,控制设置相应的枚举值,指定该epoll_ctl函数执行什么具体操作
        EPOLL_CTL_ADD:往epoll模型中添加新的节点,即添加新的要检测的套接字文件描述符fd和其event属性在epoll实例红黑树
        EPOLL_CTL_MOD:修改epoll模型中已经存在的节点,即修改已存在的套接字文件描述符fd的event属性,event要传入修改新的struct epoll_event类型变量地址
        EPOLL_CTL_DEL:删除epoll模型中的指定的节点,即删除已存在的套接字文件描述符fd和其event属性,event可以设置为NULL即可
fd:要检测的套接字文件描述符,即要添加/修改/删除的套接字文件描述符
event:struct epoll_event类型包括两个属性events/data用,events用于修饰fd套接字文件描述符,指定检测参数fd的套接字文件描述符的读/写/异常事件,保存的用户要使用的数据data,data是一个联合体,不用保存连接客户端信息可以考虑直接采用fd成员变量只保存通信文件描述符, 如果需要保存客户端信息需要使用ptr成员,需要定义结构体并采用结构体保存客户端数据,由于ptr是指针,必须将结构体开辟在堆区,否则栈区局部变量回收,使ptr无效地址,在epoll_wait取出时data的ptr时无法使用。

使用到的struct epoll_event类型和epoll_data_t类型如下:

struct epoll_event {
    uint32_t events;//枚举值,委托epoll检测的fd套接字文件描述符的具体读/写/异常事件
                    //EPOLLIN:读事件, 接收数据, 检测读缓冲区,如果有数据该文件描述符就绪
                    //EPOLLOUT:写事件, 发送数据, 检测写缓冲区,如果数据没有满该文件描述符就绪
                    //EPOLLERR:异常事件
    epoll_data_t data;//联合体类型,内核不使用,用于保存用户数
};
/联合体union, 多个变量共用同一块内存,实际只能使用联合体中的一个变量
typedef union epoll_data {
    void* ptr;    //当用户要保存除套接字文件描述符fd外的多个数据时,可以使用成员ptr
    int fd;       //一般使用成员fd用于存储待检测的文件描述符的值, 和epoll_ctl的参数fd相同即可
    uint32_t u32; //很少使用
    uint64_t u64; //很少使用
} epoll_data_t;

返回值:成功返回0,失败返回-1

  • 内核检查epoll观察的文件描述符,并将其中就绪状态的文件描述符的struct epoll_event传出
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epfd:epoll实例红黑树的套接字文件描述符,即epoll_create() 函数的返回值,通过这个参数可以找到epoll实例红黑树
events:传出参数,struct epoll_event类型数组地址,该数组保存epoll_create函数添加套接字文件描述符fd和其event属性events相应读/写/异常事件就绪的epoll实例红黑树结点的套接字文件描述符fd的struct epoll_event类型的event属性,event的成员data也会传出,不是直接返回就绪的套接字文件描述符。
maxevents:传出参数,用于指明struct epoll_event类型数组events中可存储的事件就绪的套接字文件描述符的最大个数
timeout:阻塞时长,单位为毫秒。=0:不阻塞直接返回。=-1:阻塞直到epoll实例红黑树有相应事件就绪套接字文件描述符结点返回。
返回值:函数执行失败返回-1,注意此时需要检查函数执行失败的错误码errno,当errno为EINTR(系统调用中断)EAGAIN(epoll文件描述符数据为空)时,实质函数执行不是错误;执行成功返回相应事件就绪的套接字文件描述符个数,即events数组元数个数,不超过events数组最大元素个数maxevents,当实际就绪事件event个数大于maxevents时,这次会填满events数组,再次调用获取的事件将从上次取得结尾地方开始获取,类似于队列。


epoll方式实现高并发服务器的使用流程

        在本质上epoll方式并发服务器和select方式并发服务器流程几乎一样,思想完全相同,但是由于函数不同,实现会有少许不同。

服务器端:
    1.使用socket()函数创建服务器用于监听的套接字listen_sockfd=socket();
    2.使用bind()函数将监听的套接字listen_sockfd和服务器本地的IP地址和端口号绑定
    3.使用listen()给监听的套接字文件描述符listen_sockfd设置监听
    4.使用epoll_create函数创建一个epoll实例红黑树,结点用于存储需要检测读事件的所有的文件描述符,返回这个epoll红黑树实例的文件描述符使用epoll_ctl函数添加监听套接字文件描述符listen_sockfd和检测读事件event及保存listen_sockfd的用户数据data到epoll实例红黑树中。
    5.循环使用epoll_wait函数,周期性的对添加到epoll实例所有的文件描述符进行相应事件的检测,服务器线程等待epoll_wait() 解除阻塞返回,得到内核传出的满足相应读事件就绪的套接字文件描述符结构体struct epoll_event数组events,遍历这个events数组检查data属性中是否为监听/通信的套接字文件描述符.
        1)如果events数组循化的是监听套接字文件描述符,则监听的套接字文件描述符listen_sockfd的读缓冲区有数据(有客户端发起连接),调用accept()函数和客户端建立连接,并将accept()得到的新的通信的文件描述符connect_sockfd和保存listen_sockfd的用户数据data和检测事件及工作模式events,通过epoll_ctl函数添加到epoll实例中,并保存与该客户端通信的相关数据,对于epoll方式一般考虑data指针指向结构体变量进行保存。
        2)如果events数组循环的是通信套接字文件描述符connect_sockfd,则与客户端通信的套接字文件描述符connect_sockfd的读缓冲区有数据(该客户端给服务器发送数据),服务器与客户端进行数据通信。
        服务器循环read/recv读取客户端发送的数据的固定大小,一次性接收完毕,需要提前设置与客户端通信的套接字文件描述符的O_NONBLOCK属性 ==》循环使用recv()/read()函数读取数据到固定大小的临时缓冲区recv_buff,一次性接收完毕到recv_data,每次循环判断recv()/read()函数的返回值==》
        『1』返回=0,客户端断开连接,则服务器也断开连接并回收与客户端通信的数据,必须先使用epoll_ctl()将该通信的套接字文件描述符connect_sockfd从epoll实例中删除,再关闭该套接字文件描述符(顺序搞反epoll_ctl函数会报错),最后回收相关保存与该客户端通信的数据,回收recv_data数据,退出循环读取。
        『2』返回>0,recv_buff本次循环成功接收客户端发送的数据,扩充recv_data容量,将原recv_data数据和本次循环读取的recv_buff数据放入recv_data保存
        『3』返回<0,接收客户端发送的数据失败/读取完,errno==EAGAIN服务器正常接收完客户端数据,退出循环读取,否则服务器接收数据失败,可将失败信息发送回客户端,并且释放recv_data数据,退出循环读取。如果服务器正常接收数据结束(read/recv函数返回-1并且errno==EAGAIN),服务器处理客户端发送的数据后释放recv_data数据,将处理结果发回客户端。
6.重复第5步,实现服务器单线程循环接受多个客户端的连接和通信。
*由于epoll方式连接的客户端数量不像select方式受到fd_set集合可检测套接字文件描述符最大1024数量限制,epoll红黑树可用于保存的结点个数不受限制,因此可检测的套接字文件描述符fd和其检测事件struct epoll_event类型也不会受到限制,即可连接的客户端数量也不受限制,如果需要保存服务器连接客户 端通信的数据,可以采用双向循环链表实现,采用结构体数组方式会使连接的客户端数量受到数组大小的限制。


epoll方式的LT工作模式和ET工作模式

        这两种工作模式的区别在于epoll_wait函数对传出就绪的文件描述符的就绪状态判断情况不同,LT模式只要文件描述符的读/写事件处于就绪状态,就会被epoll_wait传出ET模式要求文件描述符的读/写事件的就绪状态发生变换时,如从读不就绪变成读就绪,写不就绪变成写就绪,才会被epoll_wait传出

LI工作模式(水平触发模式)

        默认的工作模式,只要epoll实例红黑树上要检测相应读/写/异常事件的文件描述符满足触发条件,每次调用epoll_wait函数都会进行通知。

  1. 对于读就绪来说:只要观察的文件描述符的读缓冲区有数据时,epoll_wait函数就会一直传出该文件描述符的读就绪状态。
  2. 对于写就绪来说,只要观察的文件描述符的写缓冲区有剩余空间时,epoll_wait函数就会一直传出该文件描述符的写就绪状态。

        LT模式的特点就是允许服务器可以不一次性全部读取完读内核缓冲区所有数据、写满写内核缓冲区所有数据,因为可以在下一次epoll_wait循环时,持续通知上次未读取完或写满的文件描述符的就绪状态。

ET工作模式(边缘触发模式)

        默认不启用,必须在添加套接字文件描述符fd及检测事件event的属性events显示设置EPOLLET才会启用,相比于水平触发方式,边缘触发方式只会在读写就绪状态发生变化时、或有新数据或新空间来时才会触发读写就绪状态,epoll_wait函数才会返回该文件描述符的就绪状态。

  1. 对于读就绪来说:只有观察的文件描述符从不可读状态变为可读状态时、或有新数据到来时,即读缓冲区为空变成不为空有数据时、或新数据到来时,才会触发读就绪。一直处于可读状态即文件描述符的读缓冲区一直有数据时,不会产生读就绪状态被epoll_wait函数传出。
  2. 对于写就绪来说:只有观察的文件描述符从不可写状态变为可写状态时、或有新剩余空间时,即写缓冲区从满变成不为满时、或发送部分数据有新剩余空间时,才会触发写就绪。一直处于可写状态即写缓冲区一直不满可写时,不会产生写就绪状态被epoll_wait函数传出。

        ET模式的特点就是要求服务器一次性读就绪时,全部读取完读缓冲区所有数据,将读就绪状态变为不可读,直到下次有新数据发送来时,重新将不可读状态变为可读状态,这样才会触发下次读就绪事件被epoll_wait传出。写就绪同理,要求服务器一次写就绪时,全部写满写缓冲区所有数据,将可写状态变为不可写,直到内核将写缓冲区数据发送出去有剩余写缓冲区空间时,重新将不可写状态变为可写状态,这样才会触发下次写就绪事件被epoll_wait传出。

两种工作模式的比较和使用方式

ET模式比LT模式服务器效率更高,因为ET模式要求一次就绪就读完读缓冲区或写满写缓冲区。
ET模式有如下要求:

  1. 对于接收/写入数据必须一次性接收完毕/写满,一般不可能允许使用一个足够大的用户缓冲区,一次性接收/写完所有数据,而考虑使用固定较小的用户缓冲区,采用循环read接收直到读完读缓冲区或write循环写入直到写满写缓冲区。但是这时采用阻塞的通信文件描述符就会发生阻塞,不适用于单线程或单进程。
  2. 必须使用非阻塞的文件描述符,以recv/read函数或write/send函数返回-1和errno设置为EAGAIN作为循环recv/read读取结束标志或循环write/send写入结束标志。
  3. 由于采取非阻塞的文件描述符,并以返回-1和errno设置为EAGAIN作为循环结束标志。在文件描述符不处于就绪状态,如读缓冲区本身为空或写缓冲区本身已满,也会出现相同的返回-1和errno设置为EAGAIN作为循环结束标志,因此必须采用select/epoll的就绪检查,以保证是就绪状态才去读取/接收。

LT模式要求:

  1. 由于epoll的持续通知就绪特性,允许不采用一次就绪就读取/写入数据完成,因此可以采用默认的阻塞文件描述符。
  2. 为提高效率,也可以采用类似于ET模式,采用非阻塞文件描述符,采用循环读取或写入,以返回-1和errno设置为EAGAIN作为循环结束标志。

循环读取数据一次就绪接收/写入完毕方式:

  1. 采用与客户端通信的文件描述符非阻塞读取方式,头文件可设置文件描述符的非阻塞属性:
int flag = fcntl(sockfd_connect, F_GETFL);//获取当前与客户端通信的套接字文件描述符的属性
flag |= O_NONBLOCK;//采取位或运算给当前文件描述符属性flag添加非阻塞属性O_NONBLOCK
fcntl(sockfd_connect, F_SETFL, flag);//重新给文件描述符添加非阻塞属性
  1. 使用select方式/epoll方式检测到非阻塞的套接字文件描述符就绪后,才可使用read/recv函数循环读取或write/sand函数循环写入。
  2. .采用循环使用recv()/read()函数读取一个固定大小的套接字读缓冲区数据到同样大小的临时缓冲区recv_buff中,每次循环读取的临时缓冲区数据保存在一个最终结果recv_data的字符串尾部,直到读取完与客户端通信的套接字文件描述符的读缓冲区所有数据,以返回-1和errno设置为EAGAIN作为循环结束标志停止循环。同理,采用循环write/send函数写入用户要发送的数据,每次循环后都要保存上次未发送数据的位置,下次可以接着发送,直到以返回-1和errno设置为EAGAIN作为循环结束标志停止循环。

epoll方式实现高并发服务器的示例代码(详细注释)

        客户端的代码和select中客户端的代码完全一致,只介绍服务器的实现,服务器实现的基本思想遵循上面epoll方式实现高并发服务器的流程思想。

完整代码实现:默认采用LI模式实现,实际采用非阻塞socket的ET模式epoll方式并发效率更高,也是现代生产常用方式,可以看看这篇文章Socket编程进阶:百万并发服务器。

#include //提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
#include //提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
#include //提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
#include //提供用于处理C风格字符串(即以''结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
// 线程相关
#include //提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
#include  //包括信号量sem_t
// 进程相关
#include  // 进程优先级相关函数
#include  // 进程等待函数
#include  // 类型定义
#include  // 信号函数
// socket网络编程相关
#include 
#include  //Socket编程的数据结构和函数
#include  //Socket编程IO复用的select方式
#include   //Socket编程IO复用的epoll方式

// epoll方式的LI工作模式的高并发服务器和客户端
sigjmp_buf env_epoll; // 保存服务器状态
void epoll_TCP_server_client() {
    // 创建条件变量
    int semmid = semget(ftok(".", 1), 1, 0666 | IPC_CREAT);
    if (semmid == -1) {
        printf("semget() error.
");
        return;
    }
    // 初始化条件变量
    semctl(semmid, 0, SETVAL, 0); // 条件变量初始化为0

    // 创建子进程,主进程运行服务器,子进程运行并发客户端
    int pid = fork();
    if (pid > 0) {
        // 主进程运行服务器
        printf("main process:%d start server!
", getpid());

        // 注册信号和处理函数,主要包括两个信号:SIGCHLD和SIGINT
        struct sigaction sigact;
        sigact.sa_handler = [](int signal) {
            switch (signal)
            {
            case SIGCHLD:
                // 子进程终止信号,非阻塞回收
                int spid;
                while ((spid = waitpid(-1, NULL, WNOHANG)) > 0)
                    printf("main process:%d recycle son process:%d.
", getpid(), spid);
                break;

            case SIGINT:
                // ctrl+c终止信号
                siglongjmp(env_epoll, signal);
                break;

            default:
                break;
            }
            };
        // 被本信号打断的系统调用在处理完本信号的函数后会重新启动,不会让原系统调用返回错误,但不是所有系统都支持
        sigact.sa_flags = SA_RESTART;
        sigemptyset(&sigact.sa_mask); // 默认只屏蔽自身信号
        // 信号和处理函数注册
        sigaction(SIGCHLD, &sigact, NULL);
        sigaction(SIGINT, &sigact, NULL);

        // 错误提示和退出函数
        void (*clear)(char*, int, int) = [](char* tips, int semmid, int sockfd = -1) {
            // 打印错误信息
            printf("%s
", tips);
            // 执行V操作释放信号量
            sembuf sem_buf;
            sem_buf.sem_num = 0;
            sem_buf.sem_flg = 0;
            sem_buf.sem_op = +1; // V操作
            semop(semmid, &sem_buf, 1);

            // 关闭套接字文件描述符
            if (sockfd > 0)
                close(sockfd);

            // 主进程退出前,回收子进程
            int spid;
            int status;
            while ((spid = waitpid(-1, &status, 0)) > 0)
                printf("main process:%d recycle son process:%d.
", getpid(), spid);

            return;
            };

        // 1.创建TCP监听socket
        int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_sockfd == -1) {
            // 创建失败直接返回
            clear("server socket() error!", semmid, listen_sockfd);
            return;
        }
        // 设置服务器可以快速重用sock地址重启
        int opt = 1;
        int ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        if (ret == -1) {
            // 创建失败直接返回
            clear("server setsockopt() error!", semmid, listen_sockfd);
            return;
        }

        // 2.绑定本地地址
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");//htonl(INADDR_ANY); // 绑定0.0.0.0
        server_addr.sin_port = 10086;
        ret = bind(listen_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
        if (ret == -1) {
            // 绑定失败直接返回
            clear("server bind() error!", semmid, listen_sockfd);
            return;
        }

        // 3.监听客户端的连接
        ret = listen(listen_sockfd, 16);
        if (ret == -1) {
            // 监听失败直接返回
            clear("server listen() error!", semmid, listen_sockfd);
            return;
        }
        
        // 4.创建epoll文件描述符并初始化加入监听套接字文件描述符
        int epfd = epoll_create(100); // 创建epoll文件描述符
        if (ret == -1) {
            // 创建epoll文件描述符失败,直接返回
            clear("server epoll_create() error!", semmid, listen_sockfd);
            return;
        }
        // 创建epoll事件,需要包括检测事件和检测的文件描述符
        struct epoll_event listenfd_event;
        listenfd_event.data.fd = listen_sockfd; // 保存监听套接字数据
        listenfd_event.events = EPOLLIN; // 监控读事件
        // 添加监听套接字及其监听事件struct epoll_event
        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sockfd, &listenfd_event);
        if (ret == -1) {
            // 添加监听文件描述符到epoll文件描述符失败,直接返回
            clear("server epoll_ctl() error!", semmid, listen_sockfd);
            return;
        }
        // 创建保存客户端信息的结构体,可以使用链表结构
        struct CLIENTINFO {
            char ip[32]; // 保存连接客户端的IP地址
            unsigned short port; // 保存连接客户端的端口号port
            int communicate_sockfd; // 保存连接客户端的通信文件描述符
        };

        printf("main process:%d start sever listening(port:%d)..........
", getpid(), server_addr.sin_port);
        // 服务器启动成功,执行V操作释放启动子进程客户端
        sembuf sem_buf;
        sem_buf.sem_num = 0;
        sem_buf.sem_flg = 0; // 表示使用默认阻塞方式
        sem_buf.sem_op = +1;
        semop(semmid, &sem_buf, 1); // 执行V操作

        // 5.循环采用epoll_wait监听
        while (true) {
            ret = sigsetjmp(env_epoll, 1); // 保存服务器堆栈状态
            if (ret)
                break; // 出现错误,直接终止退出循环

            // 使用epoll_wait函数循环监听要观察的套接字文件描述符状态
            struct epoll_event already_fd[16]; // 保存就绪的文件描述符,假设一次最多传出16个
            ret = epoll_wait(epfd, already_fd, sizeof(already_fd)/sizeof(already_fd[0]), 1000); // 设置1s进行一次epoll检测
            // 根据epoll_wait函数的返回值判断状态
            if (ret == -1) {
                // epoll_wait系统调用被其他信号中断后会直接返回-1,errno被设置为EINTR,但select没错误,需要继续循环
                if (errno == EINTR)
                    continue;
                printf("server epoll_wait() error! errno=%d
", errno);
                break;
            }
            else if (ret == 0)
                continue; // 表示在超时时间内,没有观察的套接字就绪
            else {
                // 有监听的文件描述符就绪,进行遍历寻找就绪文件描述符,然后区分是服务器监听文件描述符或客户端通信文件描述符
                for (int i = 0; i < ret; i++)
                    // 判断当前就绪文件描述符是监听套接字、通信套接字
                    if (already_fd[i].data.fd == listen_sockfd) {
                        // 监听套接字,说明有客户端发起连接请求
                        struct sockaddr_in client_addr;
                        socklen_t len = sizeof(client_addr);
                        memset(&client_addr, 0, len);
                        // 接受客户端的连接
                        int communicate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
                        if (communicate_sockfd == -1)
                            continue; // 连接客户端失败,直接返回
                        else {
                            // 连接客户端成功,将该与客户端通信的文件描述符添加到epoll文件描述符中,实现监听
                            struct epoll_event clientfd_event;
                            clientfd_event.events = EPOLLIN; // 监听读取事件
                            // 创建保存客户端信息的结构体,必须采用堆区地址,因为其要求保存一个地址,栈区局部变量只在这里有效
                            CLIENTINFO* pclient_info = (struct CLIENTINFO*)malloc(sizeof(struct CLIENTINFO));
                            pclient_info->port = ntohs(client_addr.sin_port); // 保存客户端端口号port
                            pclient_info->communicate_sockfd = communicate_sockfd; // 保存客户端通信文件描述符
                            strncpy(pclient_info->ip, inet_ntoa(client_addr.sin_addr), sizeof(pclient_info->ip)); // 保存客户端IP地址
                            clientfd_event.data.ptr = pclient_info; // 保存客户端信息
                            // 将客户端通信的文件描述符加入到epoll文件描述符中,实现监听
                            ret = epoll_ctl(epfd, EPOLL_CTL_ADD, communicate_sockfd, &clientfd_event);
                            if (ret == -1) {
                                // 添加客户端通信文件描述符失败,直接关闭与客户端通信的文件描述符
                                close(communicate_sockfd);
                                continue;
                            }
                            printf("server connect client:%s:%d success, communicatefd:%d.
", pclient_info->ip, pclient_info->port, communicate_sockfd);
                        }
                    }
                    else {
                        // 通信文件描述符,说明有连接成功的客户端发送信息
                        char rdbuf[1024]; // 读缓冲区
                        memset(rdbuf, 0, sizeof(rdbuf));
                        // epoll_ctl加入时保存,还原客户端信息
                        struct CLIENTINFO* pclient_info = (struct CLIENTINFO*)already_fd[i].data.ptr; 
                        int communicate_sockfd = pclient_info->communicate_sockfd; // 客户端通信套接字
                        unsigned short client_port = pclient_info->port;           // 客户端端口号
                        char* client_ip = pclient_info->ip;                        // 客户端IPV4地址
                        // 读取客户端的发送的信息
                        int rdbyte = read(communicate_sockfd, rdbuf, sizeof(rdbuf));
                        // 根据信息的返回值判断读取状态
                        if (rdbyte == -1) {
                            // 读取信息出现错误,直接在epoll文件描述符中去除观察,并关闭与客户端通信套接字
                            epoll_ctl(epfd, EPOLL_CTL_DEL, communicate_sockfd, NULL); // 从epoll文件描述符中去除
                            close(communicate_sockfd); // 关闭文件描述符
                            // 释放保存客户端信息的堆区资源
                            delete pclient_info;
                            printf("server read() error! close communicate:%d, disconnect client:%s:%d.
", communicate_sockfd, client_ip, client_port);
                            continue;
                        }
                        else if (rdbyte == 0) {
                            // 客户端断开,服务器断开连接
                            epoll_ctl(epfd, EPOLL_CTL_DEL, communicate_sockfd, NULL); // 从epoll文件描述符中去除
                            close(communicate_sockfd); // 关闭文件描述符
                            // 释放保存客户端信息的堆区资源
                            delete pclient_info;
                            printf("cient:%s:%d disconnect, server disconnect! close communicatefd:%d.
", client_ip, client_port, communicate_sockfd);
                            continue;
                        }
                        else {
                            // 服务器成功读取到客户端数据
                            printf("server read client:%s:%d %d number data:%s
", client_ip, client_port, rdbyte, rdbuf);
                            // 模拟处理数据
                            sleep(1);
                            // 服务器将数据写回发给客户端
                            char wrbuf[128];
                            sprintf(wrbuf, "Hello server form client:%s:%d.", client_ip, client_port);
                            int wrbyte = write(communicate_sockfd, wrbuf, strlen(wrbuf));
                            // 根据返回值判断发送状态
                            if (wrbyte == -1) {
                                // 发送信息失败,直接断开连接,关闭套接字并停止监听
                                printf("server write() error! close communicate:%d, disconnect client:%s:%d.
", communicate_sockfd, client_ip, client_port);
                                epoll_ctl(epfd, EPOLL_CTL_DEL, communicate_sockfd, NULL); // 删除通信文件描述符
                                close(communicate_sockfd); // 关闭通信文件描述符
                                // 释放保存客户端信息的堆区资源
                                delete pclient_info;
                                continue;
                            }
                            else {
                                // 发送信息给客户端成功
                                printf("server send client:%s:%d %d number data.
", client_ip, client_port, wrbyte);
                                continue;
                            }
                        }
                    }
                    
            }
        }

        // 6.退出服务器,释放资源
        close(listen_sockfd); // 关闭监听文件描述符
        close(epfd);          // 关闭epoll文件描述符

        // 由于还是主进程,需要阻塞回收子进程
        int spid;
        while ((spid = waitpid(-1, NULL, 0)) > 0)
            printf("main process:%d recycle son process:%d.
", getpid(), spid);
        // 释放信号量
        semctl(semmid, 0, IPC_RMID);

        // 主进程服务器结束
    }
    else if (pid == 0) {
        // 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
        sembuf sem_buf;
        sem_buf.sem_num = 0;
        sem_buf.sem_op = -1; // P操作
        sem_buf.sem_flg = 0; // 默认阻塞方式
        semop(semmid, &sem_buf, 1); // 执行P操作

        // 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
        client(2, 1); // 创建2+1个客户端子进程,每个客户端只发送1次消息就退出,便于观察
    }
    else
        printf("fork() error!
");
}

3.IO多路复用方式——poll方式

        将poll方式放在最后是因为poll方式对比select方式和epoll方式没有优点,poll方式虽然相比于select方式可以突破最多观察1024个文件描述符的状态,但是select方式是跨平台的,在Windows、Linux、Mac下均可以使用,poll方式只能在Linux下使用,但是poll方式在linux下又不如epoll方式那样高效快速便捷,我的建议是了解poll方式的思想和实现方式即可,并了解对比select方式和epoll方式的优缺点和不同点即可,实际大概率使用不到

Poll方式介绍

        Poll是一种I/O多路复用技术,允许单个线程同时监控多个文件描述符的状态变化(如可读、可写或异常)。与轮询遍历相比,poll通过内核事件通知机制减少无效检查,适用于高并发场景。


Poll对比Select/Epoll的优缺点

优势
  1. 无描述符数量限制
    Select通常限制为1024个,而Poll基于链表存储,上限由系统内存决定。
  2. 更清晰的状态标识
    使用pollfd结构体独立保存每个fd的状态,避免Select的位图操作复杂性。
劣势
  1. 性能低于Epoll
    每次调用需传递整个fd集合,时间复杂度为$O(n)$;Epoll($O(1)$)仅处理活跃fd。
  2. 无状态追踪机制
    需手动维护fd集合,而Epoll内核自动维护就绪队列。

poll方式的核心函数

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 参数
    • fds:指向pollfd结构数组的指针
    • nfds:监控的fd数量
    • timeout:超时时间(毫秒),-1阻塞等待,0立即返回
  • 返回值
    成功返回就绪fd数量,失败返回-1

pollfd结构体

struct pollfd {
    int fd;         // 监控的文件描述符
    short events;   // 等待的事件(POLLIN、POLLOUT等)
    short revents;  // 实际发生的事件
};

poll方式实现高并发器示例代码(详细注释)

poll方式实现高并发服务器思想:

  1. 初始化监听
    创建TCP套接字并绑定端口,将server_fd加入poll监控队列。
  2. 事件循环
    poll阻塞直到至少一个fd就绪,通过revents字段识别事件类型。
  3. 连接管理
    • 新连接:accept后将其fd加入监控数组
    • 数据传输:读取后回显,若读失败则标记fd为无效
  4. 资源回收
    遍历移除无效fd,避免数组空洞。

完整代码如下:

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

#define MAX_FDS 1024
#define PORT 8080

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(PORT),
        .sin_addr.s_addr = INADDR_ANY
    };
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 5);

    struct pollfd fds[MAX_FDS];
    fds[0].fd = server_fd;
    fds[0].events = POLLIN; // 监听读事件(新连接)

    int nfds = 1; // 当前监控的fd数量

    while (1) {
        int ready = poll(fds, nfds, -1); // 阻塞等待事件
        if (ready < 0) {
            perror("poll error");
            exit(EXIT_FAILURE);
        }

        // 遍历所有监控的fd
        for (int i = 0; i < nfds; i++) {
            if (fds[i].revents & POLLIN) {
                if (fds[i].fd == server_fd) {
                    // 处理新连接
                    int client_fd = accept(server_fd, NULL, NULL);
                    fds[nfds].fd = client_fd;
                    fds[nfds].events = POLLIN;
                    nfds++;
                    printf("New client connected: fd=%d
", client_fd);
                } else {
                    // 处理客户端数据
                    char buffer[1024];
                    ssize_t len = read(fds[i].fd, buffer, sizeof(buffer));
                    if (len <= 0) {
                        close(fds[i].fd); // 连接关闭
                        fds[i].fd = -1;   // 标记为无效
                    } else {
                        write(fds[i].fd, buffer, len); // 回显数据
                    }
                }
            }
        }

        // 清理无效fd
        for (int i = 0; i < nfds; i++) {
            if (fds[i].fd == -1) {
                for (int j = i; j < nfds - 1; j++) {
                    fds[j] = fds[j + 1];
                }
                nfds--;
                i--;
            }
        }
    }
    return 0;
}

3种IO多路复用方式select、epoll、poll的总结对比

特性selectpollepoll
基本原理轮询所有被监控的文件描述符集合轮询所有被监控的文件描述符集合(链表结构)基于事件的就绪通知(回调机制)
文件描述符限制FD_SETSIZE 限制(通常为 1024)无硬编码限制(仅受系统资源限制)无硬编码限制(仅受系统资源限制)
效率(大量连接)O(n)O(n)O(1)
触发模式仅支持水平触发仅支持水平触发支持水平触发和边缘触发
跨平台几乎所有平台大部分平台(如 BSD)Linux 特有
内存使用固定大小的位图动态数组红黑树 + 就绪链表
使用复杂度接口复杂,需每次重置集合接口较简单接口较简单,但需理解触发模式

各自特点总结

  1. select:

    • 特点: 最古老、最广泛支持。使用位图(fd_set)表示文件描述符集合,有固定的最大数量限制。需要每次调用前重置监控集合。效率在连接数多时低下,因为需要线性扫描整个集合。
    • 优点: 可移植性极佳。
    • 缺点: 文件描述符数量限制、效率低、接口使用繁琐。
  2. poll:

    • 特点: 使用 pollfd 结构体数组来表示文件描述符集合,突破了 select 的数量限制(仅受系统资源限制)。接口比 select 稍简洁。效率同样在连接数多时低下($O(n)$ 遍历)。
    • 优点: 无文件描述符硬限制、接口相对清晰。
    • 缺点: 效率仍随连接数线性下降、需要遍历整个数组。
  3. epoll:

    • 特点: Linux 特有高性能机制。使用红黑树管理监控的文件描述符,使用就绪链表返回事件。采用事件驱动(回调)机制,当文件描述符就绪时内核会通知,避免了无效的遍历。支持两种触发模式(LT/ET)。效率高(接近 $O(1)$),尤其适合大量连接。
    • 优点: 超高效率、无文件描述符数量限制、内存占用优化、支持边缘触发。
    • 缺点: 仅适用于 Linux 系统、接口相对复杂(需理解 LT/ET)。

适用场景

  1. select:

    • 适用: 对可移植性要求极高、需要兼容旧系统、监控的文件描述符数量非常少(远小于 1024)的场景。教学或演示基本概念时。
    • 不适用: 需要处理大量并发连接的高性能服务器。
  2. poll:

    • 适用: 需要监控的文件描述符数量超过 select 限制但又不是特别巨大(几百到几千)、对性能要求不是极端苛刻、且需要较好可移植性(非仅限 Linux)的场景。
    • 不适用: 需要处理数万甚至数十万并发连接、追求极致性能的场景。
  3. epoll:

    • 适用: 运行在 Linux 系统上、需要构建高性能网络服务器(如 Web Server, 游戏服务器)、处理大量并发连接(成千上万)的场景。是 Linux 下高并发网络编程的首选。
    • 不适用: 需要跨平台支持(如 Windows)的场景(Windows 有 IOCPkqueue (BSD))。

简单来说:

  • 教学或兼容旧系统/少量连接: select
  • 跨平台/中等数量连接: poll
  • Linux 平台/高性能/海量连接: epoll

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

搜索文章

Tags

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