一、简单的服务器I/O模型


    最简单的的TCP服务器,有三种模式:

1、单执行流,一个server端连接一个client端

2、多进程,一个server端通过多进程的方式,每个进程连接一个client端

3、多线程,一个server端通过多进程的方式,每个线程连接一个client端

http://zhweizhi.blog.51cto.com/10800691/1830267) 这里实现过 


    要提升服务器性能,其实就是想要让一个server端能在负载允许的情况下,连接尽可能多的client端。

因此,以上三种模式中:

第一种模式,一个服务器连接一个客户端  ~!@#%……   这是低性能服务器 是没人会考虑的

第二种模式,用多进程,能同时服务多个client端,不过线程开销较大,因此这也不是最好的方式

第三种模式,用多线程,线程的开销比进程要小,因此这种方式是 这三种方式中,最优的方式。

 

二、所谓高性能

    我们仔细分析第三种模式下,server端每个线程和对应连接的client在进行通信的过程中,其实都是阻塞方式的。

    也就是,对于server端的每个线程,在对面客户端端有消息的时候(连接、接受、断开时)进行处理,而当对面没有消息过来的时候,则一直等待。

    因此,在这种阻塞模式下,这些等待的时间被白白浪费掉了。归根到底,每个线程也只能服务一个客户端,因此还不够高性能。要想提高性能,就是要尽可能减少等待时间,也就是说,服务器最高效的工作状态是: 在能力范围内一直在进行数据搬迁。

    要想进一步提升服务器性能,达到上述的工作状态,条件允许时(大部分情况都是如此)自然需要换一种效率更高的I/O模型,这里有以下模型:

    //1、非阻塞I/O;
    //2、I/O复用
    //3、信号驱动I/O
    //4、异步I/O

 以上在这些模型当中,效率最高的当属第二种:I/O复用模型。

 关于I/O复用,客官且听我慢慢道来



三、所谓I/O复用

 I/O复用,是针对一个单线程而言的,采用I/O复用的server端,一个线程就可以处理多个client端。

   它的实现,就好像有一个"管家",这个"管家"被托管了许多个套接字。当有新的client端要连接的时候,把这个client端(的文件描述符)托管给这个“管家”,所以这个“管家“上很有可能被托管了许多个client端。

    管家的任务就是:管理他托管的这些套接字,如果这些套接字有消息传来,就通知server。

    而server则平时一直等待它的"管家",直到"管家"告诉它有消息,才做相应的处理。

    这样,server端的一个线程,就能同时服务许多client端,相比多线程模型,线程用来等待时间的比例明显低了很多,效率也高了许多。


    要实现I/O复用,有三种模式:分别是: select、poll、epoll,以下分别介绍。


 


三、I/O复用——select 和 poll


    别看poll 和 epoll 名字就差一个e,但其实 poll 和 select 更像,所以这里就把这两个模型放在一起介绍了。

(1)、select模式

  还记得前面说到的那个"管家"吗?现在这里管家就是select,管家有一个(或多个)名单fd,fd上记录着所有被托管着的文件描述符


    //文件描述符被保存在fd_set类型的变量中。fd_set中存放的文件描述符,都是通过位运算的方式存放的。


   select模式,用到的函数有这些:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,const struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);    //删除指定文件描述符
