linux网络编程学习笔记之六 -----I/O多路复用服务端

多进程和多线程的目的是在于最大限度地利用CPU资源,当某个进程不需要占用太多CPU资源,而是需要I/O资源时,可以采用I/O多路复用,基本思路是让内核把进程挂起,直到有I/O事件发生时,再把控制返回给程序。这种事件驱动模型的高效之处在于,省去了进程和线程上下文切换的开销。整个程序运行在单一的进程上下文中,所有的逻辑流共享整个进程的地址空间。缺点是,编码复杂,而且随着每个逻辑流并发粒度的减小,编码复杂度会继续上升。

I/O多路复用典型应用场合(摘自UNP6.1)

select的模型就是这样一个实现,把每个客户的请求放入事件队列中,主线程通过非阻塞的I/O来处理他们。

select详细的用法和fd_set结构见:UNP的CH6

几个Tips

1、select在等待期间会被进程捕获的信号中断,从严谨的角度出发,应处理好EINTR错误

2、内核实际支持的时间分辨率比timeval结构的微秒级粗糙

3、select每次返回的是已就绪的总的描述位数,并把未就绪的位清0(三个fd_set参数都是值-结果参数)因此每次重新调用select时需重新对所有集合置1

一个服务器端程序的例子:

#include "simon_socket.h"

#define SERV_PORT 12345
#define FDSET_SIZE 32

typedef struct Clientinfo{
	int fd;
	struct sockaddr_in addr;
}Clientinfo;

typedef struct Clientpool{
	int count;
	Clientinfo cinfo_set[FDSET_SIZE];
}Clientpool;

void init_clientpool(Clientpool *pool)
{
	int i;
	pool->count = 0;
	memset(pool->cinfo_set, 0, sizeof(pool->cinfo_set));
	for (i = 0; i < FDSET_SIZE; i++)
		(pool->cinfo_set[i]).fd = -1;
}

void add_clientinfo(Clientpool *pool, int newfd, struct sockaddr_in client) // change
{
	int i;
	for (i = 0; i < FDSET_SIZE; i++)
	{
		if (pool->cinfo_set[i].fd < 0)
		{
			pool->cinfo_set[pool->count].fd = newfd;
			memcpy((char*)&(pool->cinfo_set[pool->count].addr), (char*)&client, sizeof(struct sockaddr_in));
			pool->count++;
			break;
		}
	}
}

int process_cli(Clientinfo cli)
{
	int recv_bytes, send_bytes;

	if ((recv_bytes = recv(cli.fd, recv_buf, MAX_BUF_SIZE, 0)) < 0)
	{
		perror("Fail to recieve data");
	}
	else if (!recv_bytes)
		return -1;

	printf("Success to recieve %d bytes data from %s:%d\n%s\n", recv_bytes, inet_ntoa(cli.addr.sin_addr), ntohs(cli.addr.sin_port), recv_buf);
	if ((send_bytes = send(cli.fd, recv_buf, recv_bytes, 0)) < 0)
	{
		perror("Fail to send data");
	}
	printf("Success to send %d bytes data to %s:%d\n%s\n", recv_bytes, inet_ntoa(cli.addr.sin_addr), ntohs(cli.addr.sin_port), recv_buf);

	return 0;
}

int main()
{
	int sockfd, retval, connfd, i, maxfd;
    size_t addr_len;
	struct sockaddr_in client_addr;
	fd_set fdset, watchset;
	Clientpool cpool;

	addr_len = sizeof(struct sockaddr);
	init_clientpool(&cpool);

	sockfd = init_tcp_psock(SERV_PORT);

	FD_ZERO(&fdset);
	FD_SET(sockfd, &fdset);
	maxfd = sockfd;

	for (; ;)
	{
		watchset = fdset;   //select 调用返回将修改fdset
		retval = select(maxfd+1, &watchset, NULL, NULL, NULL);  //两个同时连接,会不会排队?

		if (retval < 0)
		{
			perror("Select error");
			continue;
		}
		else
		{
			while (retval--)
			{
				if (FD_ISSET(sockfd, &watchset))
				{
					if ((connfd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len)) == -1)
					{
						perror("Fail to accept the connection");
						continue;
					}
					printf("Get a connetion from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
					FD_SET(connfd, &fdset);
					add_clientinfo(&cpool, connfd, client_addr);
					if ( connfd > maxfd ) maxfd = connfd;  //mark
				}
				else
				{
					for (i = 0; i < cpool.count; i++)
					{
						if (cpool.cinfo_set[i].fd < 0) //mark
							continue;
						if (FD_ISSET(cpool.cinfo_set[i].fd, &watchset))
						{
							if (process_cli(cpool.cinfo_set[i]) < 0)
							{
								printf("%s:%d quit the connection\n", inet_ntoa(cpool.cinfo_set[i].addr.sin_addr), ntohs(cpool.cinfo_set[i].addr.sin_port));
								FD_CLR(cpool.cinfo_set[i].fd, &fdset);
								close(cpool.cinfo_set[i].fd);
								cpool.count--;
								cpool.cinfo_set[i].fd = -1;
							}
						}
					}
				}
			}
		}
	}
	return 0;
}

