一、基本原理
I/O复用(I/O Multiplexing)是一种允许单个进程或线程同时监视多个文件描述符(通常是网络套接字)的机制,以检测一个或多个文件描述符是否处于可读、可写或异常状态。这样可以实现在一个线程中处理多个I/O操作,提高系统的并发性能。
多进程服务器端:针对每一个客户端创建一个进程提供服务,很消耗计算机性能
I/O复用服务器端:无论连接多少客户端,提供服务的进程只有一个。
二、select()函数实现IO复用技术
运用select()函数是最具代表性的实现复用服务器端的方法。使用select函数可以将多个文件描述符集中到一起统一监视。
1、设置文件描述符
FD_ZERO(fd_set*fdset):将fd_set变量的所有位初始化为0。
FD_SET(int fd, fd_set*fdset):在参数fdset指向的变量中注册文件描述符fd的信息。
FD_CLR(int fd, fd_set*fdset):在参数fdset指向的变量中清除文件描述符fd的信息。
FD_ISSET((int fd, fd_set*fdset):若参数fdset指向的变量中包含文件描述符fd的信息,则返回真(true)。
使用示例:
int main()
{
fd_set set;//定义fd_set变量
FD_ZERO(&set);//将set变量全部置零 0000000000.........
FD_SET(1, &set);//将set变量的1位置一,即将set变量的1位置为监视对象。 010000000000.......
FD_SET(2, &set);//将set变量的2位置一,即将set变量的2位置为监视对象。 011000000000.......
FD_CLR(2, &set);//将set变量的2位置零,即取消对set变量的2位的监视状态。 010000000000......
}
2、select()函数
函数原型:
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set*readset, fd_set*writeset, fd_set*exceptset, const struct timeval *timeout);
/*
成功返回大于0的值,失败返回-1,该值是发生事件的文件描述符数
maxfd:监视对象文件描述符的数量
readset:将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值
writeset:将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值
exceptset:将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值
timeout:调用select函数后,为防止陷入无限阻塞的状态,传递超时信息
*/
struct timeval
{
long tv_sec;//seconds 秒
long tv_usec;//microseconds 微秒
}
三、实现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>
#define BUF_SIZE 1024
int main()
{
char buf[BUF_SIZE ];//接收缓存区
//1、创建服务器套接字信息
int ser_sock = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in seraddr;
memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;//指定IPV4地址族
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
seraddr.sin_port = htons(8888);//指定服务器端口
int seraddr_len = sizeof(seraddr);
//2、绑定服务器信息
if(bind(ser_sock, (struct sockaddr*)&seraddr, seraddr_len) == -1)
{
printf("server bind error\n");
return -1;
}
//3、监听客户端连接 5个客户端
if(listen(ser_sock, 5) == -1)
{
printf("server listen error\n");
return -1;
}
//4、初始化客户端信息
int cli_sock;
struct sockaddr_in cliaddr;
memset(&cliaddr, 0, sizeof(cliaddr));
int cliaddr_len = sizeof(cliaddr);
//初始化I/O信息
struct timeval timeout;
fd_set reads,cpy_reads; //默认大小1024 bit
int fd_max, fd_num; //fa_max:最大文件描述符的值 fd_num:文件描述符的数量
FD_ZERO(&reads);//都置零
FD_SET(ser_sock, &reads);//将文件描述符ser_sock对应位置一,即监视该文件描述符
fd_max = ser_sock;//如果没有其他文件描述符被创建,则该文件描述符最大,且为3,0:标准输入(stdin), 1:标准输出 (stdout),2:标准错误(stderr)。文件描述符范围3 到 1023(通常)
while(1)
{
cpy_reads = reads;//cpy_reads:变化后, reads:变化前
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
if((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)//返回文件描述符数量,因为文件描述符从0开始,所以加一返回的才是数量
break;
if(fd_num == 0)//没有监视的文件描述符
countinue;
for(int i = 0; i < fd_max+1; i++)
{
if(FD_ISSET(i, &cpy_reads))//第i位包含监视的文件描述符信息 如果发生变化
{
if(i == ser_sock)//如果是服务器文件描述符,表示有新客户端连接请求
{
cli_sock = accept(ser_sock, (struct sockaddr*)&cliaddr, &cliaddr_len);
FD_SET(cli_sock, &reads);//添加监视信息到变化前的fd_set型变量
if(fd_max < cli_sock)
{
fd_max = cli_sock;
}
printf("connected client: %d\n", cli_sock);
}
else
{
int len = read(i, buf, BUF_SIZE);//读取对应文件描述符数据
if(len == 0)
{
FD_CLR(i, &reads);//移除该客户端的文件描述符
close(i);//关闭该客户端文件描述符连接
printf("closed client: %d\n", i);
}
else
{
//可执行打印数据等操作,此处为接收数据之后的操作
}
}
}
}
}
close(ser_sock);
return 0;
}