在之前我们实现的并发服务端时通过床将多个进程来实现的,这种并实现并发的方式简单方便,但是进程的创建和销毁是很消耗系统资源的,在访问量大时服务器很容易出现资源不够用的情况。除此之外,由于每个进程有独立的内存空间,所以进程间的通讯也相对比较复杂。因此我们可以考虑通过另一种方式来实现服务端的并发服务——IO复用。
复用:
复用在通讯领域很常见,一般常见”频分复用”,”时分复用”等名词。其实复用就是在一个通信频道内传递多个数据(信号)的技术。以频分复用为例:其实就是在一个通信信道内,发送端通过把信息加载在不同频率的波段上进行发送,而接受端在接受到波时通过滤波装置把各中频率的波进行分离,以此达到提高通信信道利用率的目的。
IO复用:
IO复用其实也是通过对IO描述符的复用来减少进程的创建,使得服务端始终只有一个进程,从而节省了系统资源,提高效率。
select()函数是最具有代表性的实现复用服务端的方法,它可以将多个文件描述符集中到一起进行统一监视,当监视到有文件描述符需要输入或者是输出时就选择该接口进行通讯,通讯完成之后就回到之前监视的状态。
监视内容:是否存在套接字接受数据?无需阻塞传输数据的套接字有哪些?哪些套接字发生了异常?
int select(int maxfd,fd_set *read_set, *write_set,fd_set *except_set, const struct timeval *timeout)选择描述符进行通讯:
maxfd(监视数量):监视对象文件描述符数量
read_set(读取文件描述符集合的地址):将所有关注”是否存在待读取数据”的文件描述符注册到fd_set集合中,并传递地址值。也就是说select()函数会监视这个集合里边的文件描述符是是否有待读取的数据,没有要监听的描述符时传0
write_set(写入文件描述符集合的地址):将所有关注”是否可传输无阻塞数据”的文件描述符注册到fd_set集合中,并传递地址值。也就是说select()函数会监视这个集合里边的文件描述符是否能发送无阻塞数据,没有要监听的描述符时传0
except_set(发生异常文件描述符集合的地址):将所有关注”是否可发生异常”的文件描述符注册到fd_set集合中,并传递地址值。也就是说select()函数会监视这个集合里边的文件描述符是否发生异常,没有要监听的描述符时传0
timeout(超时):位防止无限进入阻塞状态,设置一个超时信息
发生错误时返回-1,超时时返回0,当所关注的事件发生时,返回所发生事件的文件描述符数量
select()函数的使用比较复杂,大体分为三步:
参数设置:
设置文件描述符:使用select()函数能同时监听多个文件描述符,首先要使用fd_set类型将这些文件描述符按照分类(接收,传输,异常)集中起来。
fd_set是一个存有0和1的位数组。从下标0开始,一直到下标为当前文件描述符的最大序号为止,依次表示该文件描述符是否被监听,例如fd_set 变量fds[0]中的值为1时表示文件描述符0(标准的输入流)被监听。
针对fd_set的操作都是以位为单位的,为此专门编写了用于fd_set读写的宏定义:FD_ZERO(fd_set *fdset):将fd_set的所有位初始化为0
FD_SET(int fd,fd_set *fdset):在fd_set中注册文件描述符fd的信息
FD_CLR(int fd,fd_set *fdset):从fd_set中清除文件描述符fd的信息
FD_ISSET(int fd,fd_set *fdset):查询fd_set中是否包含文件描述符fd的信息
指定监听范围:指定监听文件描述符的范围,其实也就是fd_set中的文件描述符数量,由于每次新创建一个文件描述符时都会自动加1,所以要传入的值为最大的文件描述符+1(加一是由于文件描述符的标号从0开始)。
设置超时:由于当文件描述符没有状态的改变时select()函数会始终处于阻塞状态,设置超时时间就是为了防止无限制的等待。即使文件描述符没有发生变化,只要过了指定时间,函数会返回0。这样在函数调用时能知道当前的状态。
结构体timeval用于保存设置的超时时间,每次在调用select()函数之前都要重新设置超时时间,其结构体如下:
struct timeval{ long tv_sec;//秒数 long tv_usec://毫秒数 }
调用select()函数:监听注册的文件描述符的状态,当有状态发生变化,或者时超时时返回结果。
查看调用结果:当select()函数返回值是大于0的整数时说明是所监听的文件描述符的状态发生了变化,这时我们可以通过之前的fd_set变量来查看变化的结果。
当select()函数调用完之后向其传入的fd_set变量将发生变化,原来为1的所有位均变为0,但是发生变化的文件描述符对应位除外,因此可以认为值仍为1的位置上的文件描述符发生了变化。
至此关于select()函数的介绍就结束了,用起来比较复杂,我们梳理一遍使用过程:
准备工作:
为select()设置要监视的文件描述符集合,使用函数库提供的关于fd_set的宏定义设置fd_set
为select()设置监视范围,即当前最大文件描述符+1
位select(0设置超时时间,把秒数填入timeval结构体的tv_sec成员中,把毫秒数填入timeval结构体的tv_usec成员中,每次在调用select()函数之前都要重新设置超时时间。
调用select()函数
查看调用结果:根据fd_set调用前后的变化来确定发生变化的文件描述符,调用之后fd_set中值为1的位所对应的文件描述符状态发生了变化
调用发生变化的文件描述符进行相应的操作
在大体了解了select()函数的使用过程之后我们就可以尝试着进行一下简单的应用:
#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
#include<sys/select.h>
#define BUFF_SIZE 30
int main(){
//声明文件描述符集合
fd_set read_set;
fd_set temp_set;
//保存函数的返回结果
int select_res;
//字符串长度
int str_len;
//字符缓冲
char buff[BUFF_SIZE];
//超时时间结构体
struct timeval time_out;
//初始化fd_set,所有位都置0
FD_ZERO(&read_set);
//设置fd_set,使其监视文件描述符为0的文件描述符(系统的标准输入流)
FD_SET(0,&read_set);
while(1){
temp_set = read_set;
//设置超时时间
time_out.tv_sec = 5;
time_out.tv_usec = 0;
//调用select()函数
select_res = select(1,&temp_set,0,0,&time_out);
//根据返回值来判断是否变化
if(select_res == -1){
puts("select() error");
break;
}else if(select_res == 0){
puts("select() timeout");
}else{
//检查是否含有要查询的描述符
if(FD_ISSET(0,&temp_set)){
//从文件描述符为0的流中读取数据
str_len = read(0,buff,BUFF_SIZE);
buff[str_len] = 0;
printf("message from console : %s ",buff);
}
}
}
return 0;
}
IO复用的服务端:
/*************************************************************************
> File Name: echo_select_server.c
> Author: xjhznick
> Mail: xjhznick@gmail.com
> Created Time: 2015年03月26日 星期四 14时03分40秒
> Description:使用select函数实现I/O复用服务器端
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<sys/select.h>
void error_handling(char *message);
#define BUFF_SIZE 32
int main(int argc, char *argv[])
{
int server_sock;
int client_sock;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_addr_size;
char buff[BUFF_SIZE];
fd_set reads, reads_init;
struct timeval timeout, timeout_init;
int str_len, i, fd_max, fd_num;
if(argc!=2){ //命令行中启动服务程序仅限一个参数:端口号
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
//调用socket函数创建套接字
server_sock = socket(PF_INET, SOCK_STREAM, 0);
if(-1 == server_sock){
error_handling("socket() error.");
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
//调用bind函数分配IP地址和端口号
if( -1 == bind( server_sock, (struct sockaddr*)&server_addr,
sizeof(server_addr)) ){
error_handling("bind() error");
}
//监听端口的连接请求,连接请求等待队列size为5
if( -1 == listen(server_sock, 5) ){
error_handling("listen() error");
}
//register fd_set var
FD_ZERO(&reads_init);
FD_SET(server_sock, &reads_init);//monitor socket: server_sock
FD_SET(0, &reads_init);// stdin also works
fd_max = server_sock;
//
timeout_init.tv_sec = 5;
timeout_init.tv_usec= 0;
while(1){
//调用select之后,除发生变化的文件描述符对应的bit,其他所有位置0,所以需用保存初值,通过复制使用
reads = reads_init;
//调用select之后,timeval成员值被置为超时前剩余的时间,因此使用时也需要每次用初值重新初始化
timeout = timeout_init;
fd_num = select(fd_max+1, &reads, NULL, NULL, &timeout);
if(fd_num < 0){
fputs("Error select()!", stderr);
break;
}else if(fd_num == 0){
puts("Time-out!");
continue;
}
for(i=0; i<=fd_max; i++){
if(FD_ISSET(i, &reads)){
if(i == server_sock){//connection request!
//接受连接请求
client_addr_size = sizeof(client_addr);
client_sock = accept( server_sock, (struct sockaddr*)&client_addr, &client_addr_size );
//accept函数自动创建数据I/0 socket
if(-1 == client_sock){
error_handling("accept() error");
//健壮性不佳,程序崩溃退出
} else{
//注册与客户端连接的套接字文件描述符
FD_SET(client_sock, &reads_init);
if(fd_max < client_sock) fd_max = client_sock;
printf("Connected client : %d\n", client_sock);
}
}else{//read message!
str_len = read(i, buff, BUFF_SIZE);
if(str_len){//echo to client
buff[str_len] = 0;
printf("Message from client %d: %s", i, buff);
write(i, buff, str_len);
}else{ //close connection
FD_CLR(i, &reads_init);
close(i);
printf("Disconnected client %d!\n", i);
}
}//end of i==|!=server_sock
}//end of if(FD_ISSET)
}//end of while
}//end of for
//断开连接,关闭套接字
close(server_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(EXIT_FAILURE);
}