一.I/O复用
在《TCP套接字编程》的同步聊天程序中,我们看到TCP客户同时处理两个输入:标准输入和TCP套接字。考虑在客户阻塞于标准输入fgets调用时,服务器进程被杀死,服务器TCP虽然会给客户TCP发送一个FIN,但是客户客户进程正阻塞于标准输入读入过程,它将看不到这个EOF,直到从套接字读时为止。这样的进程需要一种预先告知内核的能力,使得内核一旦发现内核指定的一个或多个I/O条件就绪,它就通知进程。这个能力就称之为I/O复用,由select和poll这两个函数支持。
I/O复用通常应用在下列场合:
- 当客户处理多个应用场合时(交互式输入和网络套接字);
- 一个客户同时处理多个套接字;
- 一个TCP服务器既要处理监听套接字,又要处理已连接套接字;
- 一个服务器既要处理TCP,又要处理UDP;
- 一个服务器要处理多个服务或者多个协议。
二.select函数
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。
头文件<sys/select.h>中定义的FD——SETSIZE常值是数据类型fd_set中的描述符总数,其值通常是1024。
maxfdp1参数指定带测试的描述符个数,它的值是带测试最大描述符加1,描述符0,1,2,...,maxfdp1-1均将被测试。
中间三个参数readset,writeset和exceptset指定我们要让内核测试读/写和异常条件的描述符。
void FD_ZERO(fd_set *fdset); //clear all bits in fdset void FD_SET(int fd,fd_set *fdset); //turn on the bit for fd in fdset void FD_CLR(int fd,fd-set *fdset); //turn off the bit for fd in fdset int FD_ISSET(int fd,fd_set *fdset); //is the bit for fd on in fdset?
其中,描述符集的初始化非常重要。
参数timeout告知内核等待所指定描述符中的任何一个就绪可花多少时间,timeval结构用于指定这段时间的秒数和微妙数。
struct timeval { long tv_sec; //second long tv_usec; //microsecond };
这个参数有三种可能:
- 永远等待下去:仅在有一个描述符准备好I/O时才返回,为此,把该参数设为空;
- 等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过timeval所指定时间;
- 根本不等待:检查描述符后立即返回,这个称为轮询(polling),为此,秒数和微妙数必须为0。
三.异步聊天程序
写一个TCP异步聊天程序来加深理解。
服务器代码:
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<arpa/inet.h> #include<netinet/in.h> #include<sys/socket.h> #include<sys/wait.h> #include <unistd.h> #include<time.h> #define MAXSIZE 1024 #define PORT 8080 #define BACKLOG 10 int main(int argc,char **argv) { int listenfd,connfd; struct sockaddr_in servaddr,cliaddr; socklen_t len; char message[MAXSIZE]; fd_set rfds; // struct timeval tv; int retval,maxfd=-1; if((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); exit(1); } else printf("socket create success! "); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_addr.s_addr=htonl(INADDR_ANY); servaddr.sin_port=htons(PORT); if((bind(listenfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr)))==-1) { perror("bind"); exit(1); } else printf("bind success! "); if(listen(listenfd,BACKLOG)==-1) { perror("listen"); exit(1); } else printf("sever is listening! "); for( ; ; ) { printf("等待连接... "); len=sizeof(struct sockaddr); if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))==-1) { perror("accept"); exit(1); } else printf("客户端:%s: %d ",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port)); printf("开始聊天! "); for( ; ; ) { FD_ZERO(&rfds); FD_SET(0,&rfds); maxfd=0; FD_SET(connfd,&rfds); if(connfd>maxfd) maxfd=connfd; retval=select(maxfd+1,&rfds,NULL,NULL,NULL); if(retval==-1) { printf("select出错!%s",strerror(errno)); break; } else if(retval==0) { printf("等待对方输入... "); continue; } else { if(FD_ISSET(0,&rfds)) { bzero(message,MAXSIZE); printf("输入:"); fgets(message,MAXSIZE,stdin); if(!strncasecmp(message, "quit", 4)) { printf("终止聊天! "); break; } else len=send(connfd,message,strlen(message),0); if(len<0) { printf("发送失败"); break; } } if(FD_ISSET(connfd,&rfds)) { bzero(message,MAXSIZE); len=recv(connfd,message,MAXSIZE,0); if(len>0) printf("客户端:%s",message); else { if(len<0) printf("接受消息失败! "); else printf("客户端不在线! "); break; } } } } close(connfd); printf("是否退出服务器[Y/N]:"); bzero(message,MAXSIZE); fgets(message,MAXSIZE,stdin); if(!strncasecmp(message, "Y", 1)) { printf("服务器已退出! "); break; } } close(listenfd); return 0; }
客户端代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/types.h> #include <unistd.h> #include <time.h> #define MAXSIZE 1024 #define PORT 8080 int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; socklen_t len; fd_set rfds; // struct timeval tv; int retval,maxfd=-1; char message[MAXSIZE]; if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); exit(1); } else printf("socket create success! "); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(PORT); inet_aton(argv[1],&servaddr.sin_addr); if(connect(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr))==-1) { perror("connect"); exit(1); } else printf("conncet success! "); for( ; ; ) { FD_ZERO(&rfds); FD_SET(0,&rfds); maxfd=0; FD_SET(sockfd,&rfds); if(sockfd>maxfd) maxfd=sockfd; retval=select(maxfd+1,&rfds,NULL,NULL,NULL); if(retval==-1) { printf("select出错!%s",strerror(errno)); break; } else if(retval==0) { printf("等待对方输入... "); continue; } else { if(FD_ISSET(sockfd,&rfds)) { bzero(message,MAXSIZE); len=recv(sockfd,message,MAXSIZE,0); if(len>0) printf("服务器:%s",message); else { if(len<0) printf("接受消息失败! "); else printf("服务器已退出! "); break; } } if(FD_ISSET(0,&rfds)) { bzero(message,MAXSIZE); printf("输入:"); fgets(message,MAXSIZE,stdin); if(!strncasecmp(message, "quit", 4)) { printf("client 请求终止聊天! "); break; } else len = send(sockfd,message,strlen(message),0); if(len<0) { printf("消息发送失败! "); break; } } } } close(sockfd); return 0; }
编译:
gcc -Wall server.c -o server gcc -Wall client.c -o client
服务器运行结果:
./server socket create success! bind success! sever is listening! 等待连接... 客户端:127.0.0.1: 50235 开始聊天! 客户端: 客户端:你好啊,服务器! 客户端:我是客户。 输入:你好啊,客户端! 输入:客户端: 客户端:Byebye! 客户端不在线! 是否退出服务器[Y/N]:Y 服务器已退出!
客户端运行结果:
./client 127.0.0.1 socket create success! conncet success! 输入:你好啊,服务器! 输入:我是客户端。 输入:服务器: 服务器:你好啊,客户端! 输入:Byebye! 输入:quit 输入:client 请求终止聊天!