I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。通常网络程序在下列情况下需要使用I/O复用技术。
1、客户端程序要同时处理多个socket。比如本章要讨论的非阻塞的Connect 技术。
2、客户端程序要同时处理用户输入和网络连接,比如本章将要讨论的聊天室程序。
3、TCP服务器要同时处理监听socket和连接socket。这里I/O复用使用最多的场合。
4、服务器要同时处理TCP请求和UDP请求。比如回射服务器。
5、服务器要同时监听多个端口,或者处理多种服务。
需要指出的是,I/O复用虽然能同时监听多个文件描述,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外措施,程序就只能顺序依次处理其中的每个文件描述符,这使用服务器程序想串行工作。如果要实现并发,只能使用多进程等编程手段。
Linux实现的I/O复用的系统调用主要有select、poll和epoll。
1、select系统调用。
select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。本节先介绍select系统调用的API,然后讨论select判断文件描述符就绪动态条件,最后给出一个处理带外数据中的实际应用。
一、select API
1
2 3 4 |
#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set*exceptfds ,struct timeval* timeout); |
2)readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改他们来通知应用程序那些文件描述符已经就绪。这3个参数是fd_set结构指针类型。fd_set结构体定义如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include<typesizes.h>
#define __FD_SETSIZE 1024 #include<sys/select.h? #define FD_SETSIZE __FD_ETSIZE typedef long int __fd_mask; #undef __NFDBITS #define __NFDBITS (8*(int)sizeof(__fd_mask)) typedef struct { #ifdef __USE_XOPEN __fd_mask fd_bits[__FD_SETSIZE / __NFDBITS]; #define __FDS_BITS(set) ((set)->fd_bits) #else __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; #define __FD_BITS(set) ((set)->__fds_bits) #endif } fd; |
由于位操作过于繁琐,我们应该使用下面一系列宏来访问fd_set结构体中的位。
1
2 3 4 5 |
#include<sys/select.h>
FD_ZERO(fd_set *fdset); //清除fdset所有位 FD_SET(int fd, fd_set *fdset); //设置fdset的位fd FD_CLR(int fd, fd_set *fdset); //清除fdset的位fd int FD_ISSET(int fd, fd_set *fdset); //测试fdset的位fd是否被设置 |
3)timeout参数用来设置select函数的超时时间,它是一个timeval结构类的指针,采用指针参数是因为内核将修改它告诉应用程序select等待了多久。不过我们不能完全信任select调用返回后的timeout值,比如调用失败时timeout值是不确定的。timeval结构体定义如下所示:
1
2 3 4 5 |
struct timeval
{ long tv_sec; //秒数 long tv_uset; //微秒数 }; |
select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0.select失败时返回-1,并设置errno。如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。
二、文件描述符的就绪条件
那些情况下文件描述符可以被认为可读、可写或者异常呢?对于select的使用非常关键。在网络编程中,下列情况下socket可读。
1、socket内核接收缓冲区中的字节数大于或者等于低水平标记SO_RCVOWAT。此时我们可以无阻塞地读该socket,并且读该socket的读操作将返回0.
2、socket通信的对方关闭连接。此时对该socket的读操作将返回0
3、监听socket上新的连接请求。
4、socket上有未处理的错误,此时我们可以使用getsockopt来读取和清楚该错误。
5、socket内核发送缓存区中的可用字节数大于或者低于SO_SNDLOWAT。此时我们可以无阻塞的写socket,并且写操作返回的字节大于0.
6、socket的写操作被关闭。对写操作被关闭的socket执行写操作出发一个SIGPIPE信号。
7、socket使用非阻塞的Connect 连接成功或者失败之后。
8、socket上有未处理的错误。此时我们可以使用getsocket来读取和清楚该错误。
网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。
三、处理带外数据
上一小节提到,socket上接收到普通数据和带外数据使用select返回,但socket处于不同的就绪状态:前者处于可读状态,后者处于异常状态。代码如下是如何同时处理二者的。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
#include<sys/types.h>
#include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<assert.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<fcntl.h> int main(int argc, char *argv[]) { if(argc <= 2) { printf("usage:%s ip_adderss prot_number\n", basename(argv[0])); return 1; } const char *ip = argv[1]; int port = atoi(argv[2]); int ret = 0; struct sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port); int listenfd = socket(PF_INET, SOCK_STREAM, 0); assert(listenfd > 0); ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address)); assert(ret != -1); ret = listen(listenfd, 5); assert(ret != -1); struct sockaddr_in client_address; socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength); if(connfd < 0) { printf("errno is: %d\n", errno); close(listenfd); } char buf[1024]; fd_set read_fds; fd_set exception_fds; FD_ZERO(&read_fds); FD_ZERO(&exception_fds); while(1) { memset(buf, '\0', sizeof(buf)); //每次调用select前都要重新在read_fds和exception_fds中 //设置文件描述符connfd //因为事件发生之后,文件描述符被内核修改。 FD_SET(connfd, &read_fds); FD_SET(connfd, &exception_fds); ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);//阻塞 if(ret < 0) { printf("selection failure\n"); break; } //对于可读事件,采用普通的recv函数读取数据 if(FD_ISSET(connfd, &read_fds)) { ret = recv(connfd, buf, sizeof(buf) - 1, 0); if(ret <= 0) { break; } printf("get %d bytes of normal data:%s\n", ret, buf); } //对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据 else if(FD_ISSET(connfd, &exception_fds)) { ret = recv(connfd, buf, sizeof(buf) - 1, MSG_OOB); if(ret <= 0) { break; } printf("get %d bytes of normal data:%s\n", ret, buf); } } close(connfd); close(listenfd); return 0; } |