1.介绍
在前几节,我们介绍了循环服务器,并发服务器. 简单的循环服务器每次只能处理一个请求,即处理的请求是串行的。而并发服务器可以通过创建多个进程或者是线程来并发的处理多个请求。但由于进程或线程的切换会带来一定的开销。而且随着客户端请求的增多,创建的线程或进程的数目也越来越多,开销势必会增加。因此,本文提出了I/O复用的循环服务器。I/O复用的循环服务器创建两个线程,一个是客户端连接处理线程,专门用来处理客户端的连接,当有客户端到来的时候,此线程把客户端的套接字描述符放到一块公共的区域中。另一个是业务处理线程,此线程轮循(select)客户端套接字描述符集合中有没有数据到来,如果有数据到来,那么就进行处理。
2. I/O复用循环服务器处理流程
socket(...);
bind(...);
listen(...);
pthread_create(...);
pthread_join(..);
close(...); //关闭服务器套接字
连接处理线程:
while(1){
accept(...);
store(...);//存储客户端套接字描述符
}
业务处理线程:
while(1){
get(...);//取出套接字描述符放到FD_SET
select(...);
recv(....);
process(...);
send(...);
close(....);
}
从算法的主要流程可以看出,I/O复用循环服务器只用两个线程,一个是请求业务连接线程,专门处理连接。另一个是业务处理线程,轮循客户端的套接字有没有数据。
3. 相关例子
服务器;
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <time.h>
#include <pthread.h>
/**
I/O复用循环服务器
I/O并发服务器随着客户端的增多,必须增加处理单元,系统的负载会移动多个处理单元的切换上,切换进程或者是线程
而I/O复用服务器包括两个线程,一个是业务连接线程,专门处理客户端的连接,另一个是业务请求处理线程,对多个客户端描述符进行一定时间的等待,即select监听多个描述符
**/
#define PORT 8888
#define CLIENTNUM 1024
#define BUFFERSIZE 1024
#define BACKLOG 10
static int connect_host[CLIENTNUM];
static int connect_number=0;//连接的客户数
static void* handle_connect(void*argv){//业务连接函数,处理客户端的连接,将客户端的套接字描述符加入到连接池中
int ret;
int s;
int sc;
s=*((int*)argv);//服务端套接字描述符
int i=0;
struct sockaddr_in client_addr;
int len;
len=sizeof(struct sockaddr_in);
for(;;){//监听有没有客户端到来
sc=accept(s,(struct sockaddr*)&client_addr,&len);
printf("a client connect,from:%s\n",inet_ntoa(client_addr.sin_addr));
if(sc>0){
for(i=0;i<CLIENTNUM;i++){
if(connect_host[i]==-1){
connect_host[i]=sc;
connect_number++;
break;
}
}
}
}
}
//请求业务处理线程
static void* handle_request(void*argv){
time_t now;
char buffer[BUFFERSIZE];
int size;
//设置轮循的时间,每隔1秒
struct timeval tv;
tv.tv_sec=1;
tv.tv_usec=0;
int ret;
int i=0;
int maxfd;
fd_set scanfd;
for(;;){
FD_ZERO(&scanfd);//清空文件描述符集合
//将文件描述符放入文件描述符集合
for(i=0;i<CLIENTNUM;i++){
if(connect_host[i]!=-1){
FD_SET(connect_host[i],&scanfd);
if(maxfd<connect_host[i]){
maxfd=connect_host[i];
}
}
}
//select
ret=select(maxfd+1,&scanfd,NULL,NULL,&tv);//监控读文件描述符集,看看是否有数据可读
switch(ret){
case -1://错误
break;
case 0://超时
break;
default://有数据到来
if(connect_number<0)
break;
for(i=0;i<CLIENTNUM;i++){
if(connect_host[i]!=-1){
if(FD_ISSET(connect_host[i],&scanfd)){//文件描述符在文件集合中
memset(buffer,0,BUFFERSIZE);
size=recv(connect_host[i],buffer,BUFFERSIZE,0);
if(size>0&&!strncmp(buffer,"TIME",4)){//时间请求
memset(buffer,0,BUFFERSIZE);
now=time(NULL);
sprintf(buffer,"%24s\r\n",ctime(&now));
send(connect_host[i],buffer,strlen(buffer),0);
}
//更新文件描述符数组中的值
connect_host[i]=-1;
connect_number--;
close(connect_host[i]);//关闭客户端套接字描述符
}
}
}
break;
}
}
}
int main(int argc,char*argv[]){
int ret;
int s;
struct sockaddr_in server_addr;
int i;
pthread_t thread[2];//两个线程,一个是处理连接,另一个是处理请求
//建立TCP套接字
s=socket(AF_INET,SOCK_STREAM,0);
memset(connect_host,-1,CLIENTNUM);//将存储套接字描述符的数组为-1
if(s<0){
perror("socket error");
return -1;
}
//将地址结构绑定到套接字描述符
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
server_addr.sin_port=htons(PORT);
ret=bind(s,(struct sockaddr*)&server_addr,sizeof(struct sockaddr_in));
if(ret<0){
perror("bind error");
return -1;
}
//监听
ret=listen(s,BACKLOG);
if(ret<0){
perror("listen error");
return -1;
}
pthread_create(&thread[0],NULL,handle_connect,(void*)&s);//处理客户端连接,传递的是服务器的套接字描述符
pthread_create(&thread[1],NULL,handle_request,NULL);//handle_request线程回调函数
//等待线程结束
for(i=0;i<2;i++){
pthread_join(thread[i],NULL);
}
close(s);
}
客户端:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <netinet/in.h>
#define PORT 8888
#define BUFFERSIZE 1024
int main(int argc,char*argv[]){
int s;
int ret;
int size;
struct sockaddr_in server_addr;
char buffer[BUFFERSIZE];
s=socket(AF_INET,SOCK_STREAM,0);
if(s<0){
perror("socket error");
return -1;
}
bzero(&server_addr,sizeof(server_addr));
//将地址结构绑定到套接字
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(PORT);
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//连接服务器
ret=connect(s,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(ret==-1){
perror("connect error");
return -1;
}
memset(buffer,0,BUFFERSIZE);
strcpy(buffer,"TIME");
size=send(s,buffer,strlen(buffer),0);
if(size<0){
perror("send error");
return -1;
}
memset(buffer,0,BUFFERSIZE);
size=recv(s,buffer,BUFFERSIZE,0);
if(size<0){
perror("recv error");
return;
}
printf("%s",buffer);
close(s);
return 0;
}
运行结果:
[root@localhost 14章服务器模式]# ./select-tcp
a client connect,from:127.0.0.1
[root@localhost 14章服务器模式]# ./circle-tcpc14
Sat Feb 18 14:28:10 2012
总结:本文主要针对简单循环服务器,并发服务器的不足,介绍了I/O复用的循环服务器,最后给出了实例.