I/O多路复用 select模型

情景分析:

现在要编写一个echo服务器,就是接收每个客户端的链接,打印出客户端发送的一串文本。同时,再增加一项功能,可以处理标准输入。所以,服务器必须能够响应两个独立的I/O事件:

1:来自客户端发起的socket连接

2:标准输入

也就是问题转化为:如何同时监听多个文件描述符?

如何同时监听多个文件描述符?

解决这种问题的办法之一就是: I/O多路复用
什么是I/O多路复用?简单的说就是,我们告诉操作系统(内核),我要同时监听多个文件描述符,不管是哪一个,只要有事件发生(例如:可读),就告诉我。
既然是监听多个文件描述符,那么肯定要有一个可以表示集合的东西,我们向操作系统传递这个希望监听的集合,然后操作系统把集合中有事件发生的文件描述符告诉我们,这就是fd_set,现在可以把fd_set认为是一个“存储”多个文件描述符的结构。
向操作系统传递集合,以及通知我们的就是select函数。

fd_set简介

fd_set可以认为是一个位向量,假设fd_set是一个10个bit位的位向量( linux中宏FD_SETSIZE代表真实的bit位数量),那么可以告诉操作系统去监控的描述符集合就是0---9。第N位为1,则表示告诉操作系统去监控描述符N,或者操作系统告诉我们描述符N有事件发生。
如图所示,假设我们要告诉操作系统去监控标准输入(STDIN_FILENO),描述符4和描述符7,那么我们就是fd_set的对象fd_set_obj的第0位,第4位,和第7位置为1,其余位置为0。并把fd_set_ob传给操作系统。然后,假设标准输入(STDIN_FILENO)和描述符4有事件发生,那么操作系统将会再把这个fd_set_obj传回给我们,其中的第0位和第4位为1,其余位为0,我们可以通过检查第0位,第4位,第7位中哪一位为1,从而知道哪一个描述符有事件发生了。
上面只是方面理解。我们不能自己去判断每一个bit位是0或1,而应该通过系统提供的函数。

#include <unistd.h>
#include <sys/types.h>

FD_ZERO(fd_set *fdset); //把fdset中所有bit位置为0
FD_CLR(int fd, fd_set *fdset); //把fdset中第fd个bit位置为0
FD_SET(int fd, fd_set *fdset); //把fdset中第fd个bit位置为1
FD_ISSET(int fd, fd_set *fdset); //判断fdset中第fd个bit位是否为1
fd_set 隐藏的另一个含义就是:第N个bit位对应于描述符N。
现在我们已经了解了,如何表示和操作描述符集合,那么如何告诉操作系统以及操作系统如何告诉我们呢?
这就是下面的select函数。

select函数

这个函数是 阻塞的。
#include <unistd.h>
#include <sys/types.h>

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

参数介绍

nfds:监控的描述符中最大的那么描述符加1,这句话显然不太好读啊。假设要监控0,1,5这3个描述符,那么就传入6,之所以有这个参数是为了效率,如果fd_set有1000个bit位,那么将可以同时监控1000个描述符,但是,我们只是想监控0,1,5这三个描述符而已,显然操作系统轮询查询6个(0---5)描述符要比轮询查询1000个(0---999)描述符要快。这就是这个参数的意义:明确指明轮询几个描述符(最大的描述符加1)。
readfds:希望监控的有可读事件的描述符,传入select时指明希望监控的读集合,select返回时指明有读事件发生,即可读的描述符集合。
writefds:希望监控的有可写事件的描述符。。。。。。。同上。。。
exceptfds:希望监控的有异常事件的描述符。。。。。。。。同上。。。
timeout:阻塞的事件,如果这个时间内没有事件发生,那么超时返回。传入NULL,则无休止阻塞。要注意此函数会修改timeout参数,所以每次调用select都要重新初始化timeout。

返回值

负值:select发生了错误
0:超时
正值:有事件发生

例子

