IO复用:select,poll,epoll,kqueue的例子

目录

一.服务器代码

1.select方式

2.poll方式

3.epoll方式

4.kqueue方式

二.重要总结

三.测试

1.服务编译与运行

2.客户端编译与运行

3.源码下载地址


一.服务器代码

以下各函数代码功能是接收客户端的数据并原样发送给客户端。

1.select方式

FD_ZERO(*fd_set),用来清空fd_set集合。

FD_SET(*fd_set,fd),把fd加到fd_set集合。

FD_ISSET(*fd_set,fd),测试fd是否在集合fd_set中。

int  select(int maxfd, fd_set * readset, fd_set * writeset,fd_set * excepset, struct timeval * tv);

参数maxfd是当前最大文件描述符+1。

参数readset是要测试的读文件描述符集合。

参数writeset是要测试的写文件描述符集合。

参数excset是要测试的异常描述符号集合。

参数tv是设置超时时间,如果是NULL将一直等待;如果结构体填0,那将立即查询并返回。

函数返回值:如果有就绪的文件描述符则返回其个数,如果超时则返回0,如果出差返回-1。

void echo_select(int listfd)
{
	sockaddr_storage cliaddr;
	socklen_t cliaddrlen = 0;

	fd_set fsetall;
	FD_ZERO(&fsetall);
	FD_SET(listfd, &fsetall);
	int maxfd = listfd;
	unordered_set<int> socketfds;
	char buff[1024];
	int nready = 0;
	for (;;)
	{
		fd_set fsr = fsetall;
		nready = select( maxfd + 1, &fsr, NULL, NULL, NULL);
		if(nready < 0)
		{
			nready = 0;
			sys_error("select");
		}
		
		if(FD_ISSET(listfd, &fsr))
		{
			bzero(&cliaddr, sizeof(cliaddr));
			int confd = accept(listfd, (sockaddr*)&cliaddr, &cliaddrlen);
			if(confd <= 0)
				sys_error("accept");
			if(confd > maxfd)
				maxfd = confd;
			PrintClientInfo((sockaddr*)&cliaddr);
			socketfds.insert(confd);
			FD_SET(confd, &fsetall);
			if(--nready == 0)
				continue;
		}
		for(auto iter = socketfds.begin(); iter != socketfds.end();)
		{
			int clifd = *iter;
			if(FD_ISSET(clifd, &fsr))
			{
				ssize_t readn = read(clifd, buff, sizeof(buff));
				if(readn <= 0)
				{
					int errnum = errno;
					if(errnum != 0)
					{
						if(EAGAIN == errnum || EINTR == errnum || EWOULDBLOCK == errnum)
							continue;
						else if(ECONNRESET == errnum)
						{
							// client close. nothing to do.
						}
						else
							cout << "errno:"<< errnum <<" " << strerror(errnum) << " readn:" << readn << endl;
					}
					
					iter = socketfds.erase(iter);
					FD_CLR(clifd, &fsetall); //unregister
					close(clifd);
				}
				else if(readn > 0)
				{
					iter++;
					ssize_t wn = write(clifd, buff, readn);
					if(wn < 0)
						sys_error("socket write");
				}
				
				if(--nready == 0)
					break;
			}
			else
				iter++;
		}
	}

}

2.poll方式

struct pollfd {

int     fd;  //需要测试的文件描述符,-1则跳过测试

short   events; //POLLIN,POLLOUT 需要测试的条件

short   revents; //POLLERROR 的返回可不需要在events中填写

};

int poll(struct pollfd * fds, nfds_t fdslen, int time)

参数fds和fdslen指定了需要测试的struct pollfd数组。

参数time设置超市时间。如果-1则一直等待直到有一个描述符就绪,0立即返回。

函数返回值:如果有就绪的文件描述符则返回其个数,如果超时则返回0,如果出差返回-1。

