(上一篇地址)前面使用socket完成一个服务器对应多个客户端的小实验的时候,针对TCP连接,我们必须得创建新的进程来与新的客户端通信。那么,就意味着,1000个客户端就有有1000个server进程,这显然是不实际的。如果,我们可以提前把要监听的文件描述符放到一个集合里,一旦其中一个发生事件(不管是连上,还是通信),就去处理。这样,会方便很多。所以,今天学习一下IO复用。

1 五个I/O模型

  • 阻塞I/O
  • 非阻塞I/O
  • I/O复用(select和poll)
  • 信号驱动I/O
  • 异步I/O

阻塞IO

最流行的I/O模型是阻塞I/O模型,缺省时,所有的套接口都是阻塞的。

非阻塞IO

IO复用

信号驱动IO

异步IO

2 I/O复用

如果一个或多个I/O条件满足(例如:输入已准备好被读,或者描述字可以承接更多输出的时候)我们就能够被通知到,这样的能力被称为I/O复用,是由函数selectpoll支持的。

I/O复用网络应用场合

  • 当客户处理多个描述字
  • 一个客户同时处理多个套接口
  • 如果一个tcp服务器既要处理监听套接口,又要处理连接套接口
  • 如果一个服务器既要处理TCP,又要处理UDP

select

	     /* According to POSIX.1-2001 */
       #include <sys/select.h>

       /* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

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

       void FD_CLR(int fd, fd_set *set);//从集合中删除一个描述字
       int  FD_ISSET(int fd, fd_set *set);//描述字是否在该集合中
       void FD_SET(int fd, fd_set *set);//添加一个描述字到集合中
       void FD_ZERO(fd_set *set);//清空描述字集合
  • 作用:函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程 提供了即时响应多个套接的读写事件
  • 参数:
  • nfds:集合中最大的文件描述符 + 1 (指定被测试的描述字个数,它的值是要被测试的最大描述字加1,描述字0、1、2…….一直到nfds均被测试)
  • readfds:要检查读事件的容器
  • writefds:要检查写事件的容器
  • timeout:超时时间
  • 返回值:返回触发套接字的个数 中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件所需的描述字 如果我们对某个条件不感兴趣,这三个参数中相应的参数就可以设为空指针

timeout参数

时间的结构体如下:

			struct timeval(
				long tv_sec;  //秒
				long tv_usec;//微秒
			);

timeout参数有三种可能

  • 永远等待下去:仅在有一个描述字准备好I/O时才返回,为此,我们将timeout设置为空指针
  • 等待固定时间:在有一个描述字准备好I/O是返回,但不超过由timeout参数所指timeval结构中指定的秒数和微秒数
  • 根本不等待:检查描述字后立即返回,这称为轮询。定时器的值必须为0

fd_set参数

select使用描述字集,它一般是一个整数数组,每个数中的每一位对应一个描述字。

使用流程

使用select完成之前socket的测试,流程如下: 客户端代码不变。

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

	//TCP
	int main()
	{
		int fd;
		int ret;
		int addrLen;
		char acbuf[20] = "";
		struct sockaddr_in serAddr = {0};
		struct sockaddr_in myAddr = {0};

		//1.socket();
		fd = socket(PF_INET,SOCK_STREAM,0);
		if(fd == -1)
		{
			perror("socket");
			return -1;
		}

		//2.连接connect() 服务器的地址
		serAddr.sin_family = AF_INET;
		serAddr.sin_port = htons(1234);
		serAddr.sin_addr.s_addr = inet_addr("192.168.159.5");
		ret = connect(fd,(struct sockaddr *)&serAddr,sizeof(struct sockaddr_in));
		if(ret == -1)
		{
			perror("connect");
			return -1;
		}

		//获取自己的地址
		addrLen = sizeof(struct sockaddr_in);
		ret = getsockname(fd,(struct sockaddr *)&myAddr,&addrLen);
		if(ret == -1)
		{
			perror("getsockname");
			return -1;
		}
		printf("client---ip: %s , port: %d\n",\
					inet_ntoa(myAddr.sin_addr),ntohs(myAddr.sin_port));
		//3.通信
		while(1)
		{
			printf("send: ");
			fflush(stdout);
			scanf("%s",acbuf);
			if(strcmp(acbuf,"exit") == 0)
			{
				break;
			}
			write(fd,acbuf,strlen(acbuf));
		}

		//4.close()
		close(fd);
		return 0;
	}

服务器端: select.c

		#include < sys/types.h>     
		#include < sys/socket.h>
		#include < netinet/in.h>	//sockaddr_in
		#include < stdio.h>
		#include < string.h>
		#include < signal.h>
		#include < sys/select.h>
		#include < unistd.h>
		#include < sys/time.h>
		//TCP
		int main()
		{
			int fd;
			int clientfd;
			int ret;
			pid_t pid;

			int i;
			int maxfd;			//当前最大套接字
			int nEvent;
			fd_set set = {0};	//监听集合
			fd_set oldset = {0};	//存放所有要监听的文件描述符
			struct timeval time = {0};

			int reuse = 0;
			char acbuf[20] = "";
			char client_addr[100] = "";
			struct sockaddr_in addr = {0};	//自己的地址
			struct sockaddr_in clientAddr = {0};	//连上的客户端的地址
			int addrLen = sizeof(struct sockaddr_in);
			
			signal(SIGCHLD,SIG_IGN);

			//1.socket()
			fd = socket(PF_INET,SOCK_STREAM,0);
			if(fd == -1)
			{
				perror("socket");
				return -1;
			}

			//会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
			//由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
			if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
				{
				perror("setsockopet error\n");
				return -1;
				}

			//2.bind()
			addr.sin_family = AF_INET;
			addr.sin_port = htons(1234);
			addr.sin_addr.s_addr = inet_addr("192.168.159.5");
			ret = bind(fd,(struct sockaddr *)&addr,addrLen);
			if(ret == -1)
			{
				perror("bind");
				return -1;
			}

			//3.listen()
			ret = listen(fd,10);
			if(ret == -1)
			{
				perror("listen");
				return -1;
			}

			//创建监听集合
			FD_ZERO(&oldset);
			FD_SET(fd,&oldset);
			//maxfdp1:当前等待的最大套接字。比如:当前fd的值=3,则最大的套接字就是3
			//所以每当有客户端连接进来,就比较一下文件描述符
			maxfd = fd;
			//select
			//select之前,set放的是所有要监听的文件描述符;{3,4,5}
			//select之后,set只剩下有发生事件的文件描述符。{3}

			while(1)
			{
				set = oldset;
				printf("before accept.\n");
				time.tv_sec = 5;
				nEvent = select(maxfd + 1,&set,NULL,NULL,&time);	//返回文件描述符的个数(即事件的个数)
				printf("after accept.%d\n",nEvent);
				if(nEvent == -1)
				{
					perror("select");
					return -1;
				}
				else if(nEvent == 0)	//超时
				{
					printf("time out");
					return 1;
				}
				else
				{			
					//有事件发生
					//判断是否是客户端产生的事件
					for(i = 0 ; i <= maxfd ; i++)
					{
						if(FD_ISSET(i,&set))
						{
							if(i == fd)
							{
								clientfd = accept(fd,(struct sockaddr *)&clientAddr,&addrLen);
								FD_SET(clientfd,&oldset);
								printf("client ip:%s ,port:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
								if(clientfd > maxfd)
								{
									maxfd = clientfd;
								}
							}
							else
							{
								memset(acbuf,0,20);
								if(read(i,acbuf,20) == 0) //客户端退出
								{
									close(i);
									//还要从集合里删除
									FD_CLR(i,&oldset);
								}
								else
									printf("receive: %s\n",acbuf);
							}
						}
					}
				}
			}
			return 0;
		}

epoll

epoll用到的函数有以下几个:

				#include <sys/epoll.h>
       int epoll_create(int size);//创建epoll
			 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//操作函数
			 int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);	 

事件集合的结构体: (这里 ,还要注意,epoll的超时参数是int,单位是us)

使用流程

		#include <sys/types.h>     
		#include <sys/socket.h>
		#include <netinet/in.h>	//sockaddr_in
		#include <stdio.h>
		#include <string.h>
		#include <signal.h>
		#include <sys/epoll.h>

		//epoll
		//epoll_wait() epoll_creat() epoll_ctl()

		//TCP
		int main()
		{
			int fd;
			int clientfd;
			int ret;
			pid_t pid;

			int i;
			int epfd;
			int nEvent;
			struct epoll_event event = {0};
			struct epoll_event rtl_events[20] = {0};	//事件结果集

			int reuse = 0;
			char acbuf[20] = "";
			char client_addr[100] = "";
			struct sockaddr_in addr = {0};	//自己的地址
			struct sockaddr_in clientAddr = {0};	//连上的客户端的地址
			int addrLen = sizeof(struct sockaddr_in);

			signal(SIGCHLD,SIG_IGN);

			//1.socket()
			fd = socket(PF_INET,SOCK_STREAM,0);
			if(fd == -1)
			{
				perror("socket");
				return -1;
			}

			//会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
			//由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
			if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
				{
				perror("setsockopet error\n");
				return -1;
				}

			//2.bind()
			addr.sin_family = AF_INET;
			addr.sin_port = htons(1234);
			addr.sin_addr.s_addr = inet_addr("192.168.159.5");
			ret = bind(fd,(struct sockaddr *)&addr,addrLen);
			if(ret == -1)
			{
				perror("bind");
				return -1;
			}

			//3.listen()
			ret = listen(fd,10);
			if(ret == -1)
			{
				perror("listen");
				return -1;
			}

			epfd = epoll_create(1000);	//同时监听的文件描述符
			event.data.fd = fd;
			event.events = EPOLLIN;  //读
			epoll_ctl(epfd,EPOLL_CTL_ADD,fd, &event);
			while(1)
			{
		//		nEvent = epoll_wait(epfd,rtl_events,20,-1);  //-1:阻塞    0:非阻塞
				nEvent = epoll_wait(epfd,rtl_events,20,5000);
				if(nEvent == -1)
				{
					perror("epoll_wait");
					return -1;
				}
				else if(nEvent == 0)
				{
					printf("time out.");
				}
				else
				{
					//有事件发生,立即处理
					for(i = 0; i < nEvent;i++)
					{
						//如果是 服务器fd
						if( rtl_events[i].data.fd == fd )
						{
							clientfd = accept(fd,(struct sockaddr *)&clientAddr,&addrLen);
							//添加
							event.data.fd = clientfd;
							event.events = EPOLLIN;  //读
							epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&event);
							printf("client ip:%s ,port:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
						}
						else
						{
							//否则 客户端fd 
							memset(acbuf,0,20);
							ret = read(rtl_events[i].data.fd,acbuf,20);
							printf("%d\n",ret);
							if( ret == 0) //客户端退出
							{
								close(rtl_events[i].data.fd);
								//从集合里删除
								epoll_ctl(epfd,EPOLL_CTL_DEL,rtl_events[i].data.fd,NULL);
							}
							else
								printf("receive: %s\n",acbuf);
						}

					}
				}
			}

			return 0;
		}

运行结果如前,正常收发。