这个例子中,服务器监控两个I/O事件
1:客户端的连接请求,每当有客户端请求连接时,就接收连接,打印客户端发过来的文本,并主动关闭该连接。
2:标准输入,当在标准输入键入一行文本并按回车时,打印出该行文本。
所以就是同时监控2个描述符:STDIN_FILENO和服务器端监听的套接口。

服务器

#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

//检查main函数参数,获取监听端口,失败返回-1,成功返回端口
int check_arg(int argc, char *argv[]);
//创建一个TCP套接子,并监听,如果成功则返回已经被监听的套接字
//如果失败,返回-1
int open_listened(int port);
//接受客户端的内容,打印出来,然后关闭连接
void echo_fd(int fd);
//标准输入可读
void echo_cmd();
int main(int argc, char *argv[])
{
	int port = check_arg(argc, argv);
	if(port == -1) {
		exit(1);
	}
	int fd = open_listened(port);
	if(fd == -1) {
		printf("%s\n", "open_listened error");
		exit(1);
	}
	while(1) {
		fd_set read_set;
		FD_ZERO(&read_set);
		FD_SET(STDIN_FILENO, &read_set);
		FD_SET(fd, &read_set);
		if(-1 == select(fd+1, &read_set, NULL, NULL, NULL)) {
			printf("%s\n","select error");
			return 0;
		}
		if(FD_ISSET(fd, &read_set)) //监听的套接口可读
			echo_fd(fd);
		if(FD_ISSET(STDIN_FILENO,&read_set)) //标准输入可读
			echo_cmd();
	}
	return 0;
}

int check_arg(int argc, char *argv[])
{
	if(argc !=2 ) {
		printf("Usage: %s <port>\n", argv[0]);
		return -1;
	}
	return atoi(argv[1]);
}
int open_listened(int port)
{
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd == -1) {
		printf("%s\n", "create socket error");
		return -1;
	}
	struct sockaddr_in socksrv;
	bzero(&socksrv, sizeof(socksrv));
	socksrv.sin_family = AF_INET;
	socksrv.sin_addr.s_addr = htonl(INADDR_ANY);
	socksrv.sin_port = htons(port);
	int result = bind(fd, (struct sockaddr*)&socksrv, sizeof(socksrv));
	if(result == -1) {
		printf("%s\n", "bind error");
		return -1;
	}
	result = listen(fd,5);
	if(result == -1) {
		printf("%s\n","listen error");
		return -1;
	}
	return fd;
}
void echo_fd(int fd)
{
	int connfd = accept(fd, NULL, NULL);
	char buf[100] = {0};
	while(1) {
		int nread = read(connfd, buf, sizeof(buf));
		if(nread == -1) {
			if(errno == EINTR) //interrupted
				continue;
			else
				exit(1); //error
		} else if(nread == 0) {
			close(connfd); //EOF
			return;
		} else {
			printf("%s\n", buf);
			close(connfd);
			break;
		}
	}
}
void echo_cmd()
{
	char buf[100] = {0};
	int nread = read(STDIN_FILENO, buf, sizeof(buf));
	if(nread ==0) {
		exit(1); //EOF
	} else {
		printf("%s", buf);
	}
}

客户端

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	if(argc != 2) {
		printf("Usage: %s <port>\n", argv[0]);
		exit(1);
	}
	int port = atoi(argv[1]);
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in socksrv;
	bzero(&socksrv, sizeof(socksrv));
	socksrv.sin_family = AF_INET;
	socksrv.sin_addr.s_addr = htonl(INADDR_ANY);
	socksrv.sin_port = htons(port);
	connect(fd, (struct sockaddr*)&socksrv, sizeof(socksrv));
	write(fd,"hello select test!", strlen("hello select test!"));
	char buf[100] = {0};
	int nread = read(fd, buf, sizeof(buf));
	if(nread == 0) {
		printf("%s\n","server closed!");
		close(fd);
	}
	return 0;
}




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值