select 函数

2014-11-18 23:01:36   最后更新: 2015-01-11 20:18:24   访问数量:1512




该函数允许进程指示内核等待多个事件中的任何一个发生或是超时后再唤醒他

int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

 

定义于 sys/select.h 中,timeval 结构定义于 sys/time.h 中

返回就绪描述符数目,若超时返回0,出错返回-1

timeout

timeout 参数指示内核最多等待的时间,超时则会返回

timeout 有以下三种选择:

  1. 永远等待 -- 空指针
  2. 等待固定时间 -- 设置适当的秒数和微妙数
  3. 不等待(检查描述符后立即返回,即轮询) -- 将秒数和微妙数同时设置为 0

readset、writeset 和 exceptset

这三个参数分别指定需要内核测试的读、写和异常条件的描述符

如果我们对某类描述符不感兴趣,那么可以置为 NULL,如果全部置为 NULL,那么 select 函数就变成 sleep 函数了(是以微妙为单位的 sleep 函数)

 

目前支持的异常条件只有两个:

  1. 某个套接字的带外数据到达
  2. 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息

 

这三个参数既是传入参数又是返回值,当函数返回时,这三个参数中将会只保留我们关心的已就绪描述符,其他位均会被置为 0

描述符集 -- fd_set

fd_set 是由系统内核实现的一个描述符数组,定义在 sys/select.h 中,用户可以通过下列宏操作描述符集

void FD_ZERO(fd_set *fdset); void FD_SET(int fd, fd_set *fdset); void FD_CLR(int fd, fd_set *fdset); int FD_ISSET(int fd, fd_set *fdset);

 

  • 注意,fd_set 并不是一个结构体,而是一个数组,所以请如下方式创建:
fd_set fdset;

 

描述符集的初始化非常重要,否则可能发生不可预期的后果

maxfdp1 (max fd plus 1)

maxfdp1 参数指定了待测试的描述符个数,他的值是待测试的最大描述符+1

sys/select.h 一般会定义一个值为1024的宏 FD_SETSIZE

maxfdp1 存在的意义在于提高该函数的执行效率

读就绪条件

符合下列的任何一个条件,都会返回读就绪:

  1. 套接字接收缓冲区中的数据字节数大于接收缓冲区低水位标记(使用 SO_RCVLOWAT 套接字选项可以设置套接字低水位标记,TCP 与 UDP 套接字默认为 1)
  2. 该连接的读半部分关闭(接受了 FIN 的 TCP 连接)
  3. 对于监听套接字,已完成连接数不为 0
  4. 套接字错误待处理,这些待处理错误可以通过指定 SO_ERROR 套接字选项调用 getsockopt 获取并清除

写就绪条件

符合下列任何一个条件,都会返回写就绪

  1. 该套接字的发送缓冲区中的可用空间字节数大于套接字发送缓冲区低水位标记,或者套接字不需要连接(如 UDP 套接字),可以使用 SO_SNDLOCWAT 选项设置套接字低水位标记(TCP 与 UDP 套接字默认为 2048)
  2. 该连接的写半部关闭,对这样的套接字的写操作将产生 SIGPIPE 信号
  3. 使用非阻塞式 connect 的套接字已建立连接,或者 connect 已经失败
  4. 套接字错误待处理

异常条件

如果一个套接字存在带外数据或者让处于带外标记,那么他有异常条件待处理

总结

select 返回套接字就绪的条件
条件可读可写异常
有数据可读可读  
关闭连接的读一半可读  
给监听套接口准备好新连接可读  
有可用于写的空间 可写 
关闭连接的写一半 可写 
待处理错误可读可写 
TCP 带外数据  异常

之前我们实现的客户端-服务器TCP通讯的例子存在一个问题,那就是在套接字上发生某些事件时,客户可能正阻塞于fgets调用而无法及时获取到这些事件的发生

如果使用 select 调用,可以同时等待标准输入刻度或是套接字可读,以便同时处理这些事件的发生

 

客户端需要处理的三个条件如下:

  1. 如果对端 TCP 发送数据,那么该套接字变为可读,并且 read 返回一个大于 0 的值(即读入字节数)
  2. 如果对端 TCP 发送过来一个 FIN(通知对端进程终止),那么该套接字变为可读,并且 read 返回 0
  3. 如果对端 TCP 发送一个 RST(通知对端主机崩溃并重启),那么套接字变为可读,并且 read 返回 -1,errno 中包含相应的错误码

 

client

  • main.c
/* * author: liuzeyu * date: 2014-12-01 * file: main.c */ #include "function/function.h" int main () { int sockfd = 0; char serv_ip[] = "127.0.0.1"; sockfd = connect_serv(sockfd, serv_ip); str_cli (stdin, sockfd); return 0; }

 

  • function.h
/* * author: liuzeyu * date: 2014-12-01 * file: function.h */ #ifndef _CLIENT_FUNCTION_H_20141130_ #define _CLIENT_FUNCTION_H_20141130_ #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/time.h> #include <arpa/inet.h> #include <sys/select.h> #include <sys/socket.h> #include <netinet/in.h> #define MAXLINE 1024 #define SERV_PORT 8000 int max (int, int); void str_cli (FILE *, int); int connect_serv(int, char *); ssize_t writen (int, const void *, size_t); ssize_t readline (int, void *, size_t); #endif

 

  • function.c
