第12章 I/O复用

12.1 基于I/O复用的服务器端

1.多进程服务器端的缺点和解决方法

缺点:

  • 只要有连接请求就创建新进程
  • 创建进程需要付出极大代价,需要大量的运算的内存空间
  • 每个进程有独立的内存空间,相互间的数据交换会采用相对复杂的方法(IPC通信比较复杂) 解决方法:I/O复用 并不是所有情况都适用,要根据服务器端特点调整。

2.理解复用

复用:用最少的物理设备,传递最多的数据使用的技术。 有时分复用,频分复用等,具体复用的介绍建议百度。

3.复用技术在服务器端的应用

服务端引入复用技术可以减少所需进程数。

多进程服务器: 引入复用技术服务器端: 无论连接多少客户端,提供服务的进程只有一个

12.2 理解select函数并实现服务器端

运用select函数是最具代表性的实现复用服务器端的方法。Windows平台下也有同名的函数提供,移植性良好。

1.select函数的功能和调用顺序

select函数可以将多个文件描述符集中到一起统一监视,监视项目:

  • 是否存在套接字接受数据

  • 无需阻塞传输数据的套接字有哪些

  • 哪些套接字发生异常

      监视项称为“事件”。发生监视项对应情况时,称“发生了事件”。
    

select函数的调用方法和顺序:

2.设置文件描述符

用select函数同时监视多个文件描述符。监视文件描述符可视为监视套接字。需要将要监视的文件描述符集中到一起,集中时按照监视项(传输、传输、异常)进行区分。用fd_set数组变量执行此操作 不同位置代表不同的文件描述符位置,如果这一位设为1说明是监视对象。 不能通过文件描述符的数字直接注册到fd_set变量中,针对fd_set变量的操作是以位为单位进行的。通过以下宏完成注册或者更改值:

  • FD_ZERO(fd_set * fd_set):将fd_set变量的所有位初始化为0。
  • FD_SET(int fd,fd_set * fdset):从参数fdset指向的变量中注册文件描述符fd的信息(将fd_set变量的fd位置为1)。
  • FD_CLR(int fd,fd_set * fdset):从参数fdset指向的变量中清除文件描述符fd的信息(将fd_set变量的fd位置为0)。
  • FD_ISSET(int fd,fd_set * fdset):从参数fdset指向的变量中包含文件描述符fd的信息,则返回‘真’。

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

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

对于Linux来说,第一个参数只需在每次新建文件描述符时加1即可,也就是将最大的文件描述符加1(从0开始因为)传递给函数。

struct timeval
{
    long tv_sec; //秒
    long tv_usec;//毫秒
}

select只有监视的文件描述符发生变化时返回。如果未发生变化会进入阻塞状态。所以为了防止阻塞发生要将秒数和微秒数填入timeval然后传递给select,超过了指定时间,select返回0,如果不想设置超时传NULL。

4.调用select函数后查看结果

函数调用完之后,除了发生监视的相应事件的文件描述符对应位置保持不变(1),其他的全都置为0。

5.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(0, &reads); // 0 is standard input(console)
    //监视标准输入
	/*
	timeout.tv_sec=5;//不能在此处设置超时,否则,每次调用select函数,超时时间会越来远短
	timeout.tv_usec=5000;
	*/

	while(1)
	{
		temps=reads;//复制预先设的fd_set变量
                    //因为每次select函数返回除了变化的文件描述符,其他都置为0,所以不能直接用reads
		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)) //因为除了变化的文件描述符对应位,都置为0(都被撤销了注册),可以通过FD_ISSET看是不是标准输入(还在被注册着)发生了变化
			{
				str_len=read(0, buf, BUF_SIZE);
				buf[str_len]=0;
				printf("message from console: %s", buf);
			}
		}
	}
	return 0;
}

6.实现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_set
	fd_max=serv_sock;//监视的文件描述符数量

	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++)
		{
			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);
					if(str_len==0)    // close request!说明传递过来的是EOF也就是文件尾,关闭该套接字,断开与该客户端的连接(如果str_len为-1,说明缓冲区没数据)
					{
						FD_CLR(i, &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);
}

12.3 基于Windows的实现

1.在Windows平台调用select函数

#include<winsock2.h>

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *excepfds,const struct timeval * timeout)

成功返回0,失败返回-1

基本意思和Linux相同

typedef struct timeval
{
    long tv_sec; //秒
    long tv_uses;//毫秒
} TIMEVAL

typedef struct fd_set
{
    u_int fd_count;
    SOCKET fd_array[FD_SETSIZE];

 } fd_set

fd_count是套接字句柄数,fd_array保存套接字句柄

Linux的文件描述符是从0开始,逐渐递增,因此很容易知道当前文件描述符数量和最后生成的文件描述符之间的关系。但是Windows套接字句柄不是从0开始,而且句柄整数值毫无规律,因此需要保存句柄数的变量和保存句柄值的数组。

2.基于Windows实现I/O复用服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAdr, clntAdr;
	TIMEVAL timeout;
	fd_set reads, cpyReads;

	int adrSz;
	int strLen, fdNum, i;
	char buf[BUF_SIZE];

	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)//注册套接字库版本
		ErrorHandling("WSAStartup() error!"); 

	hServSock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
	servAdr.sin_port=htons(atoi(argv[1]));
	
	if(bind(hServSock, (SOCKADDR*) &servAdr, sizeof(servAdrr))==SOCKET_ERROR)//注册服务套接字地址
		ErrorHandling("bind() error");
	if(listen(hServSock, 5)==SOCKET_ERROR)//允许服务套接字接收连接请求
		ErrorHandling("listen() error");

	FD_ZERO(&reads);
	FD_SET(hServSock, &reads);//注册服务端套接字文件描述符到reads(fd_set类型)

	while(1)
	{
		cpyReads=reads;//复制
		timeout.tv_sec=5;//设置超时时间
		timeout.tv_usec=5000;

		if((fdNum=select(0, &cpyReads, 0, 0, &timeout))==SOCKET_ERROR)//监视套接字句柄
			break;
		
		if(fdNum==0)//超时
			continue;

		for(i=0; i<reads.fd_count; i++)//从注册的套接字句柄中循环检查
		{
			if(FD_ISSET(reads.fd_array[i], &cpyReads))//该套接字是否还在被注册(是否发生了变化)
			{
				if(reads.fd_array[i]==hServSock)     // connection request!受理连接请求
				{
					adrSz=sizeof(clntAdr);
					hClntSock=
						accept(hServSock, (SOCKADDR*)&clntAdr, &adrSz);
					FD_SET(hClntSock, &reads);
					printf("connected client: %d \n", hClntSock);
				}
				else    // read message!
				{
					strLen=recv(reads.fd_array[i], buf, BUF_SIZE-1, 0);
					if(strLen==0)    // close request!读到EOF,说明客户端数据传输完成
					{
						FD_CLR(reads.fd_array[i], &reads);
						closesocket(cpyReads.fd_array[i]);
						printf("closed client: %d \n", cpyReads.fd_array[i]);
					}
					else//回声信息
					{
						send(reads.fd_array[i], buf, strLen, 0);    // echo!
					}
				}
			}
		}
	}
	closesocket(hServSock);
	WSACleanup();//注销套接字字库
	return 0;
}

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

本文章使用limfx的vscode插件快速发布