Linux 网络编程 全解(六)--------多路IO转接服务器

版权声明:本BLOG上原创文章未经本人许可,不得转载,否则属于侵权行为 https://blog.csdn.net/weixin_40204595/article/details/83212900

写在前面:本篇介绍多路IO转接服务器实现的三种方式,select、poll、epoll,下面开始一一介绍,本篇文字叙述会比较少,代码的量会大点。

Linux 网络编程 全解(一)--------网络基础协议

Linux 网络编程 全解(二)--------套接字socket

Linux 网络编程 全解(三)--------TCP三次握手、数据传输、四次挥手、滑动窗口

Linux 网络编程 全解(四)--------多进程并发服务器和多线程并发服务器

Linux 网络编程 全解(五)--------TCP状态切换

正文:

一、select实现多路IO转接服务器

1、select函数简介:

     int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
    作用: 借助内核,select来监听客户端连接、数据通信事件。
    函数参数:
    nfds:监听的描述符里面的最大描述符+1.
    readfds,传入传出参数,监听的读数据的文件描述符的集合。传入:要监听的文件描述符的集合。传出:实际有事件发生的文                  件描述符集合。
    writefds:写文件描述符集合。传入传出参数。
    exceptfds:异常文件描述符集合。传入传出参数。
    timeout:三种情况:
          (1)、NULL,永远等下去
          (2)、设置 timeval,等待固定时间
          (3)、设置 timeval 里时间均为 0,检查描述字后立即返回,轮询
       返回值:
             >0:所有集合中,满足对应事件的总数。
             =0:没有满足监听事件的描述符。
             -1:异常

       相关函数:
       void FD_CLR(int fd, fd_set *set); //把文件描述符集合里 fd 清 0,将fd文件描述符从监听集合中清除
       int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里 fd 是否置 1,即判断fd这个文件描述符是否在set集合中
                                                           //返回值:1,fd在集合中;0,fd不在集合中
       void FD_SET(int fd, fd_set *set); //把文件描述符集合里 fd 位置 1,将监听的文件描述符fd,添加到监听集合中
       void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清 0 

2、下面就是代码实践部分,里面注释已经非常详细了。

#include "stdio.h"
#include <sys/types.h>          
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> 
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>


#define SER_PORT (6666)


/*  如下:fd 是递增的,如下:
 *
 *   fd数值          fd含义
 *
 *   ...              ...
 *   5                cfd2
 *   4                cfd1
 *   3				  lfd
 *   2 				STANDERR
 *   1 				STANDOUT
 *   0              STANDIN
*/


