小程序自助建站小型网站建设公司价格低
- 作者: 五速梦信息网
- 时间: 2026年04月20日 07:10
当前位置: 首页 > news >正文
小程序自助建站,小型网站建设公司价格低,濮阳网站建设0393seo,顺德网站开发招聘前言#xff1a;
在linux系统中#xff0c;实际上所有的 I/O 设备都被抽象为了文件这个概念#xff0c;一切皆文件#xff0c;磁盘、网络数据、终端#xff0c;甚至进程间通信工具管道 pipe 等都被当做文件对待。
在了解多路复用 select、poll、epoll 实现之前#xff…前言
在linux系统中实际上所有的 I/O 设备都被抽象为了文件这个概念一切皆文件磁盘、网络数据、终端甚至进程间通信工具管道 pipe 等都被当做文件对待。
在了解多路复用 select、poll、epoll 实现之前我们先简单回忆复习以下两个概念
一、什么是多路复用
多路多个 socket 网络连接。复用复用一个线程使用一个线程来检查多个文件描述符socket的就绪状态。多路复用主要有三种技术select、poll、epoll。epoll 是最新的也是目前最好的多路复用技术。
二、五种 I/O 模型 blocking I/O - 阻塞I/Onon-blocking I/O - 非阻塞I/Osignal-driven I/O - 信号驱动I/Oasynchronous I/O - 异步I/OI/O multiplexing - I/O多路复用1. 阻塞 I/O 模型
进程/线程在从调用 recvfrom() 开始到它返回的整段时间内是被阻塞的recvfrom() 成功返回后应用进程/线程开始处理数据报。
recvfrom() 是一个系统调用函数用于从一个已连接或未连接的套接字socket接收数据。函数原型如下
ssize_t recvfrom(int sockfd, //套接字文件描述符void *buf, //指向接收数据的缓冲区size_t len, //缓冲区的大小int flags, //可选的标志参数用于控制接收操作的行为struct sockaddr *src_addr, //用于存储发送方的地址信息socklen_t *addrlen //src_addr 的长度);recvfrom() 在调用时会阻塞直到有数据到达或发生错误。当有数据到达时它将数据读取到指定的缓冲区中并填充发送方的地址信息到 src_addr 参数中。如果套接字是已连接的src_addr和 addrlen 参数可以设置为 NULL。
主要特点 进程阻塞挂起不消耗CPU资源能及时响应每个操作。实现难度低。适用并发量小的网络应用开发不适用并发量大的应用。因为一个请求IO会阻塞进程所以每请求分配一个处理进程线程去响应系统开销大。
- 非阻塞 I/O 模型
进程发起 I/O 系统调用后如果内核缓冲区没有数据需要到 I/O 设备中读取进程返回一个错误而不会被阻塞如果内核缓冲区有数据内核就会把数据返回进程。
主要特点 进程轮询重复调用消耗CPU的资源。实现难度低、开发应用相对阻塞IO模式较难。适用并发量较小、且不需要及时响应的网络应用开发。
- 信号驱动 I/O 模型
当进程发起一个 I/O 操作会向内核注册一个信号处理函数然后进程返回不阻塞当内核数据就绪时会发送一个信号给进程进程便在信号处理函数中调用 I/O 读取数据。与阻塞式 I/O 或非阻塞式 I/O 模型不同信号驱动 I/O 模型允许应用程序在进行 I/O 操作时继续执行其他任务而不需要显式地轮询或阻塞等待 I/O 操作的完成。 工作流程 应用程序通过调用 sigaction() 来注册一个 信号处理函数Signal Handler用于处理特定的 I/O 相关信号如 SIGIO。将 I/O 文件描述符 设置为信号驱动模式通常使用 fcntl() 并设置 F_SETOWN 标志将文件描述符的拥有者设置为当前进程。这样当 I/O 事件发生时内核将向该进程发送相应的信号。当 I/O 事件如数据到达发生时操作系统将为相应的文件描述符生成一个信号通常是 SIGIO并将其发送给拥有者进程。拥有者进程接收到信号后会调用事先注册的信号处理函数进行相应的处理。在信号处理函数中可以执行读取数据、写入数据等操作。信号处理函数执行完毕后应用程序可以继续执行其他任务而不需要显式地等待 I/O 操作的完成。 主要特点 实现、开发应用难度大。需要合理处理信号处理函数和信号同步的问题。适用于需要同时处理多个 I/O 事件的情况如网络编程中的异步处理。
- 异步 I/O 模型
当进程发起一个 I/O 操作进程返回不阻塞但也不能返回结果内核把整个 I/O 处理完后会通知进程结果。如果 I/O 操作成功则进程直接获取到数据。 工作原理 应用程序调用系统调用函数如 aio_read、aio_write 等发起异步 I/O 操作。这些函数通常是系统提供的异步 I/O 接口函数。在发起异步 I/O 操作时应用程序还需要提供一个 回调函数该函数将在 I/O 操作完成时被调用。异步 I/O 操作被提交给操作系统或 I/O 子系统进行处理。操作系统将负责执行实际的 I/O 操作并在操作完成后触发相应的事件。当 I/O 操作完成时操作系统将调用之前注册的回调函数并将操作的结果传递给回调函数。在回调函数中应用程序可以处理操作的结果例如读取已接收的数据、处理错误情况等。应用程序可以继续执行其它任务而无需等待 I/O 操作的完成。 主要特点 不阻塞数据一步到位。需要操作系统的底层支持LINUX 2.5 版本内核首现2.6 版本产品的内核标准特性。实现、开发应用难度大。非常适合高性能高并发应用。
- I/O 复用模型
大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中操作系统会将 I/O 的数据缓存在文件系统的页缓存page cache。也就是说数据会先被拷贝到操作系统内核的缓冲区中然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间中。这种做法的缺点就是需要在应用程序地址空间和内核进行多次拷贝这些拷贝动作所带来的CPU以及内存开销是非常大的。
至于为什么不能直接让磁盘控制器把数据送到应用程序的地址空间中呢最简单的一个原因就是应用程序不能直接操作底层硬件。 总的来说IO分两阶段 1数据准备阶段 2内核空间复制回用户进程缓冲区阶段。如下图 工作原理 应用程序通过将多个 I/O 流如套接字注册到 I/O 复用机制中以便对这些流的状态进行监视。当应用程序调用 I/O 复用机制的函数如 select、poll 或 epoll时它会被阻塞直到至少一个注册的 I/O 满足指定的条件如可读、可写等。当有流满足指定条件时I/O 复用机制并返回并通知应用程序哪些流满足条件。应用程序可以遍历返回的流集合检查每个流的状态并进行相应的处理。通常应用程序会使用条件判断语句来确定流是可读还是可写并执行相应的读取或写入操作。应用程序可以继续执行其他任务或再次调用 I/O 复用机制的函数以便继续监视 I/O 流的状态变化。 主要特点 使用单个系统调用来监视多个 I/O 流的状态而不是针对每个流进行阻塞式的等待。有效减少系统调用次数和上下文切换开销。提高应用程序的并发性和响应性。
三、I/O 多路复用之select、poll、epoll详解 目前支持 I/O 多路复用的系统调用有 selectpselectpollepoll。与多进程和多线程技术相比I/O 多路复用技术的最大优势是系统开销小系统不必创建进程/线程也不必维护这些进程/线程从而大大减小了系统的开销。 I/O 多路复用就是通过一种机制一个进程可以监视多个描述符一旦某个描述符就绪一般是读就绪或者写就绪能够通知程序进行相应的读写操作。但 selectpollepoll 本质上都是 同步I/O因为他们都需要在读写事件就绪后自己负责进行读写也就是说这个读写过程是阻塞的而 异步I/O 则无需自己负责进行读写异步I/O的实现会负责把数据从内核拷贝到用户空间。 select select函数 监视的文件描述符分3类分别是 writefds、readfds 和 exceptfds。 当用户线程调用 select 的时候select 将需要监控的 readfds集合 拷贝到内核空间假设监控的仅仅是 socket可读。然后遍历自己监控的 skb(SocketBuffer)挨个调用 skb 的 poll 逻辑以便检查该 socket 是否有可读事件遍历完所有的 skb 后。如果没有任何一个 socket 可读那么 select 会调用schedule_timeout 进入 schedule 循环使得线程进入睡眠。如果在 timeout 时间内某个 socket 上有数据可读了或者等待timeout 了则调用 select 的线程会被唤醒接下来 select 就是遍历监控的集合挨个收集可读事件并返回给用户了。 相应的伪码如下 int select(int nfds, //监控的文件描述符集里最大文件描述符1fd_set *readfds, //监控读数据到达文件描述符集合fd_set *writefds, //监控写数据到达文件描述符集合fd_set *exceptfds, //监控异常发生到达文件描述符集合struct timeval timeout //定时阻塞监控时间3种情况1.NULL永远等下去。// 2.设置timeval等待固定时间// 3.设置timeval里时间均为0检查描述字后立即返回轮询 );//—————-select服务端伪码——————— //首先一个线程不断接受客户端连接并把socket文件描述符放到一个list里 while(1){connfd accept(listenfd);fcntl(connfd, F_SETFL, O_NONBLOCK);fdlist.add(connfd); } / select函数还是返回刚刚提交的list应用程序依然列出所有的fd只不过操作系统会将准备就绪的文件描述符做上标识 用户层将不会再有无意义的系统调用开销。 */ struct timeval timeout; int max 0; //用于记录最大的fd在轮询中时刻更新即可 //初始化比特位 FD_ZERO(read_fd); while(1){//阻塞获取每次需要把fd从用户态拷贝到内核态nfds select(max 1, read_fd, write_fd, NULL, timeout);//每次需要遍历所有fd判断有无读写事件发生for(int i 0; i max nfds; i){//只读已就绪的文件描述符不用过多遍历if(i listenfd){//这里处理accept事件FD_SET(i, read_fd); //将客户端socket加入到集合中}if(FD_ISSET(i, read_fd)){//这里处理read事件}} }下面是 select 工作原理的动图 select工作图 通过上面的select逻辑过程分析相信大家都意识到select存在三个问题 每次调用 select都需要把被监控的 fds 集合从用户态空间拷贝到内核态空间高并发场景下这样的拷贝会使得消耗的资源是很大的。能监听端口的数量有限单个进程所能打开的最大连接数由 FD_SETSIZE 宏定义监听上限就等于 fds_bits 位数组中所有元素的二进制位总数其大小是32个整数的大小在32位的机器上大小就是32同理64位机器上为64当然我们可以对宏 FD_SETSIZE 进行修改然后重新编译内核但是性能可能会受到影响一般该数和系统内存关系很大具体数目可以 cat /proc/sys/fs/file-max 察看。32位机默认1024个64位默认2048。被监控的 fds集合 中只要有一个有数据可读整个 socket集合 就会被遍历一次调用 sk 的 poll 函数收集可读事件由于仅关心是否有数据可读这样一个事件数据的到来是异步的于是只能挨个遍历每个socket来收集可读事件了。 poll poll 的实现和 select 非常相似只是描述 fd 集合的方式不同。针对 select 遗留的三个问题中问题(2)是fd限制问题问题(1)和(3)则是性能问题poll 使用 pollfd结构 而不是 select 的 fd_set结构这就解决了 select 的问题(2)fds集合大小限制问题。 但 poll 和 select 同样存在一个性能缺点就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间而不论这些文件描述符是否就绪它的开销随着文件描述符数量的增加而线性增大。 下面是 poll 的函数原型 int poll(struct pollfd *ufds, unsigned int nfds, int timeout); struct pollfd{int fd; //文件描述符short events; //监控的事件short revents; //监控事件中满足条件返回的事件 };//—————–poll服务端实现伪码——————— struct pollfd fds[POLL_LEN]; unsigned int nfds 0; fds[0].fd server_sockfd; fds[0].events POLLIN | POLLPRI; nfds; while(1){res poll(fds, nfds, -1);if(fds[0].revents (POLLIN | POLLPRI)){//执行accept并加入fds中nfdsif(–res 0) continue;}//循环之后的fdsif(fds[i].revents (POLLIN | POLLERR)){//读操作或处理异常等if(–res 0) continue;} }poll 相比于 select 的优点使用了 pollfd结构使得 poll 支持的 fds 集合限制远大于 select 的1024。 由于 poll 基于链表存储无最大连接数限制所以有如下缺点1大量描述符数组被整体复制于用户态和内核态的地址空间之间以及个别描述符就绪触发整体描述符集合的遍历的低效问题。2poll 随着监控的 socket 集合的增加性能线性下降使得 poll 也并不适合用于大并发场景。3若报告了 fd 后未被处理下次 poll 时会再次报告该 fd。 epoll基于Linux2.4.5 epoll 模型将主动轮询改为被动通知当有事件发生时被动接收通知。所以 epoll 模型注册套接字后主程序可做其它事情当事件发生时接收到通知后再去处理。可理解为event pollepoll 会把哪个流发生哪种 I/O 事件通知我们。所以 epoll 是事件驱动每个事件关联 fd此时我们对这些流的操作都是有意义的复杂度也降到 O ( 1 ) O(1) O(1)。 创建一个 epoll 的句柄size 表明要监听的 fd 数目。这个参数不同于 select() 中的第一个参数给出最大监听的fd1的值。需要注意的是当创建好 epoll 句柄后它就是会占用一个 fd 值在 linux 下如果查看 /proc/进程id/fd/是能够看到这个fd的所以在使用完 epoll 后必须调用 close() 关闭否则可能导致 fd 被耗尽。 epoll 的接口非常简单一共就三个函数 epoll_create创建一个 epoll 句柄epoll_ctl向 epoll 对象中添加/修改/删除要管理的连接epoll_wait等待其管理的连接时的 I/O 事件 epoll_create 函数 int epoll_create(int size);功能生成一个 epoll 专用的文件描述符。参数 size表明内核监听的文件描述符数目。并不是限制 epoll 所能监听的描述符最大个数只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 后size 参数可以填大于0的任意值。返回值成功则返回 epoll 专用的文件描述符失败则返回 -1。 epoll_create 的源码实现 asmlinkage int sys_epoll_create(int maxfds){int error -EINVAL, fd;unsigned long addr;struct inode *inode;struct file *file;struct eventpoll ep;//eventpoll接口中不可能存储超过MAX_FDS_IN_EVENTPOLL的fdif (maxfds MAX_FDS_IN_EVENTPOLL)goto eexit_1; /** Creates all the items needed to setup an eventpoll file. That is, a file structure, and inode and a free file descriptor./error ep_getfd(fd, inode, file);if (error)goto eexit_1; /** 调用去初始化eventpoll file. 这和open file operation callback一样因为 inside ep_getfd() we did what the kernel usually does before invoking* corresponding file open callback./error open_eventpoll(inode, file);if (error)goto eexit_2; / private_data 由open_eventpoll()设置 /ep file-private_data; / 分配页给event double buffer */error ep_do_alloc_pages(ep, EP_FDS_PAGES(maxfds 1));if (error)goto eexit_2; //创建event double buffer的一个用户空间的映射以避免当返回events给调用者时内核到用户空间的内存复制down_write(current-mm-mmap_sem);addr do_mmap_pgoff(file, 0, EP_MAP_SIZE(maxfds 1), PROT_READ,MAP_PRIVATE, 0);up_write(current-mm-mmap_sem);error PTR_ERR((void *) addr);if (IS_ERR((void *) addr))goto eexit_2; return fd; eexit_2:sys_close(fd); eexit_1:return error; }epoll_ctl 每次注册新事件到 epoll 句柄中时在 epoll_ctl 中指定 EPOLL_CTL_ADD会把所有 fd 拷贝进内核而非在 epoll_wait 时重复拷贝。epoll 保证每个 fd 在整个过程中只会拷贝一次。 //成功则返回0失败则返回-1 int epoll_ctl(int epfd, //epoll专用的文件描述符epoll_create的返回值int op, //表示动作用三个宏来表示1.EPOLL_CTL_ADD注册新的fd到epfd中// 2.EPOLL_CTL_MOD修改已注册的fd的监听事件// 3.EPOLL_CTL_DEL从epfd中删除一个fdint fd, //需要监听的文件描述符struct epoll_event *event //内核要监听的事件类型);epoll_wait //成功则返回要处理的事件数目超时返回0失败返回-1 int epoll_wait(int epfd, //epoll专用的文件描述符epoll_create的返回值struct epoll_event * events, //内核要监听的事件类型int maxevents, //事件个数int timeout); //超时时间为-1时函数为阻塞epoll 不像 select/poll 每次都把当前文件流加入 fd 对应的设备等待队列而只在 epoll_ctl 时把当前文件挂一遍这一遍必不可少并为每个 fd 指定一个回调函数。 当设备就绪唤醒等待队列上的等待者时就会调用该回调函数而回调函数会把就绪 fd 加入一个就绪链表。epoll_wait 实际上就是在该就绪链表中查看有无就绪 fd。 函数实现伪代码如下 const int MAX_EVENT_NUMBER 10000; //最大事件数 // 设置句柄非阻塞 int setnonblocking(int fd) {int old_option fcntl(fd, F_GETFL);int new_option old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option; }int main(){// 创建套接字int nRet0;int m_listenfd socket(PF_INET, SOCK_STREAM, 0);if(m_listenfd0){printf(fail to socket!);return -1;}// struct sockaddr_in address;bzero(address, sizeof(address));address.sin_family AF_INET;address.sin_addr.s_addr htonl(INADDR_ANY);address.sin_port htons(6666);int flag 1;// 设置ip可重用setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, flag, sizeof(flag));// 绑定端口号int ret bind(m_listenfd, (struct sockaddr *)address, sizeof(address));if(ret0){printf(fail to bind!,errno :%d,errno);return ret;}// 监听连接fdret listen(m_listenfd, 200);if(ret0){printf(fail to listen!,errno :%d,errno);return ret;}// 初始化红黑树和事件链表结构rdlist结构epoll_event events[MAX_EVENT_NUMBER];// 创建epoll实例int m_epollfd epoll_create(5);if(m_epollfd-1){printf(fail to epoll create!);return m_epollfd;}// 创建节点结构体将监听连接句柄epoll_event event;event.data.fd m_listenfd;//设置该句柄为边缘触发数据没处理完后续不会再触发事件水平触发是不管数据有没有触发都返回事件event.events EPOLLIN | EPOLLET | EPOLLRDHUP;// 添加监听连接句柄作为初始节点进入红黑树结构中该节点后续处理连接的句柄epoll_ctl(m_epollfd, EPOLL_CTL_ADD, m_listenfd, event);//进入服务器循环while(1){int number epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);if (number 0 errno ! EINTR){printf( epoll failure);break;}for (int i 0; i number; i){int sockfd events[i].data.fd;// 属于处理新到的客户连接if (sockfd m_listenfd){struct sockaddr_in client_address;socklen_t client_addrlength sizeof(client_address);int connfd accept(m_listenfd, (struct sockaddr *)client_address, client_addrlength);if (connfd 0){printf(errno is:%d accept error, errno);return false;}epoll_event event;event.data.fd connfd;//设置该句柄为边缘触发数据没处理完后续不会再触发事件水平触发是不管数据有没有触发都返回事件event.events EPOLLIN | EPOLLRDHUP;// 添加监听连接句柄作为初始节点进入红黑树结构中该节点后续处理连接的句柄epoll_ctl(m_epollfd, EPOLL_CTL_ADD, connfd, event);setnonblocking(connfd);}else if (events[i].events (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){//服务器端关闭连接epoll_ctl(m_epollfd, EPOLL_CTL_DEL, sockfd, 0);close(sockfd);}//处理客户连接上接收到的数据else if (events[i].events EPOLLIN){char buf[1024]{0};read(sockfd,buf,1024);printf(from client :%s);// 将事件设置为写事件返回数据给客户端events[i].data.fd sockfd;events[i].events EPOLLOUT | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, events[i]);}else if (events[i].events EPOLLOUT){std::string response server response \n;write(sockfd,response.c_str(),response.length());// 将事件设置为读事件继续监听客户端events[i].data.fd sockfd;events[i].events EPOLLIN | EPOLLRDHUP;epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, events[i]);}//else if 可以加管道unix套接字等等数据}}}触发模式 EPOLL LT 和 EPOLL EF 两种 LT水平触发默认只要该 fd 还有数据可读每次 epoll_wait 都会返回它的事件提醒用户程序去处理。ET边缘触发高速无论 fd 中是否还有数据都只提示一次直到下次有数据流入前都不会提示。所以 ET 模式下read 一个 fd 时一定要把它的 buffer 读完即读到 read 返回值小于请求值或遇到 EAGAIN 错误稍后重试。 epoll 使用 事件 就绪通知方式通过 epoll_ctl 注册 fd一旦该 fd 就绪内核就会采用类似回调机制激活该 fdepoll_wait 便可收到通知。 ET 的意义 若用 LT系统中一旦有大量无需读写的就绪文件描述符它们每次调用 epoll_wait 都会返回这大大降低处理程序检索自己关心的就绪文件描述符的效率。 而采用 ET当被监控的文件描述符上有可读写事件发生时epoll_wait 会通知处理程序去读写。若这次没有把数据全部读写完(如读写缓冲区太小)则下次调用 epoll_wait 时它不会通知你即只会通知你一次直到该文件描述符上出现第二次可读写事件才通知你。这比水平触发效率高系统不会充斥大量你不关心的就绪文件描述符。 优点 无最大并发连接的限制能打开的 fd 上限远大于10241G内存能监听约10万个端口。效率提升不是轮询不会随 fd 数目增加而效率下降。只有活跃可用的 fd 才会调用 callback 函数即 epoll 最大优点在于它只关心“活跃”连接而跟连接总数无关。内存拷贝利用 mmap() 文件映射内存加速与内核空间的消息传递即epoll使用mmap减少复制开销。epoll 通过内核和用户空间共享一块内存而实现。 缺点 在连接数少且都十分活跃情况下select 和 poll 性能都可能比 epoll 好因为 epoll 通知机制需要很多函数回调。epoll 是 Linux 所特有的而 select 是 POSIX 所规定一般 os 均有实现。 四、总结 selectpollepoll 都是 I/O 多路复用机制即能监视多个 fd一旦某 fd 就绪读或写就绪能够通知程序进行相应读写操作。 但 selectpollepoll 本质都是同步I/O因为他们都需在读写事件就绪后自己负责进行读写即该读写过程是阻塞的而异步 I/O 则无需自己负责进行读写异步 I/O 实现会负责把数据从内核拷贝到用户空间。 selectpoll 需自己主动不断轮询所有 fd 集合直到设备就绪期间可能要睡眠和唤醒多次交替。而 epoll 其实也需调用 epoll_wait 不断轮询就绪链表期间也可能多次睡眠和唤醒交替但它是设备就绪时调用回调函数把就绪 fd 放入就绪链表并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替但 select 和 poll 在“醒着”时要遍历整个 fd 集合而 epoll 在“醒着”的时候只需判断就绪链表是否为空节省大量CPU时间这就是回调机制带来的性能提升。 selectpoll 每次调用都要把 fd 集合从用户态往内核态拷贝一次且要把当前文件往设备等待队列中挂一次而 epoll 只要一次拷贝且把当前文件往等待队列上挂也只挂一次在 epoll_wait 开始注意这里的等待队列并不是设备等待队列只是一个 epoll 内部定义的等待队列这也能节省不少开销。 参考 一文搞懂select、poll和epoll区别IO多路复用——深入浅出理解select、poll、epoll的实现
- 上一篇: 小程序转换成网页seo外链推广工具下载
- 下一篇: 小程序做跳转微网站建设领域工人管理网站
相关文章
-
小程序转换成网页seo外链推广工具下载
小程序转换成网页seo外链推广工具下载
- 技术栈
- 2026年04月20日
-
小程序源码购买吴江seo
小程序源码购买吴江seo
- 技术栈
- 2026年04月20日
-
小程序网站建设制作手表网站功能设计
小程序网站建设制作手表网站功能设计
- 技术栈
- 2026年04月20日
-
小程序做跳转微网站建设领域工人管理网站
小程序做跳转微网站建设领域工人管理网站
- 技术栈
- 2026年04月20日
-
小程序做网站个人社保缴费比例是多少
小程序做网站个人社保缴费比例是多少
- 技术栈
- 2026年04月20日
-
小创业公司网站怎么做网站建设为什么这么贵
小创业公司网站怎么做网站建设为什么这么贵
- 技术栈
- 2026年04月20日