int  FD_ISSET(int fd, fd_set *set);     //判断指定文件描述符是否在fd中
void FD_SET(int fd, fd_set *set);        //向select表中添加指定元素
void FD_ZERO(fd_set *set);                 //清空select表

     后面4个FD开头的函数作用在注释中;


     select函数需要接收5个参数:

        第1个参数为select表中所记录的元素的数量之和 + 1;

        第2~4个参数为3个fd_set类型的地址,也就是三张表的地址,简单的说,select执行的时候,关

     心的也正是这3张表中文件描述符对应的目标的读信号、写信号、异常信号。

        最后一个参数是一个struct timeval类型的结构体指针,表示等待时间。传NULL表示阻塞,当有信号时才返回,传0表示非阻塞,传具体的经过初始化的对象则表示等待指定的时间后返回.

        另外,在使用select函数的时候要特别注意,一旦使用这3个结构体指针在传参后,select在收到信号的时候,相应的文件描述符的值就会发生变化,而该文件描述符所对应的结构体会发生改变。

        也就是说,使用完select之后,这3个结构体实际上已经不是之前的了,因此需要保存和还原。


        由于我们通常要向select中保存多个文件描述符,所以不妨利用一个辅助数组fd_arr,用来保存和恢复这些文件描述符,每次使用FD_SET添加的时候,也把这个文件描述符存到数组中,并初始化为-1.

        

        当超时或收到信号时select会返回它所保存的元素中有信号的元素的个数。当出错时返回-1,如果返回0则说明没有信号产生。


        如果返回值大于0,则说明有信号产生,这时候遍历一遍辅助数组fd_arr,如果遇到改变的文件描述符,则说明该文件描述符对应的一端有信号传过来,这时候用这个描述符就可以进行 添加、接收信号、删除等操作。



        总之,在使用时需要注意:    

    1、select的第一个参数是所有信号的数目 + 1

    2、第2 ~ 5个 fd_set指针类型的参数,每次使用前都要保存,使用后要恢复

    3、select通常支持的文件描述符数目为1024


        

        select模式的server端实现代码如下:



/*************************************************************************
	> File Name: TCP_select.c
	> Author: HonestFox
	> Mail: zhweizhi@foxmail.com 
	> Created Time: Thu 28 Jul 2016 04:49:50 PM CST
 ************************************************************************/
#include<stdio.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<netinet/in.h>

#define _MAX_SIZE 100
int fd_arr[_MAX_SIZE];     
int max_fd = 0;

void usage(char *str)
{
	printf("Usage: %s [IP]:[port]\n]", str);
	exit(1);
}

static void init_fd_arr()
{
	int i = 0;
	for(i = 0; i < _MAX_SIZE; ++i)
	{
		fd_arr[i] = -1;
	}
}

static int add_fd_arr(int fd)
{
	int i = 0;
	for(; i < _MAX_SIZE; ++i)
	{
		if(fd_arr[i] == -1)
		{
			fd_arr[i] = fd;
			return 0;
		}
	}
	return -1;
}

static int remove_fd_arr(int fd)
{
	printf("want to remove : %d\n", fd);
	int i = 0;
	for(; i < _MAX_SIZE; ++i)
	{
		if(fd_arr[i] == fd)
		{
			printf("remove : %d\n", fd);
			fd_arr[i] = -1;
			break;
		}
	}
	return 0;
}

static int reload_fd_set(fd_set *fd_set)
{
	int i = 0;
	for(; i < _MAX_SIZE; ++i)
	{
		if(fd_arr[i] != -1)
		{
			FD_SET(fd_arr[i], fd_set);
			if(fd_arr[i] > max_fd)
			{
				max_fd = fd_arr[i];
			}
		}
	}
	return 0;
}

static void print_msg(int i, char buf[])
{
	printf("fd : %d, msg : %s\n", i, buf);
}