int main(void)
{
	int lfd = -1, max_fd = 0,sel_nums = -1,cfd = -1, i = 0,rd_size;
	struct sockaddr_in ser_ip, cli_ip;
	fd_set rd_set,all_set; //rd_set是监听的文件描述符集合,all_set是所有的文件描述符集合
	char cli_ip_addr[INET_ADDRSTRLEN];
	char rd_buf[1024] = {0};
	
	socklen_t cli_ip_len = sizeof(struct sockaddr_in);
	
	lfd = socket(AF_INET, SOCK_STREAM, 0);
	
	memset(&ser_ip,0,sizeof(struct sockaddr_in));
	ser_ip.sin_family = AF_INET;
	ser_ip.sin_port = htons(SER_PORT);
	ser_ip.sin_addr.s_addr = htonl(INADDR_ANY);

	bind(lfd, (const struct sockaddr *)&ser_ip,sizeof(struct sockaddr_in));
	
	listen(lfd, 128);

	max_fd = lfd;
	
	FD_ZERO(&all_set); //清空文件描述符集合
	rd_set = all_set; //将all_set赋值给rd_set
	FD_SET(lfd,&all_set); //将连接的文件描述符lfd添加到监听集合中

	//设置端口复用
	int opt = 1;// 1:设置端口复用,0:设置端口不复用
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
	
	while(1)
	{
		rd_set = all_set; //将all_set赋值给rd_set
		
		sel_nums = select(max_fd + 1, &rd_set, NULL,NULL, NULL);
		
		if(sel_nums < 0) //select 错误
		{
			printf("select error\n");
			
			return;
		}
		if(sel_nums > 0)
		{
			if(FD_ISSET(lfd, &rd_set)) //如果FD_ISSET返回值>0,说明满足lfd
			{						   //的条件在rd_set中,即有新的客户端发生了连接  
				
				//server调用accept与client连接,这里不会阻塞了,因为select已经监听到了连接事件
				cfd = accept(lfd, (struct sockaddr *)&cli_ip, &cli_ip_len);				
				if(cfd > 0)  
				{
					memset(cli_ip_addr,0,sizeof(cli_ip_addr));
					inet_ntop(AF_INET, &cli_ip.sin_addr.s_addr, cli_ip_addr, sizeof(cli_ip_addr));
					printf("client connected:client ip:%s\n",cli_ip_addr);
					
					//获取最大的fd,保存到max_fd中
					max_fd = max_fd > cfd ? max_fd : cfd;
					
					printf("max_fd = %d\n",max_fd);
					
					//将cfd添加到监听集合中
					FD_SET(cfd,&all_set); 
					
				}
				
				//注意:这里很重要,即select的返回值是1,并且lfd在监听的集合当中,也就是只返回了lfd
				//这意味着只有连接事件发生,并没有通信读写事件发生
				if(1 == sel_nums)
				{
					continue;
				}
			}   
			//(1):因为文件描述符是递增的,所以第一个文件描述符是lfd,最大的文件描述符是max_fd
			//(2):既然程序到了这里 ,说明发生了客户端的通信读写事件
			//(3):TODO:代码优化,这里轮询其实效率是很低的,优化方法:单独定义一个
			//          数组来存储监听到的fd,每次轮询这个数组的有效区即可
			for(i = lfd +1;i < max_fd + 1;i++) 
			{
				memset(rd_buf,0,sizeof(rd_buf));
				if(FD_ISSET(i, &rd_set))     //找到满足读事件的那个fd
				{
					rd_size = read(i, rd_buf, sizeof(rd_buf));
					if(rd_size == 0)
					{
						close(i);
						FD_CLR(i, &all_set);//这里很重要,从监听的文件描述符中移除,不移除会出问题
						continue;
					}
					else
					{
						printf("read client buf:%s\n",rd_buf);
					}
					
					if(sel_nums == 1)
						break;
															
				}
			}
			
	
		}
		
			
	}
	
	
	close(lfd);
	
	return 0;
}

3、测试结果:

  

4、select的优缺点

        缺点:监听上限受文件描述符限制,最大1024.

        有点:跨平台。windows、linux、micOS、unix、mips都支持select。

二、poll实现多路IO转接服务器

1、poll函数简介

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);

   作用: poll函数的作用也是实现多路IO转接服务器。

   函数参数:

    struct pollfd {
               int   fd;         /* file descriptor */ 文件描述符
               short events;     /* requested events */ 传入的事件,待监听的文件描述符对应的事件,读POLLIN、写                                                                POLLOUT、异常POLLERR等
               short revents;    /* returned events */传出的事件:读、写、异常等,传入时给0.满足对应事件会返回非0。
           };

    fds:监听的文件描述符的数组,传入传出参数。
    nfds:监听数组的实际有效监听个数。
    timeout:阻塞时长,时间ms。
         =0:不阻塞;
         >0: 阻塞时长
         <0: 阻塞
    返回:返回满足对应监听事件的文件描述符总个数。

2、代码实践,注释也很详细

#include "stdio.h"
#include <sys/types.h>         
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <poll.h>
#include <string.h>

#define SER_PORT (6666)

//定义一个server的 poll函数监听管理的结构体
typedef struct
{
	struct pollfd  monitor_set[1024]; //最大可以poll可以监听1024个客户端
	int maxIndex;             //记录monitor_set 数组使用的最大下标值
}tMonitorManager;

tMonitorManager g_ser_monitor_mgr ;