void echo_poll(int listfd)
{
	struct pollfd fds[OPEN_FD_MAX];
	fds[0].fd = listfd;
	fds[0].events = POLLIN;
	for(int i = 1; i < arraysize(fds); ++i)
		fds[i].fd = -1;
	
	int realsize = 1;
	int nready = 0;
	sockaddr_storage cliaddr;
	socklen_t cliaddrlen = 0;
	char buff[1024];
	for(;;)
	{
		assert(nready == 0);
		nready = poll(fds, realsize, -1);
		int kk = nready;
		if(nready < 0)
		{
			sys_error("poll");
			nready = 0;
		}
		
		if(fds[0].revents & POLLIN)
		{
			bzero(&cliaddr, sizeof(cliaddr));
			int confd = accept(listfd, (sockaddr*)&cliaddr, &cliaddrlen);
			if(confd <= 0)
				sys_error("accept");
			PrintClientInfo((sockaddr*)&cliaddr);
			
			//绑定新的连接到一个空闲的位置
			for(int i = 1; i < arraysize(fds); ++i)
			{
				if(fds[i].fd == -1)
				{
					fds[i].fd = confd;
					//fds[i].events = (POLLIN | POLLERR);// 不需要设置监听POLLERR,当POLLERR发生时总会在fds[i].revents中返回
					fds[i].events = POLLIN;
					if(i >= realsize)
						realsize = i + 1;
					break;
				}
			}
			
			if(--nready == 0)
				continue;
		}
		
		for(int i = 1; i < realsize; ++i)
		{
			if(fds[i].fd == -1)
				continue;
			if((fds[i].revents & (POLLIN | POLLERR)) > 0 )
			{
				while(true)
				{
					ssize_t readn = read(fds[i].fd, buff, sizeof(buff));
					if(readn > 0)
					{
						write(fds[i].fd, buff, readn);
					}
					else
					{
						int errnum = errno;
						if(errnum != 0)
						{
							if(EINTR == errnum)
								continue;
							else if(EWOULDBLOCK == errnum || EAGAIN == errnum)
								break;
							else if(ECONNRESET == errnum)
							{
								// client close. nothing to do.
							}
							else
								cout << "errno:"<< errnum <<" " << strerror(errnum) << " readn:" << readn << endl;
						}
						
						close(fds[i].fd);
						fds[i].fd = -1; //unregister
					}
					break;
				}
				if(--nready == 0)
					break;
			}
		}
		assert(nready == 0);
	}
	
}

3.epoll方式

epoll_create打开一个epoll文件描述符,供下面2函数使用。

iepoll_ctl添加、修改、删除需要测试的文件描述符。

epoll_wait进行测试。

/*
 EPOLLET模式。
 1.如果用这个模式监听listfd,需要while accept。
 2.如果用这个模式监听socketfd,需要while read
 3.如果使用while相应的得使用fcntl设置成非阻塞模式
 */

void echo_epoll(int listfd, bool bet)
{
	int epfd = epoll_create(OPEN_FD_MAX);
	epoll_event tmpet;
	tmpet.data.fd = listfd;
	if(bet)
	{
		tmpet.events = EPOLLIN|EPOLLET;
		SetNoBlock(listfd);
	}
	else
		tmpet.events = EPOLLIN;
	int retepollctl = false;
	if(epoll_ctl(epfd, EPOLL_CTL_ADD, listfd, &tmpet) < 0)
		sys_error("epoll_ctl");
	epoll_event epoll_events[OPEN_FD_MAX];
	sockaddr_storage cliaddr;
	socklen_t cliaddrlen = 0;
	char buff[1024];
	for(;;)
	{
		int nready = epoll_wait(epfd, epoll_events, OPEN_FD_MAX, -1);
		if(nready < 0)
		{
			sys_error("epoll_wait");
			nready = 0;
			continue;
		}
		for(int i = 0; i < nready; i++)
		{
			epoll_event* pitem = &epoll_events[i];
			if(pitem->data.fd == listfd)
			{
				do{
					bzero(&cliaddr, sizeof(cliaddr));
					int confd = accept(listfd, (sockaddr*)&cliaddr, &cliaddrlen);
					if(confd < 0)
					{
						if(errno == EWOULDBLOCK)
							break;
						else
							sys_error("accept");
					}
					PrintClientInfo((sockaddr*)&cliaddr);
					tmpet.data.fd = confd;
					if(bet)
					{
						tmpet.events = EPOLLIN | EPOLLET;
						SetNoBlock(confd); // EPOLLET must use noblock.
					}
					else
						tmpet.events = EPOLLIN;
					
					if(epoll_ctl(epfd, EPOLL_CTL_ADD, confd, &tmpet) < 0)
						sys_error("epoll_ctl");
				}
				while(bet);
			}
			else
			{
				do
				{
					int readn = read(pitem->data.fd, buff, sizeof(buff));
					if(readn <= 0)
					{
						int errnum = errno;
						if(errnum != 0)
						{
							if(EINTR == errnum)
								continue;
							if(EAGAIN == errnum  || EWOULDBLOCK == errnum)
								break;
							else if(ECONNRESET == errnum)
							{
								// client close. nothing to do.
							}
							else
								cout << "errno:"<< errnum <<" " << strerror(errnum) << " readn:" << readn << endl;
						}
						
						// step1. if first call close, epoll_ctl will  call error, errno to bo Bad file descriptor.
						tmpet.data.fd = pitem->data.fd;
						tmpet.events = EPOLLIN | EPOLLET;
						if(epoll_ctl(epfd, EPOLL_CTL_DEL, pitem->data.fd, &tmpet) < 0)
							sys_error("epoll_ctl");
						// step2.
						close(pitem->data.fd);
					}
					else
						write(pitem->data.fd, buff, readn);
				}
				while (bet);				
			}
		}
	}
 
	
}

