网络编程中Select的学习记录

网络编程中Select的学习记录

主要记录个人学习过程中,对select函数的理解和使用

为什么要使用Select

通过socket(),bind(),listen(),accept()就可以创建一个简单服务器时,但这种服务器在同一时间,只能有一个客户端与之连接。
为了能够使得一个服务器能够同时和多个服务器连接,可以通过多进程或多线程的方式。多进程的方式,会频繁的发生进程的创建和销毁,增加cpu和内存的的开销,线程与之类似。
因此可以使用I/O复用技术来解决这个问题,简单来说,I/O复用就是可以使用一个进程来处理多个I/O操作,具体解释,如下大佬说明:

IO复用、多进程和多线程三种并发编程模型

关于select函数的基本结构

这里只是简述,很多大佬已经解释的很详细了

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

nfds:一般传入监听的文件描述符中,最大的文件描述符+1
readfds:读 文件描述符监听集合 传入传出参数
writefds:写 文件描述符监听集合 传入传出参数
exceptfds:异常 文件描述符监听集合 传入传出参数
timeout: >0: 设置监听时常。注意:这里传入的不是一个整型变量,而是一个结构体,因此如果想设置监听时常,需定义一个结构体变量
NULL: 阻塞监听
0: 非阻塞监听,轮询

关于select函数的使用过程中,须知得几个函数

fd_set rset;							//定义一个监听集合
void FD_ZERO(fd_set *set);				//清空一个文件描述符集合
		FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set);		//将待监听得文件描述符,添加到监听集合中
		FD_SET(3, &rset);  
void FD_CLR(int fd, fd_set *set);		//将一个文件描述符从监听集合中移除
		FD_CLR(3, &rset);
int FD_ISSET(int fd, fd_set *set);		//判断一个文件描述符是否在监听集合中
		FD_ISSET(3, &rset);

select服务器的实现过程

select重点在设置的监听集合。

fd_set rset;	//通过该设置一个监听集合

在使用select函数前,和普通服务器的创建方法一样,通过socket,bind,listen创建出一个监听的文件描述符lfd。

	int lfd;
	struct sockaddr_in ser_addr,cli_addr;
	socklen_t cli_addr_len;

	memset(&ser_addr,0,sizeof(ser_addr));		//结构体初始化
	ser_addr.sin_family=AF_INET;
	ser_addr.sin_port=htons(SERV_PORT);
	ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	
	lfd = socket(AF_INET,SOCK_STREAM,0);
	if(lfd < 0)
		sys_err("socket error");
	
	int opt = 1;
	setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));		//端口复用

	if(bind(lfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr)))
		sys_err("bind error");

	if(listen(lfd,128))
		sys_err("listen error");

由于select的形参 readfds 是一个传入传出参数,因此在这个服务器中,设置了两个集合,一个为传入集合allset,一个为传出集合,rset。
在lfd创建之后,创建出了两个集合。
解释:为什么一个叫传入集合,一个叫传出集合。
传入集合,allset是:传给select函数的集合,表示有多少文件描述符被监听。
传出集合,rset是:select返回的集合,表示被被监听的文件描述符中,哪几个文件描述符有事件发生。这里集合的本质是位图。

传入集合初始化,并将lfd添加到传入集合中,从此以后,lfd不会从传入集合中移除。
然后传出集合初始化(此时的传出集合刚被传入集合初始化,所以作为参数传给select,当select返回后,传出集合就变成了真正的传出集合),并传给select函数,来记录被监控的文件描述符中哪些有事件发生。
lfd的作用是,当有新的客户端连接到服务端,lfd会被加入到传出集合中,然后通过遍历传出集合,操作accept函数,创建出新的文件描述符cfd,用于客户端与服务端的数据传输。

fd_set rset,allset;			//allset为传入集合 ,表示有多少文件描述符需要监听,rset为传出集合,表示监听到了有事件发生的文件描述符。
FD_ZERO(&allset);			//清空监听集合
FD_SET(lfd,&allset);		//将待监听的fd添加到监听集合中

rset = allset;				//传出集合通过传入集合初始化,然后传入到select函数中,根据事件发生改变	
ret = select(maxfd+1,&rset,NULL,NULL,NULL);		//使用select监听,返回值ret表示监听的集合中有几个事件发生,
												//例如,被监听的文件描述符有10个,而真正有事件发生的文件描述符只有5个,ret=5
