【网络】Select服务器的实现

本文详细介绍了Unix下的五种I/O模型,包括阻塞式I/O、非阻塞I/O、I/O复用、信号驱动I/O及异步I/O,并通过具体例子帮助读者更好地理解各模型的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

五种I/O模型

Unix下共有五种I/O模型,分别是

(1)阻塞式I/O;

(2)非阻塞I/O;

(3)I/O复用(select和(e)poll);

(4)信号驱动I/O(SIGIO);

(5)异步I/O(Posix.1的aio_系列函数);

阻塞I/O模型

应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。如果数据没有准备好,一直等待。数据准备好了,从内核拷贝到用户空间,表示IO结束,IO函数返回成功指示。

非阻塞I/O模型

我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试 数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。

I/O复用模型

该种模型又被称作是多路转接,I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

信号驱动I/O模型

首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

异步I/O模型

调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。也就是它只需要发起这个读写事件,不要等待与数据搬迁,只需要在结束之后得到成果。

举例

(1)张三在钓鱼,当鱼没有上钩时,便一直进行等待;当鱼上钩后,将鱼调出。这便是基本的阻塞式I/O

(2)张三在钓鱼,这次,当鱼没有上钩时,他便翻着看《C语言入门》这本书,也可以玩玩手机;当鱼上钩后,将鱼调出。这是非阻塞I/O

(3)张三在钓鱼,这次他放了一百个鱼竿。依次检查这些鱼竿,当一个鱼竿有鱼上钩后,将鱼调出,然后继续检查所有鱼竿。这是I/O多路复用

(4)张三钓鱼时,将鱼竿上放一个铃铛,当鱼上钩后会响。然后他便可以干自己的事情,当领响时,他知道上钩了,再去收鱼

(5)张三准备钓鱼,这次。。他直接雇了个人给自己钓鱼。这便是异步I/O

相关函数介绍

重定向的dup函数

头文件

#include<unistd.h>

函数原型

int dup(int oldfd);

int dup2(int oldfd, int newfd);

功能

dup 

将oldfd文件描述符进行一份拷贝,返回值为最新拷贝生成的文件描述符(最小的未被使用的文件描述符)

dup2 

使用newfd对oldfd文件描述符做一份拷贝,必要是可以先关闭newfd文件描述符

调用dup2之后,oldfd文件描述符不变,newfd和oldfd相等。

代码测试

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>

int main()
{
	umask(0);

	int fd = open("test.txt",O_CREAT|O_RDWR,0666);
	const char* msg = "hello world";
	printf("fd -> %d\n",fd);
	write(fd,msg,strlen(msg));
	int new_fd = dup(fd);//将fd进行拷贝,保存到new_fd中
	printf("new_fd -> %d\n",new_fd);
	write(new_fd,msg,strlen(msg));
	int cpfd = dup(1);//拷贝标准输出文件描述符
	dup2(fd,1);
	printf("nice\n");
	dup2(cpfd,1);
	printf("niec\n");
	return 0;
}

运行结果



select函数

头文件

#include<sys/time.h>

#include<sys/types.h>

#include<unistd.h>

函数原型

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

函数功能

同时等待多个文件描述符

参数

nfds

表示的是等待的文件描述符中的最大的那个+1;

fd_set

表示的是是否需要等待某个文件描述符,所以这里的fd_set底层是用位图实现的,所以我们最多可以等待的文件描述符的个数为sizeof(fd_set)*8;

readfds

表示的是需要等待的读事件的文件描述符集;

writefds

表示的是需要等待的写事件的文件描述符集;

exceptfds

表示的是需要等待的异常事件的文件描述符集;

timeout

表示的是每次select的时间为ty_sec秒

返回值

返回集合,该集合表示了收到消息的文件描述符

与fd_set有关的函数

fd_set为一个集合,底层是用位图进行实现的

它的每一位都可以用来表示对应的文件描述符是否收到了事件消息

#include <sys/time.h>  
#include <sys/types.h>  
#include <unistd.h>  
  
void FD_CLR(int fd, fd_set *set);//将文件描述符中的fd位去掉  
int  FD_ISSET(int fd, fd_set *set);//检测文件描述符集set中的fd位是否存在  
void FD_SET(int fd, fd_set *set);//为set文件描述符集设置fd为设置  
void FD_ZERO(fd_set *set);//将set文件描述符集清空  

Select模型

理解select模型的关键在于理解fd_set

假设fd_set长度为1字节

fd_set中的每⼀一bit

可以对应⼀一个⽂文件描述符fd

则1字节长的fd_set最⼤大可以对应8个fd

步骤:
(1)执⾏行fd_set set; FD_ZERO(&set);则set⽤用位表⽰示是0000,0000。

(2)若fd=5,执⾏行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加⼊入fd=2,fd=1,则set变为0001,0011

(4)执⾏行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发⽣生可读事件,则select返回,此时set变为0000,0011。注意:没有事件
发⽣生的fd=5被清空