4.kqueue方式

kqueue();

int     kevent(int kq,  //kqueue();返回的文件描述符

const struct kevent *changelist, //需要测试的列表

int nchanges,//需要测试的列表的元素个数

struct kevent *eventlist,//监测到就绪后返回的列表

int nevents,//返回列表的元素个数

 const struct timespec *timeout);//超时时间,NULL一直等待,0立马返回

void echo_kqueue(int listfd, bool bet)
{
	int kq = kqueue();
	struct kevent ke;	
	if(bet)
		EV_SET(&ke, listfd, EVFILT_READ, EV_ADD|EV_CLEAR, 0, 0, 0); 
	else
		EV_SET(&ke, listfd, EVFILT_READ, EV_ADD, 0, 0, 0); 
	
	kevent(kq, &ke, 1, NULL, 0, NULL);
	SetNoBlock(listfd);
	
	struct kevent eventlist[1024];
	sockaddr_storage cliaddr;
	socklen_t cliaddrlen = 0;
	char buff[1024];
	for(;;)
	{
		int nready = kevent(kq, NULL, 0, eventlist, arraysize(eventlist), NULL);
		if(nready < 0)
			sys_error("kevent");
		
		for(int i = 0;i < nready; ++i)
		{
			struct kevent act = eventlist[i];
			int actfd =  (int)act.ident;
			if(actfd == listfd)
			{
				bzero(&cliaddr, sizeof(cliaddr));
				do 
				{
					int confd = accept(listfd, (sockaddr*)&cliaddr, &cliaddrlen);
					if(confd > 0)
					{
						PrintClientInfo((sockaddr*)&cliaddr);
						if(bet)
						{
							EV_SET(&ke, confd, EVFILT_READ, EV_ADD|EV_CLEAR, 0, 0, 0);
							SetNoBlock(confd);
						}
						else
							EV_SET(&ke, confd, EVFILT_READ, EV_ADD, 0, 0, 0);
						kevent(kq, &ke, 1, NULL, 0, NULL); // 如果返回列表设置为0,函数会立马返回,相当于只注册,类似epoll_ctl。
					}
					else
					{
						if(errno == EWOULDBLOCK)
							break;
						else
							sys_error("accept");
					}
				}while(bet);
			}
			else
			{
				do
				{
					ssize_t readn = read(actfd, buff, sizeof(buff));				
					if(readn > 0)
					{
						write(actfd, buff, readn);
					}
					else
					{
						int errnum = errno;
						if(errnum != 0)
						{
							if(EINTR == errnum)
								continue;
							if(EAGAIN == errnum || EWOULDBLOCK == errnum)
								break;
							else if(ECONNRESET)
							{
								//client close. nothing to do.
							}
							else
								cout << "errno:"<< errnum <<" " << strerror(errnum) << " readn:" << readn << endl;
						}
						
						/*
						 EV_DELETE  Removes the event from the kqueue.  Events which are attached
						 to file descriptors are automatically deleted on the last close of	the descriptor.
						 */
						
						//manual unregister. in this example this step can cancel.
						EV_SET(&ke, actfd, EVFILT_READ, EV_DELETE, 0, 0, 0);
						kevent(kq, &ke, 1, NULL, 0, NULL);
						
						//close will auto unregister the kevent.
						close(actfd);
						
					
						break;
					}
				}while(bet);
			}
		}
	}
}

