UNP——第六章,多路转接IO——select
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
1.函数介绍
nfds
最大文件描述符+1
通过告诉内核最多需要检查的文件描述符数量,以提高效率,(否则内核需要检查所有的文件描述符)
至于为什么是最大文件描述符值+1,表示 需要检查的文件描述符的数量,原因是 文件描述符从0开始,而数量从1 开始。
readfds, writefds, exceptfds
都是传入传出参数
通过位掩码的方式表示监听的文件描述符
void FD_CLR(int fd, fd_set *set); int FD_ISSET(int fd, fd_set *set); void FD_SET(int fd, fd_set *set); void FD_ZERO(fd_set *set);
timeout
可以选择:
一直阻塞,直到有事件触发
阻塞一段时间,直到有事件触发
不阻塞,立即返回
阻塞可能被信号处理中断,而timeout不是传出参数,所以timeout不会记录剩余的等待时间,而是使用上次的值调用。
如果需要剩余的等待时间,可以在select调用前后记录系统时间以计算。
2. 事件触发的情况
3. 改写客户端
基于select的 str_cli
客户端套接字可能的事件如下
void str_cli(FILE *fp, int sockfd) { int maxfdp1; fd_set rset; char sendline[MAXLINE], recvline[MAXLINE]; FD_ZERO(&rset); for ( ; ; ) { FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1; Select(maxfdp1, &rset, NULL, NULL, NULL); if (FD_ISSET(sockfd, &rset)) { /* socket is readable */ if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */ if (Fgets(sendline, MAXLINE, fp) == NULL) return; /* all done */ Writen(sockfd, sendline, strlen(sendline)); } } }
改进
(1)批量输入
因为程序采用 停等方式,如果采用交互输入,一个RTT的情况如下
可见只用了管道的 1/8
如果使用文件重定向的方式运行程序,那么会批量输入程序,但运行结果出错,输出文件小于输入文件。
考虑下面情况
当运行到时刻8时,客户端已经read完了文件,所以会close,造成套接字进入半关闭,
如此服务器发送的数据,客户端就无法接受,导致数据丢失。
半关闭
单方面调用close后,
主动关闭方进入 FIN_WAIT_2, 不能进行读写操作
被动关闭方进入 CLOSE_WAIT,能进行读写操作,不过read返回0,write的数据不能传递给对端的应用层(在对端的TCP层被接受后丢弃)。
解决方法是:
发送FIN,告诉对方自己已经完成了数据发送,但是仍然保持套接字描述符打开以便读取,
使用 shutdown实现
(2)缓冲和 select
select 关于读写操作的事件触发是按照read/write的情况,即不带缓冲的情况,
如果使用 stdio或者自定义的缓冲操作容易造成错误,
如,使用 fgets,当 select 触发后,fgets会尽可能读数据到缓冲,可能读了多行输入,但是一次只返回一行,
于是select触发一次,收到多行输入,但是只处理了一行输入。
解决方法:
避免select和缓冲io合用。
shutdown函数
为了实现,客户端关闭后,仍能接受对方的输入
int shutdown(int sockfd, int how);
howto
SHUT_RD
关闭读,丢弃套接字接受缓冲区所有数据,所有新来的对端数据被悄悄接受,回复,并丢弃
SHUT_WR
关闭写,套接字发送缓冲区现有数据正常发送,并发送FIN,进程不能对该套接字进行写操作
SHUT_RDWR
相当于分别调用 SHUT_RD,和SHUT_WR
修订版
void str_cli(FILE *fp, int sockfd) { int maxfdp1, stdineof; fd_set rset; char buf[MAXLINE]; int n; stdineof = 0; FD_ZERO(&rset); for ( ; ; ) { if (stdineof == 0) FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1; Select(maxfdp1, &rset, NULL, NULL, NULL); if (FD_ISSET(sockfd, &rset)) { /* socket is readable */ if ( (n = Read(sockfd, buf, MAXLINE)) == 0) { if (stdineof == 1) return; /* normal termination */ else err_quit("str_cli: server terminated prematurely"); } Write(fileno(stdout), buf, n); } if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */ if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) { stdineof = 1; Shutdown(sockfd, SHUT_WR); /* send FIN */ FD_CLR(fileno(fp), &rset); continue; } Writen(sockfd, buf, n); } } }
select 实现服务器
通过select ,实现单个进程检测多个套接字。
思考需要的数据结构,由于单进程要维护多个客户端连接,即需要区分多个客户端,客户端的区分使用对应套接字,所以需要一个维护已连接客户端的文件描述符数组。
另外,由于使用select,监控read事件,所以需要一个readfds数组。
client中,将未使用的置为-1.
rset中,前三个描述符被占用,fd3为监听套接字。
另外需要思考,添加客户端,和删除客户端,时对上面的数据结构的操作顺序。
对于添加客户端,
首先,已经知道的数据,accept获得 connfd,所以client[i] = connfd,FD_SET(connfd, &rset)
然后select需要 maxfd1 ,所以需要维护一个maxfd, if(connfd > maxfd) maxfd = connfd;
对于删除客户端
首先,已经知道的数据:FD_ISSET(fd, &rset) 获得 connfd,所以 if(client[i] == fd) client[i] = -1; FD_CLR(fd, &rset)
对于maxfd1,不方便处理,所以就不处理了,比较几乎不会影响效率。
/* include fig01 */ #include "unp.h" int main(int argc, char **argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; ssize_t n; fd_set rset, allset; char buf[MAXLINE]; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); maxfd = listenfd; /* initialize */ maxi = -1; /* index into client[] array */ for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; /* -1 indicates available entry */ FD_ZERO(&allset); FD_SET(listenfd, &allset); /* end fig01 */ /* include fig02 */ for ( ; ; ) { rset = allset; /* structure assignment */ nready = Select(maxfd+1, &rset, NULL, NULL, NULL); if (FD_ISSET(listenfd, &rset)) { /* new client connection */ clilen = sizeof(cliaddr); connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); #ifdef NOTDEF printf("new client: %s, port %d\n", Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL), ntohs(cliaddr.sin_port)); #endif for (i = 0; i < FD_SETSIZE; i++) if (client[i] < 0) { client[i] = connfd; /* save descriptor */ break; } if (i == FD_SETSIZE) err_quit("too many clients"); FD_SET(connfd, &allset); /* add new descriptor to set */ if (connfd > maxfd) maxfd = connfd; /* for select */ if (i > maxi) maxi = i; /* max index in client[] array */ if (--nready <= 0) continue; /* no more readable descriptors */ } for (i = 0; i <= maxi; i++) { /* check all clients for data */ if ( (sockfd = client[i]) < 0) continue; if (FD_ISSET(sockfd, &rset)) { if ( (n = Read(sockfd, buf, MAXLINE)) == 0) { /*4connection closed by client */ Close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1; } else Writen(sockfd, buf, n); if (--nready <= 0) break; /* no more readable descriptors */ } } } } /* end fig02 */
拒绝服务型攻击
对于上面这种单个服务进程处理多个客户端,可能由拒绝服务型攻击,
比如,如果服务器使用 ReadLine 与客户端通信,那么恶意客户端可能只输入一个字符,不输入回车,于是服务器就会阻塞在readline,导致无法为其他客户端服务。
解决方法是,(a)多进程或多线程处理客户端 (b)非阻塞IO(c)IO操作设置超时
pselect
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
pselect 是select的增强版,具体增强如下:
(1)使用 timespec ,代替timeval
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; and struct timespec { long tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ };
timespec.tv_nsec是纳秒级。而select只能设置到微秒级。
(2)可以设置pselect阻塞期间的 sigmask。这可解决如下问题
SIGINT的回调函数只设置全局变量 intr_flag,通过检测intr_flag决定是否处理handle_intr,
但是如果 SIGINT 发生在 if(intr_flag) 之后,select阻塞之前,则程序会永远阻塞。
if (intr_flag) handle_intr(); if ((nready = select()) < 0) { if (errno == EINTR) { if (intr_flag) handle_intr(); } }
可以使用pselect解决,在pselect之前阻塞 SIGINT,pselect阻塞时,放开 SIGINT
sigset_t newmask, oldmask, zeromask sigemptyset(&zeromask); sigemptyset(&newmask); sigaddset(&newmask, SIGINT); sigprocmask(SIG_BLOCK, &newmask, &oldmask); if (intr_flag) handle_intr(); if ((nready = pselect(..., &zeromask)) < 0) { if (errno == EINTR) { if (intr_flag) handle_intr(); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?