由于select每次遍历地对描述字集合进行监测,当集合较大时,效率会受到极大的影响(随在线人数的线性递增呈二次乃至三次方下降)

Epoll的出现是作为 select的升级版,linux2.6以上都支持。它采用事件响应的方法,只遍历那些被内核I/O事件异步唤醒而加入Ready队列的描述符集合。因而显著减少了大量连接而只有少量活跃用户情况的系统CPU利用率。

epoll的接口函数很简单,只有三个:

#include<sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, intfd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

详情可以参考:

epoll的工作原理是,如果想进行I/O操作,先向epoll查询是否可读或者可写,如果处于可读或可写状态,epoll会调用epoll_wait函数进行通知,此时再进一步recv或send。

epoll仅仅是一个异步事件通知机制,其本身并不进行任何的I/O读写操作,它只负责通知是不是可读或可写了,而具体的读写操作将由应用层自己来做。这种方式保证了事件通知和I/O操作之间彼此的独立性。

epoll的两种模式:ET(边缘触发)和LT(水平触发)

采用ET模式,仅当状态发生变化时内核才会通知,而采用LT模式,类似与select,只要还有没处理的事件内核就会一直通知。因此,ET模式是通过减少系统调用来达到提高并行效率的目的的。另一方面,ET模式对编程要求高,需要细致地处理每个请求,否则容易发生事件丢失的情况。比如:对ET而言,accept调用返回时,除了建立当前这个连接外,不能马上就epoll_wait,还要继续循环accept,直到返回-1,且errno==EAGAIN,才不继续accept。LT在服务编写上的表现就对编码要求低一些:只要数据没有被获取,内核就会不断地进行通知,因此不必担心事件丢失的情况。如果调用accept时,有返回就可以马上建立这个连接,再调用epoll_wait等待下次通知,和select类似。

一个服务器端程序的例子:

#include"simon_socket.h"
#include<fcntl.h>
#include<sys/epoll.h>

#define SERV_PORT 12345
#define MAX_EPOLLFD  100
#define EVENT_SIZE 90

int set_fd_nonblocking(int fd)
{
	int flag;

	flag = fcntl(fd, F_GETFL, 0);
	if (flag == -1)
	{
		perror("fcntl error: ");
		return -1;
	}

	flag |= O_NONBLOCK;

	if (fcntl(fd, F_SETFD, flag) == -1)
	{
		perror("fcntl error: ");
		return -1;
	}
	return 0;
}

int main()
{
	int i, listenfd, contfd, epfd, readyfd, curfd = 1, recv_bytes;
	struct sockaddr_in cli_addr;
	struct epoll_event ev_tmp, events[EVENT_SIZE];

	size_t addr_len = sizeof(struct sockaddr);

	epfd = epoll_create(MAX_EPOLLFD);

	listenfd = init_tcp_psock(SERV_PORT);

	ev_tmp.data.fd = listenfd;
	ev_tmp.events = EPOLLIN | EPOLLET;
	if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev_tmp) == -1)
	{
		perror("Add event failed: ");
		return 1;
	}

	printf("Epoll server startup at port %5d\n", SERV_PORT);

	while(1)
	{
		readyfd = epoll_wait(epfd, events, EVENT_SIZE, -1);

		for (i = 0; i < readyfd; i++)
		{
			if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP))
			{
				perror("Epoll error: ");
				close(events[i].data.fd);
				continue;
			}
			else if (events[i].data.fd == listenfd)
			{
				if ((contfd = accept(listenfd, (struct sockaddr *)&cli_addr, &addr_len)) == -1)
				{
					perror("Accept request failed: ");
					return 1;
				}
				else
					printf("Get a connection from %s:%5d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));

				if (curfd > EVENT_SIZE)
				{
					printf("Too many connections, more than %d\n", EVENT_SIZE);
					continue;
				}

				set_fd_nonblocking(contfd);

				ev_tmp.data.fd = contfd;
				ev_tmp.events = EPOLLIN | EPOLLET;
				epoll_ctl(epfd, EPOLL_CTL_ADD, contfd, &ev_tmp);

				curfd++;
				continue;
			}
			else if (events[i].events & EPOLLIN)
			{
				if ((recv_bytes = recv(events[i].data.fd, recv_buf, MAX_BUF_SIZE, 0)) <= 0)
				{
					epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
					getpeername(events[i].data.fd, (struct sockaddr *)&cli_addr, &addr_len);
					printf("%s:%5d quit the connection\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
					close(events[i].data.fd);
					curfd--;
				}
				else
				{
                    ev_tmp.data.fd = events[i].data.fd;
                    ev_tmp.events = EPOLLOUT | EPOLLET;
                    epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &ev_tmp);
				}
			}
			else if (events[i].events & EPOLLOUT)
			{
				send(events[i].data.fd, recv_buf, recv_bytes, 0);

				ev_tmp.data.fd = events[i].data.fd;
				ev_tmp.events = EPOLLIN | EPOLLET;
				epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &ev_tmp);
			}
		}
	}
	close(listenfd);
	return 0;
}