if(FD_ISSET(lfd,&rset))			//有客户端请求链接,rset集合中有lfd
{
	cli_addr_len = sizeof(cli_addr);
	cfd = accept(lfd,(struct sockaddr *)&cli_addr,&cli_addr_len);		//调用accept创建链接,返回cfd
	if(cfd < 0)
		sys_err("accept error");

	FD_SET(cfd,&allset);		//将返回的fd加入到传入集合中,表示这个文件描述符被监听

整体代码如下(里面解释最详细):

嗯,注释尽量写的详细,主要看注释

/*
   select,多路I/O转接服务器
 */

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<ctype.h>
#include<arpa/inet.h>

#define SERV_PORT 9090 

void sys_err(const char *str)
{
	perror(str);
	exit(1);
}


int main(int argc,char *argv[])
{
	int lfd,cfd;
	int ret,i,n;
	char buf[BUFSIZ];

	struct sockaddr_in ser_addr,cli_addr;
	socklen_t cli_addr_len;

	memset(&ser_addr,0,sizeof(ser_addr));		//结构体初始化
	ser_addr.sin_family=AF_INET;
	ser_addr.sin_port=htons(SERV_PORT);
	ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	
	lfd = socket(AF_INET,SOCK_STREAM,0);
	if(lfd < 0)
		sys_err("socket error");
	
	int opt = 1;
	setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));		//端口复用

	if(bind(lfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr)))
		sys_err("bind error");

	if(listen(lfd,128))
		sys_err("listen error");

	fd_set rset,allset;		//allset为传入集合 ,表示有多少文件描述符需要监听,rset为传出集合,表示监听到了有事件发生的文件描述符。
	int maxfd = 0;			//定义最大文件描述符
	maxfd = lfd;
	

	FD_ZERO(&allset);			//清空监听集合
	FD_SET(lfd,&allset);		//将待监听的fd添加到监听集合中

	while(1)
	{
		rset = allset;		
		ret = select(maxfd+1,&rset,NULL,NULL,NULL);		//使用select监听,返回值ret表示监听的集合中有几个事件发生,
														//例如,被监听的文件描述符有10个,而真正有事件发生的文件描述符只有5个,ret=5
		if(ret < 0)
		{
			sys_err("select error");
		}
		/*
		**注意:此时的rset为传出集合,如果lfd在传出集合rset中,表示有连接事件发生,如果lfd不在传出集合中,表示没有连接事件发生。
			allset为传入集合,传入集合存储的是被监控的文件描述符,当某个文件描述符对应的事件发生了,则传出集合rset中会有该文件描述符,
			如果某个文件描述符对应的事件没有发生,则传出集合中就没有这个文件描述符,我们只需要处理传出集合rset中的事件即可。
			
			例如lfd,在传入集合allset中,一定会有lfd,表示lfd一定会被监听,至于监听了有没有连接事件发生,就不一定了。
			如果有新的客户端连接进来,则传出集合rset中会有lfd,然后通过accept函数,产生新的文件描述符cfd,然后将cfd加入到allset中(之所以
			加入allset中而不是rset中,是因为此时表示某个客户端连接进来,但是并没有发生数据的读写,加入到allset中表示这个客户端会受到监控)
			在本次循环中,会处理rset中的cfd,表示在这次循环中,这些cfd有数据的读写。**
		*/
		if(FD_ISSET(lfd,&rset))		//有客户端请求链接,rset集合中有lfd
		{
			cli_addr_len = sizeof(cli_addr);
			cfd = accept(lfd,(struct sockaddr *)&cli_addr,&cli_addr_len);		//调用accept创建链接,返回cfd
			if(cfd < 0)
				sys_err("accept error");

			FD_SET(cfd,&allset);		//将返回的cfd加入到传入集合中,表示这个文件描述符被监听

			if(maxfd < cfd)				//更改最大的cfd的值
				maxfd = cfd;
			//if(ret == 1)				//说明select的返回值为1,并且是lfd,表示该次while循环只有新客户端连接进来,而并没有发生客户端和服务端之间
											//的读写操作,因此接下来的for循环就不必在经历了,直接进行下次while循环。
				//continue;
		}

		for(i=lfd+1;i<maxfd+1;i++)		//处理满足读事件的cfd,此时表示有客户端和服务端之间的读写操作,遍历查找,i是文件描述符
		{
			if(FD_ISSET(i,&rset))		//找到满足读事件的cfd
			{
				n = read(i,buf,sizeof(buf));
				if(n == 0)				//检测到客户端关闭链接
				{
					close(i);			//关闭cfd
					FD_CLR(i,&allset);		//将关闭的cfd,从传入集合中除去,表示不再对此文件描述符进行监听
				}
				else if(n == -1)
					sys_err("read error");

				for(int j = 0;j<n;j++)
					buf[j]=toupper(buf[j]);

				write(i,buf,n);
				write(STDOUT_FILENO,buf,n);
			}
		}
	}


	return 0;
}

在代码中,maxfd的作用是为了select第一个参数的传递,同时减少在rset中的遍历次数,算是一种优化,如果不设置,就得从0循环到1023来查找哪些文件描述符有事件发生。
上述代码中,在判断select得返回值,ret的大小时,我的理解是,如果ret == 1,也有可能表示没有连接事件发生,只是有一个读写事件发生,因此我把源代码中的这个语句注释掉了,如果我理解有误,望大佬们不吝赐教。
以上为我对select函数的个人理解,如有错误,希望大佬们帮忙指正,提前感谢!!!!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值