二.重要总结

1.设置非阻塞时,不能影响原来标志位.

int flag = fcntl(fd, F_GETFL, 0);

fcntl(fd, F_SETFL, flag|O_NONBLOCK);

2.listen(int listenfd,int backlog);backlog参数是设置新连接队列的长度限制,即半连接队列(未完成3次握手)和全连接队列的连接数总和。

backlog=0,在不同系统中实现不同,可尝试用 char* ptr = getenv("LISTENQ");取环境变量的值。

3.io阻塞函数会被系统信号中断,errno被设置为EINT.

4.如果一个socket一端已经关闭,另一端第一次写会返回ECONNRESET,再次写会EPIPE。可捕获信号.

fcntl(socketfd, F_SETOWN, getpid()); //先设置属主

signal(SIGPIPE, del_pipe);//再设置信号捕捉函数

5.select每次调用会修改fd_set,所以每次调用前需要重设需要监听的fd_set.

6.poll的events无需设置监听POLLERR,当错误产生时会在revents中返回,移除监听把fd设置为-1即可.

7.epoll用ET模式时一次事件只触发一次,所以需要用while循环,用循环为了避免阻塞需要把描述符设置为非阻塞.

例如socket缓冲区收到100字节,读取50字节后,调用epoll_wait,如果是ET模式不会再次触发,非ET模式会再次触发。如果监听套接字也是ET模式,accept不用while循环的话,只能获取一个就绪连接,导致其他的连接不能及时响应。

8.kqueue的EV_CLEAR类似epoll的POLLET.

9. kqueue的kevent函数类似epoll的epoll_ctl和epoll_wait二者组合.

kevent(int kq,const struct kevent *changelist, int nchanges,struct kevent *eventlist, int nevents, const struct timespec *timeout);

如果参数nchanges不为0,nevents设置为0,相当于注册类似epoll_ctl,如果参数nchanges为0,nevents不为0,相当于等待事件触发类似epoll_wait。

10.epoll和kqueue中当描述符要关闭时,确保在调用close前先unregister事件.

在epoll中,先close(fd);再epoll_ctl(,EPOLL_CTL_ADD,fd)。如果fd是最后的关闭(比如fork需要父子进程都close),此处epoll_ctl将出错,errno=EBADF,Bad file descriptor。

在kqueue中 Events which are attached to file descriptors are automatically deleted on the last close of the descriptor.

11.服务器使用getaddrinfo函数,这个函数把协议相关性隐藏在库函数内部,适配ipv4和ipv6.

12.kqueue官方文档

https://man.freebsd.org/cgi/man.cgi?kqueue#EXAMPLES

三.测试

写了个客户端程序,同时启动100个线程模拟并发,进行数据的发送和读取。

1.服务编译与运行

通过参数指定用哪种模式运行。

注意select,poll是跨平台的, epoll是linux实现,kqueue是macos实现。

参数epoll,epollet,kqueue,kqueueet分别模拟了水平触发模式和边缘触发模式。

[root@local apue.3e]# g++ service.cpp -o service
[root@local apue.3e]# ./service epollet
run epoll_et model.
^C
[root@local apue.3e]# ./service poll
run poll model.
^C
[root@local apue.3e]# ./service select
run select model.
^C
[root@local apue.3e]# ./service epoll
run epoll model.
yadou@yadou-mac Debug % ./service kqueue
run kqueue model.
^C
yadou@yadou-mac Debug % ./service kqueueet
run kqueue_et model.

2.客户端编译与运行

因为使用了多线程所以编译需要链接线程库,-lpthread.

[root@local apue.3e]# g++ client.cpp -lpthread -o client
[root@local apue.3e]# ./client 
finish:100 success:100 fail:0
send:204500
recive:204500

3.源码下载地址

https://download.csdn.net/download/yadoufeng/87743624

下载源码后可以修改服务器和客户端的缓冲大小,观察边缘触发模式和水平触发模式。

以验证重要总结第7点。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值