TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端
我们接下来延伸并发服务器的实现。首先我们需要知道多进程服务器端的缺点,那就是创建进程时需要付出很大的代价,需要大量的运算和内存空间,由于每个进程都具有独立的内存空间,所以相互间的数据交换也需要使用管道这样相对复杂的方法。
那么能否不创建进程也能同时向多个客户端提供服务?当然是可以的!I/O复用技术就是一种。
1.I/O复用技术
I/O复用技术能使程序同时监听多个文件描述符,这对提高程序性能至关重要。Linux下实现I/O复用的系统调用主要有select、poll、epoll。本节的内容是select。
对I/O复用最直观的理解是小时候使用的纸杯电话。
它的优点是减少了连线长度与纸杯个数。多进程服务器端模型与I/O复用服务器端模型的对比如下图所示,
可以看出复用技术可以减少进程数,提供服务的进程只有一个。
2.select函数
select函数是最具有代表性的实现复用技术服务器端的方法
(1)select函数的功能及调用顺序
使用select函数时可以将多个文件描述符集中到一起统一监视,监视的项目如下:
- 是否存在套接字接收数据?
- 无需阻塞传输数据的套接字有哪些?
- 哪些套接字发生了异常?
我们把监视项称为事件,发生监视项对应的情况时,称“事件发生”。下图介绍select函数的调用方法和顺序,
select函数使用和普通函数区别较大,需要一些准备工作,而且在还需要查看调用结果,接下来我们按照步骤逐一讲解。
(2)设置文件描述符
select函数可以同时监视多个文件描述符。当然,监视文件描述符可以视为监视套接字。首先需要将要监视的文件描述符集中到一起,集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述3种监视项分成3类。
使用fd_set位数组执行此操作,最左端的位表示文件描述符0的位置,该位为1表示该文件描述符是监视对象。
使用如下的宏来对fd_set进行注册和更改
- 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的信息,则返回“真”
eg
(3)设置监视范围及超时
我们先介绍下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函数后,为防止陷入无限阻塞的状态,传递超时信息
select函数用来验证三种监视项的变化情况,根据监视项声明三个fd_set型变量,分别向其注册文件描述符信息,并把变量的地址值传递到上述函数的第二到第四个参数。但在调用select函数之前,需要决定下面两件事:
- 文件描述符的监视范围
- 如何设定select函数的超时时间
文件描述符的监视范围与select函数的第一个参数有关。实际上,select函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在fd_set变量中的文件描述符数。但每次新建文件描述符时,其值都会加一,故只需将最大的文件描述符值加一再传递到select函数即可,加一是因为文件描述符的值从0开始。
select函数的超时时间与select函数的最后一个参数相关,其中timeval结构体定义如下。
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
select函数在监视的文件描述符发生变化时才返回,如果未发生变化,就会进入阻塞状态,指定超时时间就是为了防止这种情况发生。通过声明上述结构体变量,将秒数填入tv_sec成员,将毫秒数填入tv_usec成员,然后将结构体的地址值传递到select函数的最后一个参数。超时select函数返回0,不设置超时,可以传递NULL。
(4)调用select函数后查看结果
select函数调用完成后,向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外。因此,可以认为值仍然为1的位置上的文件描述符发生了变化。
select函数使用示例伪代码如下
fd_set reads, temps;
int result, str_len;
struct timeval timeout;
//①设置文件描述符
FD_ZERO(&reads);//初始化fd_set变量都为0
//②设置监视范围
FD_SET(0, &reads); //监视文件描述符0位置
while (1)
{
temps = reads;//每次调用select剩余位会被初始化为零,所以需要保存初始值
//③设置超时
timeout.tv_sec = 5;
timeout.tv_usec = 0;
//④调用select函数
result = select(1, &temps, 0, 0, &timeout);
//⑤查看结果
if (result == 0)//超时返回0
{
//超时
}
else if(FD_ISSET(0, &temps)//验证变化的文件描述符是否是标准输入
{
//有输入则输出
}
}
完整示例如下
#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