也就是说,readfds[]数组来表示连接的客户端,该数组大小表示最多可以连接的数量

而rfds是一个集合,底层用位图实现

表示的是对应的文件描述符有没有收到对方发来的消息

若有,则对应的位上被置为1

代码实现

select服务器

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

int readfds[sizeof(fd_set)*8];
int writefds[sizeof(fd_set)*8];

static void Usage()
{
	printf("Usage: [ipaddr] [port]\n");
}

int startUp(char* ip, int port)
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd < 0)
	{
		perror("socket");
		exit(2);
	}

	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);

	if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)) < 0)
	{
		perror("bind");
		exit(3);
	}

	if(listen(sockfd,5)<0)
	{
		perror("listen");
		exit(4);
	}

	return sockfd;
}

int main(int argc, char* argv[])
{
	int i = 1;
	if(argc != 3)
	{
		Usage();
		exit(1);
	}

	int listen_sock = startUp(argv[1],atoi(argv[2]));

	int num = sizeof(fd_set)*8;
	writefds[0] = -1;
	readfds[0] = listen_sock;

	for(; i<num; i++)
	{
		writefds[i] = -1;
		readfds[i] = -1;
	}

	fd_set rfds,wfds;
	while(1)
	{
		int maxfd = -1;
		FD_ZERO(&rfds);
		FD_ZERO(&wfds);

		for(i = 0; i<num; ++i)
		{
			if(readfds[i] != -1)
			{
				FD_SET(readfds[i],&rfds);
			}

			if(writefds[i] != -1)
			{
				FD_SET(writefds[i],&wfds);
			}

			maxfd = readfds[i] > maxfd ? readfds[i] : maxfd;
			maxfd = writefds[i] > maxfd ? writefds[i] : maxfd;
		}

		struct timeval time = {1,0};
	
		int n = select(maxfd+1,&rfds,&wfds,NULL,&time);
		
		switch(n)
		{
			case 0:
				printf("time out...\n");
				break;
			case -1:
				break;
			default:
			{
				for(i = 0; i<num; ++i)
				{
					if(FD_ISSET(readfds[i],&rfds))
					{
						if(i == 0)
						{//监听服务器就绪
							struct sockaddr_in client;
							socklen_t len = sizeof(client);
							int client_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
						
							if(client_sock < 0)
							{
								perror("accpet");
								exit(5);
							}
							else
							{
								int tmp = 0;
								for(; tmp < num; ++tmp)
								{
									if(readfds[tmp] == -1)
									{
										readfds[tmp] = client_sock;
										break;
									}
								}

								if(tmp == num)
								{
									printf("readfds 满了\n");
									exit(6);
								}
							}
						}
						else
						{//等待的普通文件描述符就绪
							char buf[1024];	
							ssize_t s = read(readfds[i],buf,sizeof(buf));
							if(s < 0)
							{
								perror("read");
								exit(7);
							}
							else if(s == 0)
							{
								printf("客户端退出..\n");
								close(readfds[i]);
								readfds[i] = -1;
								continue;
							}
							else
							{
								buf[s] = 0;
								printf("#client: %s",buf);
								fflush(stdout);
								write(readfds[i],buf,strlen(buf));
							}
						}
					}
				}
			}
		}
	}

	return 0;
}

select客户端

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

static void Usage()
{
	printf("Usage: [ipaddr] [port]\n");
}

int main(int argc,char* argv[])
{
	if(argc != 3)
	{
		Usage();
		exit(1);
	}

	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd < 0)
	{
		perror("socket");
		exit(2);
	}

	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[2]));
	addr.sin_addr.s_addr = inet_addr(argv[1]);

	if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0)
	{
		perror("connect");
		exit(3);
	}

	printf("连接成功...\n");

	char buf[1024];
	while(1)
	{
		//发数据
		printf("#client: ");
		fflush(stdout);
		ssize_t s = read(0,buf,sizeof(buf)-1);
		if(s <= 0)
		{
			perror("read");
			exit(4);
		}

		int fd = dup(1);
		dup2(sockfd,1);
		printf("%s",buf);
		fflush(stdout);
		dup2(fd,1);
		
		//收数据
		s = read(sockfd,buf,sizeof(buf)-1);
		if(s == 0)
		{
			printf("服务器退出...\n");
			break;
		}
		else if(s < 0)
		{
			perror("read");
			exit(5);
		}
		else
		{
			buf[s-1] = '\0';
			printf("#server: %s\n",buf);
		}
	}
	close(sockfd);
	return 0;
}

select服务器优缺点

优点

1、不需要fork或者pthread_create就可以实现一对多的通信,简化了进程线程的使用

2、同时等待多个文件描述符,效率相对较高

缺点

1、每次调用select,都需要把fd集合从用户态拷贝到内核态,在fd很多的情况下,循环次数多;

2、每次调用select都需要在内核遍历传递进来的所有fd,开销比较大

3、select支持的文件描述符数量过小,默认是1024

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值