//初始化server 监听的结构体
void serMonitorManagerInit(void)
{
	int i = 0;
	
	for(i = 0;i < 1024; i++)
	{
		g_ser_monitor_mgr.monitor_set[i].fd = -1;   //将fd都初始化为-1
		g_ser_monitor_mgr.monitor_set[i].events = 0;
		g_ser_monitor_mgr.monitor_set[i].revents = 0;
	}
	
	g_ser_monitor_mgr.maxIndex = 0;
	
	return ;
	
}


int main(void)
{
	int lfd = -1,cfd = -1,i = 0,rd_size;
	int listen_nums = -1;
	struct sockaddr_in ser_addr,cli_addr;
	socklen_t cli_addr_len = sizeof(struct sockaddr_in);
	char cli_ip[16];
	char rd_buf[1024];
	
	lfd = socket(AF_INET, SOCK_STREAM, 0);
	
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_port = htons(SER_PORT);
	ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	
	bind(lfd, (const struct sockaddr *)&ser_addr,sizeof(ser_addr));
	
	listen(lfd,20);
	
	//设置端口复用
	int opt = 1;// 1:设置端口复用,0:设置端口不复用
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
	
	serMonitorManagerInit();

	//将监听数组的0号下标位置设置为监听连接的fd
	g_ser_monitor_mgr.monitor_set[0].fd = lfd;
	//将监听数组的0号下标位置的监听事件设置为读事件
	g_ser_monitor_mgr.monitor_set[0].events = POLLIN;
	
	
	while(1)
	{
		//调用poll阻塞监听事件
		listen_nums = poll(g_ser_monitor_mgr.monitor_set, g_ser_monitor_mgr.maxIndex + 1, -1);
		
		if(listen_nums < 0)
		{
			printf("poll error\n");
			
			return -1;
		}
		if(listen_nums > 0)
		{
			//查看监听数组0号下标的revents是否发生了连接事件
			if(g_ser_monitor_mgr.monitor_set[0].revents & POLLIN )
			{
				//发生了连接事件,server调用accept函数跟client连接
				memset(&cli_addr,0,sizeof(cli_addr));
				cfd = accept(lfd, (struct sockaddr *)&cli_addr, &cli_addr_len);
				
				memset(cli_ip,0,sizeof(cli_ip));
				inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, cli_ip, 16);
				printf("client connected,client ip : %s\n",cli_ip);
				
				//将client的fd也添加到监听数组中去,监听读事件
				for(i = 1; i < 1024; i ++)
				{
					if(g_ser_monitor_mgr.monitor_set[i].fd < 0)
					{
						g_ser_monitor_mgr.monitor_set[i].fd = cfd;
						g_ser_monitor_mgr.monitor_set[i].events = POLLIN;
						
						break;
					}
				}
				
				if(i >= 1024)
				{
					printf("monitor arrey full\n");
					
					return -1;
				}
				//下标大的哪一个赋值给g_ser_monitor_mgr.maxIndex
				g_ser_monitor_mgr.maxIndex = i > g_ser_monitor_mgr.maxIndex ? i : g_ser_monitor_mgr.maxIndex ;
				printf("g_ser_monitor_mgr.maxIndex =%d\n",g_ser_monitor_mgr.maxIndex);
			
				//poll的返回值是1,并且lfd在监听的集合当中,也就是只返回了lfd
				//这意味着只有连接事件发生,并没有通信读写事件发生
				if(1 == listen_nums )
				{
					continue;
				}

			}
			//程序到了这里 ,说明发生了客户端的通信读写事件
			//就要遍历监听的有效数组区看看哪一个发生了读事件
			for(i = 1; i < g_ser_monitor_mgr.maxIndex + 1; i ++)
			{
				if(g_ser_monitor_mgr.monitor_set[i].fd < 0)
					continue;
				
				if(g_ser_monitor_mgr.monitor_set[i].revents & POLLIN)
				{
					memset(rd_buf,0,sizeof(rd_buf));
					
					rd_size = read(g_ser_monitor_mgr.monitor_set[i].fd, rd_buf, 1024);
					
					if(0 == rd_size)//对端关闭
					{
						close(g_ser_monitor_mgr.monitor_set[i].fd);
						//从监听集合中移除
						g_ser_monitor_mgr.monitor_set[i].fd = -1;
						g_ser_monitor_mgr.monitor_set[i].events = 0;
						g_ser_monitor_mgr.monitor_set[i].revents = 0;
						
						//g_ser_monitor_mgr.maxIndex --;
						//g_ser_monitor_mgr.maxIndex  = g_ser_monitor_mgr.maxIndex <= 0 ? 0: g_ser_monitor_mgr.maxIndex ; 
						
						continue;
					}
					if(rd_size > 0)
					{	
						printf("receive from client:%s\n",rd_buf);
					}
					
					if(listen_nums == 1)
						break;
					
				}
			}

			
		}

		
		
		
	}	
	

	close(lfd);	
	return 0;
}