linux网络编程学习笔记之六 -----I/O多路复用服务端

时间: 2024-10-03 23:10:04

linux网络编程学习笔记之六 -----I/O多路复用服务端的相关文章

TCP/IP网络编程 学习笔记_7 --基于UDP的服务端/客服端

理解UDP UDP套接字的特点:在笔记2中讲套接字类型有提,类似信件或邮件的传输.UDP在数据传输过程中可能丢失,如果只考虑可靠性,TCP的确比UDP好.但UDP在结构上比TCP更简洁.UDP没有ACK,SEQ那样的操作,因此,UDP的性能有时比TCP高出很多.编程中实现UDP也比TCP简单.另外,虽然UDP是不可靠的数据传输,但也不会像想象中那么频繁地发生数据丢失.因此,在更重视性能而非可靠性的情况下(如传输视频,音频时),UDP是一种很好的选择.而如果是传递压缩文件则必须要用TCP,因为压缩

linux网络编程学习笔记之二 -----错误异常处理和各种碎碎(更新中)

errno 在unix系统中对大部分系统调用非正常返回时,通常返回值为-1,并设置全局变量errno(errno.h),如socket(), bind(), accept(), listen().erron存放一个正整数来保存上次出错的错误值. 对线程而言,每个线程都有专用的errno变量,不必考虑同步问题. strerror converts to English (Note: use strerror_r for thread safety) perror is simplified str

linux网络编程学习笔记之五 -----并发机制与线程?

进程线程分配方式 简述下常见的进程和线程分配方式:(好吧,我仅仅是举几个样例作为笔记...并发的水太深了,不敢妄谈...) 1.进程线程预分配 简言之,当I/O开销大于计算开销且并发量较大时,为了节省每次都要创建和销毁进程和线程的开销.能够在请求到达前预先进行分配. 2.进程线程延迟分配 预分配节省了处理时的负担,但操作系统管理这些进程线程也会带来一定的开销.由此,有个折中的方法是,当某个处理须要花费较长时间的时候,我们创建一个并发的进程或线程来处理该请求.实现也非常easy,在主线程中定时,定

linux网络编程学习笔记之四 -----多线程并发服务端

相对于使用进程实现并发,用线程的实现更加轻量.每个线程都是独立的逻辑流.线程是CPU上独立调度运行的最小单位,而进程是资源分配的单位.当然这是在微内核的操作系统上说的,简言之这种操作系统的内核是只提供最基本的OS服务,更多参看点击打开链接 每个线程有它自己的线程上下文,包括一个唯一的线程ID(linux上实现为unsigned long),栈,栈指针,程序计数器.通用目的寄存器和条件码,还有自己的信号掩码和优先级.同一个进程里的线程共享这个进程的整个虚拟地址空间,包括可执行的程序文本.程序的全局

linux网络编程学习笔记之五 -----并发机制与线程池

进程线程分配方式 简述下常见的进程和线程分配方式:(好吧,我只是举几个例子作为笔记...并发的水太深了,不敢妄谈...) 1.进程线程预分配 简言之,当I/O开销大于计算开销且并发量较大时,为了节省每次都要创建和销毁进程和线程的开销.可以在请求到达前预先进行分配. 2.进程线程延迟分配 预分配节省了处理时的负担,但操作系统管理这些进程线程也会带来一定的开销.由此,有个折中的方法是,当某个处理需要花费较长时间的时候,我们创建一个并发的进程或线程来处理该请求.实现也很简单,在主线程中定时,定时到期,

linux网络编程学习笔记之三 -----多进程并发服务端

首先是fork()函数.移步APUE 8.3.  比較清晰的解释能够參考http://blog.csdn.net/lingdxuyan/article/details/4993883和http://www.oschina.net/question/195301_62902 补充一点是:fork返回后,原进程中的每一个文件或套接口描写叙述符的引用计数加1(相当于被多打开了一次),每调用一次close,引用计数减1,仅仅有当引用计数减到0时才会真正关闭该套接字. 可运行文件被linux运行的唯一方式

linux网络编程学习笔记之四 -----多-threaded服务器

对于使用过程中并发.通过实现更轻量级线程. 每个线程都是一个独立的逻辑流. 主题是CPU在执行调度的最小独立单位,这个过程是资源分配单元.当然,这是在微内核操作系统说.总之,这是唯一的一个操作系统内核提供了最重要的OS服务,许多人看点击打开链接 每一个线程有它自己的线程上下文.包含一个唯一的线程ID(linux上实现为unsigned long),栈,栈指针.程序计数器.通用目的寄存器和条件码,还有自己的信号掩码和优先级.同一个进程里的线程共享这个进程的整个虚拟地址空间,包含可运行的程序文本.程

linux 网络编程之最简单的tcp通信服务端

编写一个最为简单的tcp通信服务端.代码如下: #include <iostream> #include <cstring> using namespace std; #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <uni

转 网络编程学习笔记一:Socket编程

网络编程学习笔记一:Socket编程 “一切皆Socket!” 话虽些许夸张,但是事实也是,现在的网络编程几乎都是用的socket. ——有感于实际编程和开源项目研究. 我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的.本文的主要内容如下: