Select单进程非阻塞TCP echo服务器
Select单进程非阻塞TCP echo服务器
1. select 描述
#include <sys/select.h>
#include <sys/time.h>
int select( int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
返回:正确:就绪描述符数目;超时: 0;出错: -1
参数说明:
timeout: 内核等待指定描述符集合中的任意一个就绪的时间。
struct timeval {
long tv_sec; //seconds
long tv_usec; //microseconds
};
timeout = NULL: 永远等待下去:仅在有一个描述符准备好I/O才返回。
timeout != NULL: 等待timeval中指定的时间返回。
timeout != NULL, timeval == 0 : 不等待:检查描述符后立即返回。这称为轮循(polling);
前两种情况会被信号中断,并从信号处理程序返回。
timeout为const表示函数返回时不会修改timeval,但是有的linux版本会修改timval值。
考虑到可移植性,每次都应对timeout进行初始化。
readset, writeset, exceptset:指定我们让内核测试读写异常条件的描述符:
可读:
(1) 套接字缓冲区数据字数 >= 套接字接受缓冲区低水位标记的当前大小。 不会阻塞,返回>0的值(准备好读入的数据)。
可用SO_RCVLOWAT套接字选项设置标志,TCP和UDP默认1。所以read时可能返回1字节数据,需要反复读。
(2) 接受到FIN,读操作不阻塞返回0(EOF)。
(3) 监听套接字已完成的连接数不为0,新的连接到达。accept不阻塞。
(4) 有一个套接字错误要处理。读不阻塞返回-1。设置errno
可写:
(1) 发送缓冲区可用空间字节数 >= 最低水位。写不阻塞返回>0(例如:传输层接受的字节数)。 TCP和UDP最低水位2048。
(2) 连接的写半部关闭,对这样的套接字写产生SIGPIPE信号。
(3) 非阻塞的connect套接字已建立连接,或者connect已失败告终。
(4) 有一个套接字错误带处理。写不阻塞返回-1.设置errno。
异常:带外数据到达。
maxfdp1: 指定待测试的描述符个数,为最大描述符 + 1, 从0开始。
2. select 宏和相关说明
select使用描述符集fd_set,通常为一个整数数组,其中每个整数中的每一位对应一个描述符。
4个宏设置fd_set:
void FD_ZERO(fd_set *fdset); //清空fdset中所有位
void FD_SET(int fd, fd_set *fdset); //fd_set中fd位置位
void FD_CLR(int fd, fd_set *fdset); //fd_set中fd位复位
void FD_ISSET(int fd, fd_set *fdset); //fd_set中fd是否置位
select中的readset,writeset,exceptset = NULL,表示我们不关心这个。select会修改这三个描述符集和,时值——结果参数。
调用select时指定我们关心的描述符值,返回时指示那些已经就位,未就位的置0。因此每次调用select时都需要把我们关心的描述符置1。
3. select 缺陷
#include <sys/types.h> 定义了FD_SETSIZE,通常为1024。当描述符超出时,可用poll解决。
通过修改内核的FD_SETSIZE大小,需要重新编译内核,可能带来扩展性问题,不推荐使用。
4. 一个echo服务器程序示例
#include "unp.h" int main(int argc, char **argv) { if (argc != 2) { err_quit("Usage: a.out <#port>"); } int listenfd = Tcp_listen(NULL, argv[1], NULL); int flags = Fcntl(listenfd, F_GETFL, 0); Fcntl(listenfd, F_SETFL, flags | O_NONBLOCK); //最大文件描述符 int maxfd = listenfd; //连接的客户端描述符 int client[FD_SETSIZE]; memset(client, -1, FD_SETSIZE); //client已使用的描述符最大下标 int maxi = -1; //要监听的描述符集合 fd_set allset; FD_ZERO(&allset); FD_SET(listenfd, &allset); for (;;) { //select会修改描述符集合,每次重新初始化 fd_set rset = allset; DPRINTF("Start to select()"); //select阻塞到可读 int nready = Select(maxfd + 1, &rset, NULL, NULL, NULL); //监听描述符可读 if (FD_ISSET(listenfd, &rset)) { DPRINTF("Start to accept a connection"); //int connfd = Accept(listenfd, NULL, NULL); //非阻塞方式忽略若干错误 int connfd = accept(listenfd, NULL, NULL); DPRINTF("connfd %d", connfd); if (connfd < 0) { #ifdef EPROTO DPRINTF("EPROTO"); if (errno == EINTR || errno == EWOULDBLOCK || errno == ECONNABORTED || errno == EPROTO) { #else if (errno == EINTR || errno == EWOULDBLOCK || errno == ECONNABORTED) { #endif continue; } else { err_sys("accept() failed"); } } DPRINTF("Accept a connection"); //设置非阻塞 int flags = Fcntl(connfd, F_GETFL, 0); Fcntl(connfd, F_SETFL, flags | O_NONBLOCK); int i = 0; while (i < FD_SETSIZE) { if (client[i] < 0) { client[i] = connfd; break; } ++i; } if (i == FD_SETSIZE) { err_quit("Too many client"); } FD_SET(connfd, &allset); if (connfd > maxfd) { maxfd = connfd; } if (i > maxi) { maxi = i; } //检测是否还有准备好的连接 if (--nready <= 0) { continue; } } //end if(FD_ISSET) //循环检测已连接的客户端 DPRINTF("maxi = %d", maxi); for (int i = 0; i <= maxi; ++i) { int sockfd = client[i]; if (sockfd < 0) { continue; } DPRINTF("client[]: i %d, sockfd %d", i, sockfd); if (FD_ISSET(sockfd, &rset)) { char line[MAXLINE]; int n; again: while ((n = read(sockfd, line, MAXLINE)) > 0) { Write(sockfd, line, n); } if (n < 0 && errno == EINTR) { goto again; } else if (n < 0 && errno == EWOULDBLOCK) { ; } else if (n <= 0) { //0: FIN, close by client; -1: RST, error Close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1; DPRINTF("read n = %d", n); if (n < 0) { perror("n < 0"); } } } if (--nready <= 0) { break; } } //end for(0..maxi) } //end for(;;) return 0; }