3、测试结果

4、poll函数优缺点

   优点: 自带数组结构,可以将监听事件集合和返回事件集合分离。可以拓展监听上限,可以超出1024个,select 最多1024。

   缺点:不能跨平台,只能在linux中使用。无法直接定位满足监听事件的文件描述符,需要轮询数组。

三、epoll实现多路IO转接服务器

1、epoll相关函数简介

  (1)   int epoll_create(int size);
   作用:创建一个epoll句柄,实际上创建了一个监听红黑二叉树。
   参数:size:创建的红黑树的监听节点数量,仅供内核参考,即告诉内核要监听的文件描述符的个数。
   返回值:成功返回文件描述符,即指向新创建的红黑二叉树的fd。
                失败: -1

   (2) int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

   函数作用:操作监听红黑树, 控制某个 epoll 监控的文件描述符上的事件:添加、修改、删除。
   参数:
       epfd: epoll_create()返回值;
       op:  对该监听红黑树所做的操作,
        EPOLL_CTL_ADD,添加fd到监听红黑树;
        EPOLL_CTL_MOD,修改fd在监听红黑树上的事件;修改已经监听的fd的监听事件。
        EPOLL_CTL_DEL,将一个fd从监听红黑树上删除,即取消监听。
       fd:监听的fd;
       event:
       typedef union epoll_data {
               void        *ptr;   //泛型指针
               int          fd;    //对应监听事件的fd
               uint32_t     u32;   //不用
               uint64_t     u64;   //不用
           } epoll_data_t;

           struct epoll_event {
               uint32_t     events;  /* Epoll events */ // 读事件EPOLLIN;写事件EPOLLOUT;异常事件EPOLLERR;
               epoll_data_t data;    /* User data variable */ //
           };
   返回:成功 0;失败 -1;
 (3)epoll_wait函数

   int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
   作用:阻塞监听,等待所监控文件描述符上有事件的产生,类似于 select()调用;

   参数:
    epfd:epoll_create()返回值;
    events:用来存内核得到事件的集合,可以看做是个数组,这是一个传出参数,传出满足监听条件的那些fd结构体。
    maxevents:表示数组events元素的总个数。
    timeout:阻塞时长,时间ms。
         =0:不阻塞;
         >0: 阻塞时长
         <0: 阻塞
  返回值:>0:满足监听的总个数,可以用作循环上限。
      =0:没有fd满足监听事件
      =-1:异常。

2、代码实践,同样,注释部分已很详细

#include "stdio.h"
#include <sys/types.h>         
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> 
#include <arpa/inet.h>
#include <string.h>
#include <sys/epoll.h>



#define SER_PORT (6666)

//server的监听连接的结构体类型
typedef struct 
{
	struct epoll_event levent;
}tListen;

//定义一个epoll 管理的结构体
typedef struct
{
	struct epoll_event epoll_set[1024]; //epoll 监听的文件描述符的集合
	int maxIndex;  //这个变量此工程中未用到
	tListen lsn;
	
}tEpollMgr;


tEpollMgr g_tEpollMgr;

void ePollMgrInit(void)
{
	int i = 0;
	
	for(i = 0;i < 1024;i ++)
	{
		g_tEpollMgr.epoll_set[i].data.fd = -1; //将监听的fd都设为-1
	}
	
	g_tEpollMgr.maxIndex = 0;
	
}


