现在我们看看实现并发服务器的第三章方法:I/O复用
I/O复用主要是select函数, 但select函数有重大缺陷:
select函数不适用于以web服务器端开发为主流的现代开发环境
select复用方法,无法同时接入上百个客户端
我们现在一般用select的替代品:弥补select函数缺点,仅向操作体统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的项
但前提是操作系统支持这种方式,Linux是epoll,Windows是IOCP,本文章只对select做简单介绍
理解select函数
监视:【事件】
1、是否存在套接字接收数据
2、无需阻塞传输数据的套接字有哪些
3、那些套接字发生了异常
#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
参数分别为:监视对象文件描述符数量、
将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值
将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值
将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值
传递超时信息
select函数调用过程:
1、设置文件描述符:fd_set数组,存有0、1的位数组。
从左到右,依次存文件描述符0123;该位为一,表示该文件描述符为监视对象
由于是位数组,采用宏注册或更改值
FD_ZERO(fd_set * fdset):所有位初始为0
FD_SET(int fd, fd_set * fdset):向参数fdset指向的变量中注册文件描述符fd的信息
将位fd置为1.
FD_CLR(int fd, fd_set * fdset):向参数fdset指向的变量中清除文件描述符fd的信息
FD_ISSET(int fd, fd_set * fdset):若参数fdset指向的变量中包含文件描述符fd的信息【fd对应位设为1】,则返回真
调用select函数之后,fd_set变量原来为1的值会变为0,但发生变化的文件描述符对应位除外;
#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函数后,为了防止陷入无限阻塞状态,传递超时信息
返回值,发生错误时返回-1,超时时返回0,发生关注的事件返回时,返回发生事件的文件描述符数
struct timeval
{
long tv_sec;
long tv_usec;
}
3、设置超时
4、调用select函数
调用select函数之前 ,每次都要初始化 超时信息timeval的值
因为调用select函数之后,结构体timeval 的成员tv_sec和tv_usec会被替换为超时前剩余时间
5、查看调用结果
通过查看fd_set数组,调用select函数之后,原来为1的所有位都变为0,
但发生变化的文件描述符除外。因此,可以认定调用select函数后值仍为1的位置文件描述符
发生变化
下面来看代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
FD_ZERO(&reads); //将数组所有变量初始化为0
FD_SET(0, &reads);// 将文件描述符0对应的位设定为1,监视标准输入的变化
/*
timeout.tv_sec=5;
timeout.tv_usec=5000;
*/
while(1)
{
temps=reads;
timeout.tv_sec=5;
timeout.tv_usec=0;
result=select(1, &temps, 0, 0, &timeout); //将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量&temps,并传递其地址值
if(result==-1) //失败
{
puts("select() error!");
breake;
}
else if(result==0) //超时
{
}
else
{
if(FD_ISSET(0, *temps)) //验证文件描述符0是否被设置,即是否有标准输入
{
str_len=read(0, buf, BUF_SIZE);
buf[str_len]=0;
printf("message from console: %s", buf);
}
}
}
return 0;
}
顺便复习一下Linux的文件函数:
1、底层文件访问(Low-Level File Access)和文件描述符
底层;与标准无关的操作系统独立提供的
文件描述符(对应于window的句柄):系统分配给文件或套接字的整数
文件和套接字一般经过创建过程才会被分配文件描述符,输入输出
对象即使未经过特殊的创建过程,程序开始运行后也会被自动分配
文件描述符。
2、打开文件的函数:
int open(const char *path, int flag);
成功时返回文件描述符,失败时返回-1.
path为文件名的字符串地址(路径信息),flag文件打开模式信息
3、关闭文件的函数
ssize_t write(int fd, const void * buf, size_t nbytes);
成功时返回写入的字节数,失败时返回-1
fd,显示数据传输对象的文件描述符
buf,保存要传输数据的缓冲地址值
nbytes,要传输数据的字节数
size_t是通过typedef声明的unsigned int类型,ssize_t代表signed int,
s代表signed
ssize_t、size_t都是元数据类型,操作体统定义的数据类型会添加后缀_t
4、创建文件的函数
fd=open("data.txt",O_CREAT|O_WRONLY|O_TRUNC);
创建空文件,只写模式打开,若存在同名文件,则清空文件全部数据
5、读取文件的函数
ssize_t read(int fd, void * buf, size_t nbytes);
成功时返回接收的字节数(文件结尾返回0),失败时返回-1
文件描述符从3开始以由小到大的顺序编号,0,1,2是分配给标准I/O的描述符【0是标准输入,1标准输出,2标准错误】