TCP/IP网络编程学习(12):I/O复用

基于多进程方式的服务端中一个进程服务一个客户端,但是创建进程需要的代价极大,需要大量的内存和运算,因此对于高并发服务器来说不是最好的方案。
I/O复用技术就是解决在不创建新进程的同时向多个客户端提供服务。

理解复用

为了提高物理设备的效率,用最少的物理要素传递最多数据时使用的技术 。
比如三个人用纸杯相互通信可以建立如下模型:
在这里插入图片描述
此时需要6个杯子和3根线。为了完成对话,说话时要对着两个杯子。并且通信链路是1对1的。如果设计成下面这种形式:
在这里插入图片描述
每个人使用一个杯子和他人通信。连接到杯子的电话线被复用了。为了不发生两个人一起说话的情形,可以约定时间段进行任意两个人交流。(时分复用技术)。但是不同人说话的频率不同,同时说话如果能够使用频率提取出两个人的声音也行。频分复用技术

复用技术在服务端使用

之前的多进程模型如下:
在这里插入图片描述
引入复用技术后:

在这里插入图片描述

select函数实现并发

I/O复用服务器端的进程需要确认举手 ( 收到数据 ) 的套接字,并通过举手的套接字接收数据。
运用select函数是最具代表性的实现复用服务器端方法。

select函数的功能和调用顺序

使用select函数时可以将多个文件描述符集中到一起统一监视,项目如下 。

  1. 是否存在套接字接收数据?
  2. 无需阻塞传输数据的套接字有哪些?
  3. 哪些套接字发生了异常?

上述监视项称为"事件"。发生监视项对应情况时,称 “发生了 事件” 。
select函数的使用方法与一般函数区别较大,更准确地说,它很难使用 。 但为了实现I/O复用服务器端,我们应掌握select函数,并运用到套接字编程中 。 认为 "select函数是1/0复用的全部内容"也并不为过 。 select函数的调用方法和顺序 。
在这里插入图片描述

设置文件描述符

利用select函数可以同时监视多个文件描述符 。 当然监视文件描述符可以视为监视套接字。此时首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述3种监视项分成3类 。

使用" 执行此项操作该数组是存有0和 1 的位数组。
在这里插入图片描述
最左端的位表示文件描述符fd0 (所在位置)。 如果该位设置为1,则表示该文件描述符是监视对象 。如上fd1和fd3是监视对象。在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 *创set) : 若参数fdset指向的变量中包含文件描述符fd的信息, 则返回"真"

设置检查 ( 监视〉范围及超时

select函数

int select(int maxfd, fd_set * readset, fd_set * writeset,fd_set * exceptset, const struct timeval * timeout);
/*
maxfd     监视对象文件描述符数量。
readset   将所有关注"是否存在待读取数据"的文件描述符注册到fd_set型变量,并传递其地址值。
writeset  将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set型变量,并传递其地址值。
exceptset 将所有关注"是否发生异常"的文件描述符注册到fd_set型变量,并传递其地址值。
timeout   调用select函数后,为防止陷入无限阻塞的状态,传递超时 ( time-out )信息。
返回值     发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
*/

select函数用来验证3种监视项的变化情况 。 根据监视项声明3个fd_set型变量,分别向其注册文件描述符信息,并把变量的地址值传递到上述函数的第二到第四个参数 。

但在此之前(调用select函数前)需要决定下面2件事 :

  1. 文件描述符的监视(检查)范围是?
  2. 如何设定select函数的超时时间?

第一 文件描述符的监视范围与select函数的第一个参数有关 。 实际上, select函数要求通过第一个参数传递监视对象文件描述符的数量 。 因此需要得到注册在fd_set变量中的文件描述符数。但每次新建文件描述符时,其值都会增1,故只需将最大的文件描述符值加 1再传递到 select函数即可。加1是因为文件描述符的值从0开始 。
第二 select面数的超时时间与select函数的最后一个参数有关,其中 timeval结构体定义如下 。

struct timeval
{
	long tv_sec;//seonds
	long tv_usec;//microseconds
};

本来select函数只有在监视的文件描述符发生变化时才返回 。 如果未发生变化,就会进入阻塞状态 。 指定超时时间就是为了防止这种情况的发生 。 通过声明上述结构体变量,将秒数填入tv_sec成员将毫秒数填入tv_usec成员,然后将结构体的地址值传递到select函数的最后一个参数 。此时,即使文件描述符中未发生变化.只要过了指定时间也可以从函数中返回 。不过这种情况下select函数返回 0。因此,可以通过返回值了解返回原因 。 如果不想设置超时,则传递NULL参数 。

调用 select 函数后查看结果

select函数的返回值,如果返回大于0的整数,说明相应数量的文件描述符发生变化 。文件描述符变化是指监视的文件描述符中发生了相应的监视事件。例如,通过 select 的第二个参数传递的集合中存在需要读数据的描述符时,就意味着文件描述符发生变化。

select函数返回正整数时,怎样获知哪些文件描述符发生了变化
在这里插入图片描述
select函数调用完成后,向其传递的fd_set变量中将发生变化 。 原来为1的所有位均变为0,但发生变化的文件描述符对应位除外 。因此可以认为值仍为1的位置上的文件描述符发生了变化。

select函数调用示例

#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);//初始化fd_set变量
	FD_SET(0, &reads); // 0 is standard input(console)需要监视标准输入的变化。

	/*
	timeout.tv_sec=5;
	timeout.tv_usec=5000;
	*/
	//不能在此处设置,因为执行到select 也花时间了。
	while(1)
	{
		temps=reads;//为了保存初始值,此时复制一个fd_set
		timeout.tv_sec=5;//设置超时
		timeout.tv_usec=0;
		result=select(1, &temps, 0, 0, &timeout);//如果控制台有输入则返回或者超时返回
		if(result==-1)
		{
			puts("select() error!");
			break;
		}
		else if(result==0)
		{
			puts("Time-out!");
		}
		else {
			if(FD_ISSET(0, &temps)) 
			{
				str_len=read(0, buf, BUF_SIZE);
				buf[str_len]=0;
				printf("message from console: %s", buf);
			}
		}
	}
	return 0;
}