/* * author: liuzeyu * date: 2014-12-01 * file: function.c */ #include "function.h" int connect_serv(int sockfd, char *serv_ip) { struct sockaddr_in servaddr; if ((sockfd = socket (AF_INET, SOCK_STREAM, 0)) <= 0) { perror ("socket error"); exit(-1); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, serv_ip, &servaddr.sin_addr.s_addr); if (connect (sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) { perror ("connect error"); exit(-1); } return sockfd; } void str_cli (FILE *fp, int sockfd) { int maxfdp1; fd_set rset; char sendline[MAXLINE], recvline[MAXLINE]; FD_ZERO (&rset); while (1) { FD_SET (fileno(fp), &rset); FD_SET (sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1; if (select(maxfdp1, &rset, NULL, NULL, NULL) < 0) { perror ("select error"); exit(-1); } if (FD_ISSET(sockfd, &rset)) { if (readline(sockfd, recvline, MAXLINE) == 0) { printf("str_cli: server terminated prematurely\n"); exit(-1); } fputs(recvline, stdout); } if (FD_ISSET(fileno(fp), &rset)) { if (fgets(sendline, MAXLINE, fp) == NULL) return; writen(sockfd, sendline, strlen(sendline)); } } } int max (int a, int b) { return a>b ? a : b; } ssize_t writen (int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ((nwritten = write (fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else { perror ("write error"); exit(-1); } } nleft -= nwritten; ptr += nwritten; } return n; } ssize_t readline (int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n=1; n<maxlen; n++) { again: if ((rc = read(fd, &c, 1)) == 1) { *ptr++ = c; if (c == '\n') break; } else if (rc == 0) { *ptr = 0; return n-1; } else { if (errno == EINTR) goto again; perror("read:"); exit(-1); } } *ptr = 0; return n; }

 

server

  • main.c
/* * author: liuzeyu * date: 2014-12-04 * file: main.c */ #include "function/function.h" int main () { int listenfd, connfd, sockfd, nready, maxfd, maxi, i, n; int client[FD_SETSIZE]; fd_set allset, rset; socklen_t clilen; struct sockaddr_in cliaddr; char buf[MAXLINE]; listenfd = get_listenfd(); maxfd = listenfd; maxi = -1; for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; FD_ZERO(&allset); FD_SET(listenfd, &allset); while (1) { rset = allset; nready = select(maxfd+1, &rset, NULL, NULL, NULL); if (nready < 0) { perror("select"); exit(-1); } if (FD_ISSET(listenfd, &rset)) { clilen = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); if (connfd < 0) { perror("accept"); exit(-1); } for (i = 0; i < FD_SETSIZE; i++) { if (client[i] < 0) { client[i] = connfd; break; } } if (i == FD_SETSIZE) { printf("too many clients\n"); exit(-1); } FD_SET(connfd, &allset); if (connfd > maxfd) maxfd = connfd; if (i > maxi) maxi = i; if (--nready <= 0) continue; } for (i = 0; i <= maxi; i++) { if ((sockfd = client[i]) < 0) continue; if (FD_ISSET(sockfd, &rset)) { n = read(sockfd, buf, MAXLINE); if (n < 0) { perror("read"); exit(-1); } else if (n == 0) { close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1; } else { writen(sockfd, buf, n); } if (--nready <= 0) break; } } } }

 

  • function.h
/* * author: liuzeyu * date: 2014-12-04 * file: function.h */ #ifndef _FUNCTION_H_20141201_ #define _FUNCTION_H_20141201_ #include <stdio.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #define SERV_PORT 8000 #define LISTENQ 1024 #define MAXLINE 1024 ssize_t writen (int, const void *, size_t); int get_listenfd (); #endif

 

  • function.c
/* * author: liuzeyu * date: 2014-12-01 * file: function.c */ #include "function.h" int get_listenfd () { int listenfd; struct sockaddr_in servaddr; if ( (listenfd = socket (AF_INET, SOCK_STREAM, 0)) <= 0) { perror ("socket"); exit(-1); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (SERV_PORT); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) { perror ("bind"); exit(-1); } if (listen(listenfd, LISTENQ)) { perror ("listen"); exit(-1); } return listenfd; } ssize_t writen (int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; char *ptr; int i; ptr = (char *)vptr; nleft = n; for (i=0; i<n; ++i) { if (ptr[i] >= 'a' && ptr[i] <= 'z') ptr[i] += 'A'-'a'; } while (nleft > 0) { if ((nwritten = write (fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else { perror ("write error: "); return -1; } } nleft -= nwritten; ptr += nwritten; } return n; }

 

上述程序有一些潜在的BUG

关闭 TCP 连接中的一端

这个程序是以交互式方式运行的,即:发送一行文本给服务器,然后等待应答,这段时间被称为“往返时间”(RTT)

如果我们批量地发出请求,而服务端程序同样不断的返回数据(我们假设服务器端的处理时间为 0),这样,管道中在不断的往返数据,如下图所示:

 

当客户端发送完所有的数据后立即关闭连接,而管道中却仍然有其他的请求和应答存在,从而造成了数据的丢失

因此,我们需要有一种可以关闭 TCP 连接中的一半的方法,也就是说,客户端只给服务器端发送一个 FIN,以便告知服务器,已经完成了数据发送,但是仍然保持套接字描述符打开以便读取

这个功能可以用 shutdown 函数完成

缓冲机制

在上面的代码中我们引入了缓冲机制,每次 fgets 读取的文本都存在 stdio 缓冲区中,并在之后把 fgets 返回的单个输入行写给服务器,而不管 stdio 缓冲区中是否还有额外输入

因此,混合使用 stdio 和 select 函数很可能会造成数据的丢失

同样,在 readline 函数中,我们也实现了自己的缓冲区,而 select 函数对此一无所知,他不会去理会我们的缓冲区中是否仍然留有数据

 






读书笔记      技术帖      linux      unix      network      c语言      计算机网络      unp      unix网络编程      网络编程      龙潭书斋      select     


京ICP备15018585号