int select_server(char *_ip, char *_port)
{
	struct sockaddr_in ser;
	struct sockaddr_in cli;
	fd_set fds;
	
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd == 0)
	{
		perror("create socket error");
		exit(2);
	}

	int tmp_val = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &tmp_val, sizeof(int));

	memset(&ser, '\0', sizeof(ser));
	ser.sin_family = AF_INET;
	ser.sin_port = htons(atoi(_port));
	ser.sin_addr.s_addr = inet_addr(_ip);

	if(bind(fd, (struct sockaddr*)&ser, sizeof(ser)) != 0)
	{
		perror("bind error");
		exit(3);
	}

	init_fd_arr();
	add_fd_arr(fd);
	FD_ZERO(&fds);

	if(listen(fd, 5) != 0)
	{
		perror("listen error");
		exit(4);
	}

	int done = 0;
	while(!done)
	{
		max_fd = 0;
		reload_fd_set(&fds);
		printf("max_fd : %d\n", max_fd);
		struct timeval timeout = {1, 1};
		switch(select(max_fd + 1, &fds, NULL, NULL, &timeout))
		{
			reload_fd_set(&fds);
			case -1:
			{
				perror("select error");
				exit(5);
			}
			case 0:
			{
				printf("timeout  .. .. ..\n");
				break;
			}
			default:
			{
				int index = 0;
				for(index = 0; index < _MAX_SIZE; ++index)
				{
					if(index == 0 && fd_arr[index] != -1 && FD_ISSET(fd_arr[index], &fds))
					{
						socklen_t len = sizeof(cli);
						memset(&cli, '\0', sizeof(cli));
						int new_fd = accept(fd, (struct sockaddr*)&cli, &len);
						printf("new : %d\n", new_fd);  //
						if(new_fd != -1)
						{
							printf("get a new client!\n");
							if(add_fd_arr(new_fd) == -1)
							{
								perror("fd arr is full, close new fd:\n");
							}
						}
						continue;
					}
					if(fd_arr[index] != -1 && FD_ISSET(fd_arr[index], &fds ))
					{
						char buf[1042];
						memset(buf, '\0', sizeof(buf));
						printf("flag3\n");
						ssize_t _size = recv(fd_arr[index], buf, sizeof(buf)-1, 0); //read
						if(_size == 0 || _size == -1)
						{
							printf("client close\n");
							remove_fd_arr(fd_arr[index]);
							close(fd_arr[index]);
							FD_CLR(fd_arr[index], &fds);
						}
						else
						{
							print_msg(index, buf);
						}
						FD_ZERO(&fds);
					}
				}
				printf("out for()\n");
			}
			break;
		}
	}
}

int main(int argc, char *argv[])
{
	if(argc != 3)
	{
		usage(argv[0]);
		exit(1);
	}
	select_server(argv[1], argv[2]);
	return 0;
}



(2)、poll模式

    刚刚介绍的select是通过 fd_set 作为“管家的名单”的,而 fd_set则是一个“位图”,它用过位运算的方式存取各个被托管的端口的文件描述符。也就是说,把所有要监听接收、发送、错误信号的文件描述符分别放进三个不同的fd_set中。

    而poll 是用一个 pollfd 结构体实现的,这个pollfd结构体包括3个变量:长度、类型、返回值。然后再用一个数组将所有要托管的poll存起来就可以了。

    所以poll函数就不需要传那么多参数了,只需要传递 存放pollfd的数组、 nfds 、等待时间即可。

wKiom1eh-kfxjFWiAAEljQcMFSk619.png

    poll模式和select模式很像,基本上就是poll模式下,对文件描述符进行了封装,然后还是将这些封装后的结构体存在数组中,然后遍历数组中的元素,看其中哪些收到了信号。不过,由于这个数组是用户自己开辟、维护的,因此不像select用的位图那样有数量限制。 理论上poll模式是没有数量限制的。




三、I/O复用—— epoll



    epoll模式不同于 select/poll 的地方在于,我们需要先创建一个 epoll的文件描述符 然后将需要托管的端口的文件描述符通过 epoll_ctl 函数注册到epoll的文件描述符中,然后等待的时候调用 epoll_wait函数,内核会在epoll中那些注册过的端口中等待信号,直到收到信号的时候返回。  


wKiom1eh-yXBZQl_AAZnb_7HREc632.png

     要注意epoll_create得到的的是一个fd,所以使用完后要记得关闭



wKiom1eh-ybiZ6KCAAKTqkFbURE649.png

    这里应该特别注意以下 第四个参数,又是一个结构体,通过设置它,可以选择epoll的工作模式,其中

events可以设置两种触发模式:ET模式和LT模式

    其中,LT模式是缺省的模式。

    ET与LT的 区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件, 可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件 再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相 反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。 因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。

    举个例子,如果读到信息 处理完后,设置为ET模式不做其他的处理,那么该client端再发消息的时候,server端是收不到的。因为server对本次通信的处理仅仅是接收,并没有完整的处理套接字缓冲区。


wKioL1eh-yjCw_oRAANxtC0b9-s155.png

    其中,epoll_wait的第二个参数是一个结构体数组,epoll会把发生的事件放进这个数组中;而第三个参数就是这个数组的大小

    正是因为这样,在轮循等待信号的时候,就不需要向前两种模型那样把整个数组都遍历一遍,而是只需要遍历已经注册的信号就行了,因此epoll模型也是这三种模型中,被公认是最高效的。


实现代码如下:

/*************************************************************************
> File Name: epoll_tcp.c
> Author: HonestFox
> Mail: zhweizhi@foxmail.com
> Created Time: Fri 29 Jul 2016 03:14:27 PM CST
************************************************************************/
#include<stdio.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h> 
#include<string.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>

static void usage()
{
	printf("usage: ip: port\n");
}

static int set_nonblock(int sock)
{
	int fl = fcntl(sock, F_GETFL);
	return fcntl(sock, F_SETFL, fl | O_NONBLOCK);
}

static int startup(const char *_ip, const int _port)
{
	//Create Socket
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock < 0)
	{
		perror("socket");
		exit(2);
	}

	//Bind
	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(_port);
	local.sin_addr.s_addr = inet_addr(_ip);

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

	//Set Listen
	if (listen(sock, 5) < 0)
	{
		perror("listen");
		exit(5);
	}
	return sock;
}

int main(int argc, char *argv[])
{
	if (argc != 3)
	{
		usage();
	}
	int listen_sock = startup(argv[1], atoi(argv[2]));

	int epfd = epoll_create(256);
	if (epfd < 0)
	{
		perror("epoll_create");
		exit(5);
	}

	struct epoll_event _ev;
	_ev.events = EPOLLIN;
	_ev.data.fd = listen_sock;

	epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &_ev);

	struct epoll_event _ready_ev[128];
	int _ready_evs = 128;
	int _timeout = 1000;
	int done = 0;
	int nums = 0;
	while (!done)
	{
		nums = epoll_wait(epfd, _ready_ev, _ready_evs, _timeout);
		switch (nums)
		{
		case -1:
			perror("epoll_wait");
			exit(6);
		case 0:
			printf("time out\n");
			break;
		default:
		{
			int i = 0;
			for (; i < nums; ++i)
			{
				int _fd = _ready_ev[i].data.fd;
				if (_fd == listen_sock && (_ready_ev[i].events & EPOLLIN))
				{
					printf("get a new client\n");
					struct sockaddr_in peer;
					socklen_t len = sizeof(peer);
					int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
					//Get a New Link
					if (new_sock > 0)
					{
						printf("new sock > 0\n");
						printf("client info %s : %d\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
						_ev.events = EPOLLIN | EPOLLET; //ET
						_ev.data.fd = new_sock;
						set_nonblock(new_sock);
						epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &_ev);
					}
				}
				else
				{
					if (_ready_ev[i].events & EPOLLIN)
					{
						char buf[1024];
						memset(buf, '\0', sizeof(buf));
						ssize_t _s = recv(_fd, buf, sizeof(buf) - 1, 0);
						if (_s > 0)
						{
							printf("client : %s\n", buf);
							//_ev.events = EPOLLOUT | EPOLLET; //ET  如果缺省是LT
							//_ev.data.fd = _fd;
							epoll_ctl(epfd, EPOLL_CTL_MOD, _fd, &_ev);	//Mod
						}
						else if (_s == 0)	//Client Close
						{
							printf("client close...\n");
							close(_fd);
							epoll_ctl(epfd, EPOLL_CTL_DEL, _fd, NULL);
						}
						else
						{
							perror("recv");
							exit(5);
						}
					}
					else if (_ready_ev[i].events & EPOLLOUT)
					{
			                    //写信号,做的事情
					}
				}
			}
		}
		break;
		}
	}
	return 0;
}


四、总结

    这次介绍了3种I/O多路转接的模型,采用I/O复用模型的服务器,性能上要比多线程服务器高得多,因此也叫 高性能服务器。

    三种模型分别是 select模型、 poll模型、epoll模型。

    

    select模型和poll模型都只提供了一个等待函数,每次调用相应等待函数的时候,都需要把client端的文件描述符集合遍历一遍,如果集合空间很大但实际存放的文件描述符并不多,那就会浪费很多额外的时间,此外,这两种模型需要多次发生用户态和内核态之间的拷贝,开销比较大。


    epoll相比select/poll的优点:

    1、相比select,没有    fd数目的限制
    2、每次注册的时候,就将新的文件描述符存入内核态,因此开销较小。

    3、epoll的具体实现采用了mmap,加速了内核态和用户态之间传递的效率。