基于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 100
void error_handling(char *buf);

int main(int argc, char *argv[]){
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	struct timeval timeout;
	fd_set reads, cpy_reads;

	socklen_t adr_sz;
	int fd_max, str_len, fd_num, i;
	char buf[BUF_SIZE];
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	FD_ZERO(&reads);
	FD_SET(serv_sock, &reads);
	fd_max=serv_sock;
//向要传到select函数第二个参数的fd_set变量reads注册服务器端套接字。这样,接
//收数据情况的监视对象就包含了服务器端套接字。客户端的连接请求同样通过传输
//数据完成。因此,服务器端套接字中有接收的数据,就意味着有新的连接请求。

	while(1){
		cpy_reads=reads;
		timeout.tv_sec=5;
		timeout.tv_usec=5000;

		if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)
			break;
		// 失败直接退出
		
		if(fd_num==0)//超时
			continue;

		for(i=0; i<fd_max+1; i++)
		{
		// select函数返回大于等于1的值时执行的循环。
			if(FD_ISSET(i, &cpy_reads))//查找发生状态变化的(有接收数据的套接字的)文件描述符 。
			{
				if(i==serv_sock)     //connection request! 有新的连接服务端套接字
				{
					adr_sz=sizeof(clnt_adr);
					clnt_sock= accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
					FD_SET(clnt_sock, &reads); //注册与客户端连接的套接字
					if(fd_max<clnt_sock)
						fd_max=clnt_sock;
					printf("connected client: %d \n", clnt_sock);
				}
				else    // read message!
				{
					str_len=read(i, buf, BUF_SIZE);//读客户端发来的套接字, 如果是EOF说明关闭。str_len = 0;
					if(str_len==0)    // close request!
					{
						FD_CLR(i, &reads);//接收的数据为EOF时需要关闭套接字,并从reads中删除相应信息 
						close(i);
						printf("closed client: %d \n", i);
					}
					else//写回客户端
					{
						write(i, buf, str_len);    // echo!
					}
				}
			}
		}
	}
	close(serv_sock);
	return 0;
}

void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

Q1. select函数的观察对象中应包含服务器端套接字(监听套接字);那么应将其包含到哪一类监听对象集合?请说明原因 。
response: 应将其包含在接收套接字的监听集合内,因为服务器套接字用来监听客户端是否发起请求连接。当有新的客户端发起请求连接时,其状态发生改变。由此可以判断是否有新的连接请求。复用服务器可以同时服务多个客户端,循环调用select函数,当fd_set中对应文件描述符(套接字)状态改变的时候,可以选择到进行服务。与多进程服务器端不同,基于select的复用服务器端只需要 1 个进程 。 因此,可以减少因创建进程产生的服务器端的负担。调用 select函数前需要集中I/O监视对象的文件描述符 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值