int main(void)
{
	int lfd = -1,epfd = -1,ret = -1,ep_wait_nums = -1,i = 0,cfd = -1,rd_size = -1;
	struct sockaddr_in ser_ip, cli_ip;
	socklen_t cli_ip_len = sizeof(cli_ip);
	char cli_ip_addr[16] = {0};
	struct epoll_event temp;
	char rd_buf[1024] = {0};
	
	lfd = socket(AF_INET, SOCK_STREAM, 0);
	
	memset(&ser_ip,0,sizeof(ser_ip));
	ser_ip.sin_family = AF_INET;
	ser_ip.sin_port = htons(SER_PORT);
	ser_ip.sin_addr.s_addr = htonl(INADDR_ANY);
	
	bind(lfd, (const struct sockaddr *)&ser_ip,sizeof(ser_ip));

	listen(lfd, 20);
	
	//设置端口复用
	int opt = 1;// 1:设置端口复用,0:设置端口不复用
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
	
	//初始化epoll的管理结构体
	ePollMgrInit();
	
	//创建epoll红黑树,设置红黑树的监听节点最大为500
	epfd = epoll_create(500);
	if(epfd < 0)
	{
		printf("epoll_create error\n");
		
		return -1;
	}
	
	//给监听连接的fd赋值
	g_tEpollMgr.lsn.levent.data.fd = lfd;
	
	//设置监听事件为读事件
	g_tEpollMgr.lsn.levent.events = EPOLLIN;
	
	//添加监听事件到监听树中
	ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &g_tEpollMgr.lsn.levent);
	if(ret < 0)
	{
		printf("epoll_ctl error\n");
		return -1;
	}
	
	while(1)
	{
		//阻塞监听epoll事件
		ep_wait_nums = epoll_wait(epfd, g_tEpollMgr.epoll_set,1024, -1);
		
		if(ep_wait_nums < 0)
		{
			printf("epoll_wait err\n");
			
			return ;
		}
		if(ep_wait_nums > 0)//有监听的事件发生,并且客户端的个数是ep_wait_nums
		{
			for(i = 0;i < ep_wait_nums;i ++ )
			{
				//如果数组相应的位置没有读事件,继续下一个循环
				if(!(g_tEpollMgr.epoll_set[i].events & EPOLLIN))
				{
					continue;
				}
				//如果客户端发生了连接事件
				if(g_tEpollMgr.epoll_set[i].data.fd == lfd)
				{
					//server 调用 accept函数与client连接
					memset(&cli_ip,0,sizeof(cli_ip));
					cfd = accept(lfd, (struct sockaddr *)&cli_ip, &cli_ip_len);
					
					if(cfd > 0)
					{
						memset(cli_ip_addr,0,16);
						inet_ntop(AF_INET, &cli_ip.sin_addr.s_addr, cli_ip_addr, 16);
						
						printf("client connected,client ip addr:%s\n",cli_ip_addr);
						
						temp.events = EPOLLIN;
						temp.data.fd = cfd;
						//并将连接上来的client添加到监听树中
						epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
						
					}

				}
				else //发生了读事件
				{
					memset(rd_buf,0,1024);
					
					rd_size = read(g_tEpollMgr.epoll_set[i].data.fd, rd_buf, 1024);
					if(rd_size == 0)//对端已关闭
					{
						//将其从监听树中删除
						//删除的话 epoll_ctl的第三个参数传NULL就可以了
						epoll_ctl(epfd, EPOLL_CTL_DEL, g_tEpollMgr.epoll_set[i].data.fd, NULL);
						close(g_tEpollMgr.epoll_set[i].data.fd);
					}
					if(rd_size > 0)
					{
						printf("receive from client:%s\n",rd_buf);
					}

				}
				
			}
			
		}
	}


	close(lfd);
	
	
	return 0;
}

3、测试结果

4、 epoll优缺点总结:

   优点:高效
   缺点: 不能跨平台,只支持linux。

猜你喜欢

转载自blog.csdn.net/weixin_40204595/article/details/83212900