上一小节讲到可以实现多客户端与服务器进行通讯,对于每一个客户端的连接请求,服务器都要分配一个进程进行处理。对于多用户连接时,服务器会受不了的,而且还很消耗资源。据说有个select函数可以用,好像还很NB的样子。
使用select多路转换处理聊天程序
下面摘取APUE 14.5小结 I/O多路转接
当从一个描述符读,然后又写到另一个描述符时,可以在下列形式的循环中循环中使用阻塞I/O:
while((n = read(STDIN_FILENO, buf, BUFFSIZ))>0)
if(write(STDOUT_FILENO, buf, n)!=n)
err_sys("write error");
这种形式的阻塞I/O到处可见。但是如果必须从两个描述符读,又将如何呢?如果仍旧使用阻塞I/O,那么就可能长时间阻塞在一个描述符上,而另一个描述符虽有很多数据却不能得到及时处理。所以为了处理这种情况显然需要另一种不同的技术。
方法一:也就是上一小节使用的方法,使用多进程。每一个进程处理一个描述符
方法二:和上面相似的,使用多线程,不同的线程处理不同的描述符
方法三:仍然使用一个进程执行该程序,但使用非阻塞I/O读取数据。然后对所有的描述符进行遍历一遍,判断对应的描述符是否有数据,如果有就读取,如果没有就立即返回。这种办法就是轮询(polling)
方法四:异步I/O。其基本的思想是进程告诉内核,当一个描述符已经准备好可以进行I/O时,用一个信号通知它。
方法五:这是一种比较好的办法。叫做I/O多路转换(I/O multiplexing)。先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已经准备好进行I/O时,该函数才会返回。在返回时,它高数进程哪些描述符已经准备好可以进行I/O。
poll,pselect和select这三个函数使我们能够执行I/O多路转换。本程序只使用select函数。
#include <sys/select.h>
int select (int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct time val *restrict tvptr); //返回值:准备就绪的描述符数,若超时则返回0,否则出错返回-1
select 函数讲解
FD_ISSET判断描述符fd是否在给定的描述符集fdset中,通常配合select函数使用,由于select函数成功返回时会将未准备好的描述符位清零。通常我们使用FD_ISSET是为了检查在select函数返回后,某个描述符是否准备好,以便进行接下来的处理操作。
fd_set数据类型的操作
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset); //判断fd是否在fdset中
void FD_CLR(int fd, fd_set *fdset); //进fd从fdset中取出
void FD_SET(int fd, fd_set *fdset); //将fd放入fdset
void FD_ZERO(fd_set *fdset); //将fdset清空
timeval结构分析
struct timeval{
long tv_sec; //seconds
long tv_usec; //and microseconds
};
client.c的代码没有改
server.c的代码如下
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <errno.h> 4 #include <string.h> 5 #include <netdb.h> 6 #include <sys/types.h> 7 #include <sys/socket.h> 8 #include <sys/time.h> 9 #include <sys/un.h> 10 #include <sys/ioctl.h> 11 #include <sys/wait.h> 12 #include <sys/select.h> 13 #include <netinet/in.h> 14 #include <arpa/inet.h> 15 16 #define SERVER_PORT 12138 17 #define BACKLOG 20 18 #define MAX_CON_NO 10 19 #define MAX_DATA_SIZE 4096 20 21 int MAX(int a,int b) 22 { 23 if(a>b) return a; 24 return b; 25 } 26 27 int main(int argc,char *argv[]) 28 { 29 struct sockaddr_in serverSockaddr,clientSockaddr; 30 char sendBuf[MAX_DATA_SIZE],recvBuf[MAX_DATA_SIZE]; 31 int sendSize,recvSize; 32 int sockfd,clientfd; 33 fd_set servfd,recvfd;//用于select处理用的 34 int fd_A[BACKLOG+1];//保存客户端的socket描述符 35 int conn_amount;//用于计算客户端的个数 36 int max_servfd,max_recvfd; 37 int on=1; 38 socklen_t sinSize=0; 39 char username[32]; 40 int pid; 41 int i; 42 struct timeval timeout; 43 44 if(argc != 2) 45 { 46 printf("usage: ./server [username]\n"); 47 exit(1); 48 } 49 strcpy(username,argv[1]); 50 printf("username:%s\n",username); 51 52 /*establish a socket*/ 53 if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) 54 { 55 perror("fail to establish a socket"); 56 exit(1); 57 } 58 printf("Success to establish a socket...\n"); 59 60 /*init sockaddr_in*/ 61 serverSockaddr.sin_family=AF_INET; 62 serverSockaddr.sin_port=htons(SERVER_PORT); 63 serverSockaddr.sin_addr.s_addr=htonl(INADDR_ANY); 64 bzero(&(serverSockaddr.sin_zero),8); 65 66 /* 67 * SOL_SOCKET.SO_REUSEADDR 允许重用本地地址 68 * */ 69 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); 70 71 /*bind socket*/ 72 if(bind(sockfd,(struct sockaddr *)&serverSockaddr,sizeof(struct sockaddr))==-1) 73 { 74 perror("fail to bind"); 75 exit(1); 76 } 77 printf("Success to bind the socket...\n"); 78 79 /*listen on the socket*/ 80 if(listen(sockfd,BACKLOG)==-1) 81 { 82 perror("fail to listen"); 83 exit(1); 84 } 85 86 timeout.tv_sec=1;//1秒遍历一遍 87 timeout.tv_usec=0; 88 sinSize=sizeof(clientSockaddr);//注意要写上,否则获取不了IP和端口 89 90 FD_ZERO(&servfd);//清空所有server的fd 91 FD_ZERO(&recvfd);//清空所有client的fd 92 FD_SET(sockfd,&servfd); 93 conn_amount=0; 94 max_servfd=sockfd;//记录最大的server端描述符 95 max_recvfd=0;//记录最大的client端的socket描述符 96 while(1) 97 { 98 FD_ZERO(&servfd);//清空所有server的fd 99 FD_ZERO(&recvfd);//清空所有client的fd 100 FD_SET(sockfd,&servfd); 101 //timeout.tv_sec=30;//可以减少判断的次数 102 switch(select(max_servfd+1,&servfd,NULL,NULL,&timeout))//为什么要+1,是因为第一个参数是所有描述符中最大的描述符fd号加一,原因的话在APUE中有讲,因为内部是一个数组,第一个参数是要生成一个这样大小的数组 103 { 104 case -1: 105 perror("select error"); 106 break; 107 case 0: 108 //在timeout时间内,如果没有一个描述符有数据,那么就会返回0 109 break; 110 default: 111 //返回准备就绪的描述符数目 112 if(FD_ISSET(sockfd,&servfd))//sockfd 有数据表示可以进行accept 113 { 114 /*accept a client's request*/ 115 if((clientfd=accept(sockfd,(struct sockaddr *)&clientSockaddr, &sinSize))==-1) 116 { 117 perror("fail to accept"); 118 exit(1); 119 } 120 printf("Success to accpet a connection request...\n"); 121 printf(">>>>>> %s:%d join in!\n",inet_ntoa(clientSockaddr.sin_addr),ntohs(clientSockaddr.sin_port)); 122 //每加入一个客户端都向fd_A写入 123 fd_A[conn_amount++]=clientfd; 124 max_recvfd=MAX(max_recvfd,clientfd); 125 } 126 break; 127 } 128 //FD_COPY(recvfd,servfd); 129 for(i=0;i<MAX_CON_NO;i++)//最大队列进行判断,优化的话,可以使用链表 130 { 131 if(fd_A[i]!=0) 132 { 133 FD_SET(fd_A[i],&recvfd);//对所有还连着服务器的客户端都放到fd_set中用于下面select的判断 134 } 135 } 136 137 switch(select(max_recvfd+1,&recvfd,NULL,NULL,&timeout)) 138 { 139 case -1: 140 //select error 141 break; 142 case 0: 143 //timeout 144 break; 145 default: 146 for(i=0;i<conn_amount;i++) 147 { 148 if(FD_ISSET(fd_A[i],&recvfd)) 149 { 150 /*receive datas from client*/ 151 if((recvSize=recv(fd_A[i],recvBuf,MAX_DATA_SIZE,0))==-1) 152 { 153 //perror("fail to receive datas"); 154 //表示该client是关闭的 155 printf("close\n"); 156 FD_CLR(fd_A[i],&recvfd); 157 fd_A[i]=0;//表示该描述符已经关闭 158 } 159 else 160 { 161 printf("Client:%s\n",recvBuf); 162 //可以判断recvBuf是否为bye来判断是否可以close 163 memset(recvBuf,0,MAX_DATA_SIZE); 164 } 165 } 166 } 167 break; 168 } 169 170 } 171 return 0; 172 }
运行后的截图结果
可以看出三个客户端都可以随时连接到服务器,并且发送数据给服务器。实现的效果跟上一节的多进程实现是一样的。毕竟没有大量客户端进行连接,所以就看不出效果,从书中和网上介绍说,这样可以提高某些方面的性能。
下一节将介绍服务器端向各个还在线的客户端进行发送数据,实现交互。然后再实现聊天室功能,大概的思路就是对接收到的数据进行转发。
参考资料: http://www.cnblogs.com/gentleming/archive/2010/11/15/1877976.html