Linux网络编程-4. I/O多路转接

4.1 I/O多路转接

I/O多路转接技术:

  • 先构造一张有关文件描述符的列表,将要监听的文件描述符添加到列表中
  • 然后调用一个函数,监听该列表中的文件描述符,知道这些描述符中的一个进行I/O操作时,该函数才返回
    – 该函数为阻塞函数
    – 函数对文件描述符的检测操作是由内核完成的
  • 在返回时,他告诉进程有多少(哪些)描述符要进行I/O操作

使用多路转接技术:selectpollepoll
第一种:selectpoll
只能确定数量
第二种:epoll
不仅能确定数量、而且能确定是哪一个

4.2 同步I/O多路复用select

优点:

  • 跨平台

缺点:

  • 每次调用selcet,都需要吧fd集合从用户态拷贝到内核态
  • 同时每次调用select都需要在内核遍历传递进来的所有fd
  • select支持的文件描述符数量太小了,默认是1024
#include <sys/select.h>

/*
 * 函数功能:
 *		同步I/O多路复用
 * 参数:
 *		nfds:要检测的文件描述符中最大的fd+1(最大值1024)
 *		readfds:读集合
 *			传入传出参数
 *		writefds:写集合,通常为NULL
 *		exceptfds:异常集合,通常为NULL
 *		timeout:
 *			NULL:永久阻塞,当检测到fd发生变化时返回
 *			阻塞等待:tv_sec+tv_usec
 *	返回值:
 *		成功:0
 *		失败:-1
 */
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

struct timeval {
	time_t      tv_sec;         /* seconds */
	suseconds_t tv_usec;        /* microseconds */
}


//把文件描述符集合里fd清0
void FD_CLR(int fd, fd_set *set);

//测试文件描述符集合里fd是否置1
int  FD_ISSET(int fd, fd_set *set);

//把文件描述符集合里fd位置1
void FD_SET(int fd, fd_set *set);

//把文件描述符集合里所有位清0
void FD_ZERO(fd_set *set);

4.2.1 select伪代码实现

int main()
{
	int lfd = socket();
	bind();
	listen();
	
	// 创建一个文件描述符的表
	fd_st reads, temp;
	// 初始化
	FD_ZERO(&reads);
	// 将lfd加入用于监听连接请求
	FD_SET(lfd, &reads);
	int max_fd = lfd;

	while(1)
	{
		// 委托内核检测
		temp = reads;
		int ret = select(max_fd+1, &temp, NULL, NULL, NULL);
		// 判断是否为监听文件描述符
		if(FD_ISSET(lfd, &temp))
		{
			// 有新连接
			int cfd = accept();
			// 将cfd加入reads
			FD_SET(cfd, &reads);
			// 更性max_fd
			max_fd = max_fd < cfd ? cfd : max_fd;
		}
		// 客户端发送数据
		for(int i = lfd + 1; i <= max_fd; ++i)
		{
			if(FD_ISSET(i, &temp))
			{
				int len = read();
				if(len == 0)
				{
					// 客户端断开连接,从集合中删除
					FD_CLR(i, &reads);
				}
				write();
			}
		}
	}
}

4.2.2 select代码实现

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>

#define MAX_LISTEN 128

int main(int argc, const char* argv[])
{
	if(argc < 2)
	{
		printf("eg: ./server port\n");
		exit(-1);
	}
	struct sockaddr_in serv_addr;
	socklen_t serv_len = sizeof(serv_addr);
	int port = atoi(argv[1]);
	
	// 创建套接字
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	// 初始化服务器sockaddr_in
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	// 绑定端口和IP
	bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
	// 设置监听上限
	listen(lfd, MAX_LISTEN);
	struct sockaddr_in client_addr;
	socklen_t client_len = sizeof(client_addr);
	
	printf("Start accept...\n");
	
	// 最大的文件描述符 
	int max_fd = lfd;
	//文件描述符的集合
	fd_set reads, temp;
	// 初始化
	FD_ZERO(&reads);
	FD_SET(lfd, &reads);
	while(1)
	{
		temp = reads;
		// 使用select委托内核做I/O检测
		int ret = select(max_fd + 1, &temp, NULL, NULL, NULL);
		if(ret == -1)
		{
			perror("select error:");
			exit(-2);
		}
		if(FD_ISSET(lfd, &temp))
		{
			// 有新连接
			int cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_len);
			if(cfd == -1)
			{
				perror("accept error\n");
				exit(-3);
			}
			char ip[64] = { 0 };
			printf("new client IP:%s, port:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(client_addr.sin_port));
			// 将新连接加入raeds
			FD_SET(cfd, &reads);
			// 更新
			max_fd = max_fd < cfd ? cfd : max_fd;
		}
		// 客户端发送新数据
		int i;
		for(i = lfd + 1; i <= max_fd; ++i)
		{
			if(FD_ISSET(i, &temp))
			{
				char buf[1024] = { 0 };
				int len = recv(i, buf, sizeof(buf), 0);
				if(len == -1)
				{
					perror("rece error\n");
					exit(-4);
				}
				else if(len == 0)
				{
					printf("Client disconneted\n");
					close(i);
					FD_CLR(i, &reads);
				}
				else
				{
					printf("recv buf: %s\n", buf);
					send(i, buf, strlen(buf), 0);
				}
			}
		}
	}
	close(lfd);
	pthread_exit(0);
}

4.3 等待文件描述符的某个事件poll

优点:

  • 可以突破1024限制

缺点:

  • 每次调用selcet,都需要吧fd集合从用户态拷贝到内核态
  • 同时每次调用select都需要在内核遍历传递进来的所有fd
  • 不跨平台
#include <poll.h>

/*
 * 函数功能:
 *		等待文件描述符的某个事件
 * 参数:
 *		fds:数组的地址
 *		nfds:最大文件描述符+1
 *		timeout:
 *			-1:永久阻塞,当检测到fd发生变化时返回
 *			0:调用完立即返回
 *			>0:等待的时长(ms)
 *	返回值:
 *		成功:0
 *		失败:-1
 */
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
	int   fd;         /* 文件描述符 */
	short events;     /* 等待事件 */
	short revents;    /* 实际发生的事件 */
};
事件说明
POLLIN普通或带外优先数据可读,即POLLRDNORM or POLLRDBAND
POLLRDNORM数据可读
POLLRDBAND优先级带数据可读
POLLPRI高优先级可读数据
POLLOUT普通或带外数据可写
POLLWRNORM数据可写
POLLWRBAND优先级带数据可写
POLLERR发生错误
POLLHUP发生挂起
POLLNVAL描述字不是一个打开的文件

代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#include <poll.h>

#define MAX_LISTEN 128

int main(int argc, const char* argv[])
{
	if(argc < 2)
	{
		printf("eg: ./server port\n");
		exit(-1);
	}
	struct sockaddr_in serv_addr;
	socklen_t serv_len = sizeof(serv_addr);
	int port = atoi(argv[1]);
	
	// 创建套接字
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	// 初始化服务器sockaddr_in
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	// 绑定端口和IP
	bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
	// 设置监听上限
	listen(lfd, MAX_LISTEN);
	struct sockaddr_in client_addr;
	socklen_t client_len = sizeof(client_addr);
	
	printf("Start accept...\n");
	
	// poll
	struct pollfd allfd[MAX_LISTEN];
	int max_index = 0;
	// init
	for(int i = 0;i < MAX_LISTEN; ++i)
	{
		allfd[i].fd = -1;
		allfd[i].events = POLLIN;
	}
	allfd[0].fd = lfd;
	while(1)
	{
		int i = 0;
		// 使用poll委托内核做I/O检测
		int ret = poll(allfd, max_index + 1, -1);
		if(ret == -1)
		{
			perror("select error:");
			exit(-2);
		}
		// 判断是否有连接请求
		if(allfd[0].revents & POLLIN)
		{
			client_len = sizeof(client_addr);
			// 接收新连接
			int cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_len);
			// 添加cfd到poll组
			for(i = 0;i < MAX_LISTEN;++i)
			{
				if(allfd[i].fd == -1)
				{
					allfd[i].fd = cfd;
					break;
				}
			}
			// 更新下标
			max_index = max_index < i ? i : max_index;
		}
		// 有数据到达
		for(i = 0; i <= MAX_LISTEN; ++i)
		{
			int fd = allfd[i].fd;
			if(fd == -1)
				continue;
			if(allfd[i].revents & POLLIN)
			{
				// 读取数据
				char buf[1024] = { 0 };
				int len = recv(fd, buf, sizeof(buf), 0);
				if(len == -1)
				{
					perror("rece error\n");
					exit(-4);
				}
				else if(len == 0)
				{
					printf("Client disconneted\n");
					close(fd);
					allfd[i].fd = -1;
				}
				else
				{
					printf("recv buf: %s\n", buf);
					send(i, buf, strlen(buf), 0);
				}
			}
		}
	}
	close(lfd);
	pthread_exit(0);
}

4.4 I/O事件通知epoll

#include <sys/epoll.h>

/*
 * 函数功能:
 *		生成一个epoll专用的文件描述符
 * 参数:
 *		size: epoll上能关注的最大文件描述符
 *	返回值:
 *		成功:文件描述符
 *		失败:-1
 */
int epoll_create(int size);
 /*
 * 函数功能:
 *		控制整个epoll描述符事件,可以注册,修改,删除
 * 参数:
 *		epfd:epoll_create生成的epoll专用的描述符
 *		op:
 *			EPOLL_CTL_ADD:注册
 *			EPOLL_CTL_MOD:修改
 *			EPOLL_CTL_DEL:删除
 *		fd:关联的文件描述符
 *	返回值:
 *		成功:0
 *		失败:-1
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

/*
 * 函数功能:
 *		等待IO事件发生 - 可以设置阻塞的函数
 * 参数:
 *		epfd:要检测的句柄
 *		events:用于回传待处理事件的数组(传出参数)
 *		maxevents:告诉内核events的大小
 *		timeout:
 *			-1:永久阻塞
 *			 0:立即返回
 *			>0:等待时间(ms)
 *	返回值:
 *		成功:发生变化的数量
 *		失败:-1
 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
 
typedef union epoll_data {
	void	    *ptr;
	int			fd;
	uint32_t	u32;
	uint64_t	u64;
}epoll_data_t;

struct epoll_event {
	uint32_t		events;
	epoll_data_t	data;
}
event说明
EPOLLIN
EPOLLOUT
EPOLLERR异常

4.4.1 epoll模型

int main()
{
	// 创建监听的套接字
	int lfd = socket();
	// 绑定
	bind();
	// 监听
	listen();

	// epoll 数根节点
	int epfd = epoll_create(EPOLL_LENGTH);
	// 存储发生变化的fd对应信息
	struct epoll_event all[EPOLL_LENGTH];
	// init 
	// 将监听的lfd加入到epoll树
	struct epoll_event ev;
	ev.data.fd = lfd;
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	while(1)
	{
		// 委托内核检测事件
		int ret = epoll_wait(epfd, all, EPOLL_LENGTH, -1);
		if(ret == -1)
		{
			peintf("epoll_wait error");
			exit(-1);
		}
		for(int i = 0; i < ret; ++i)
		{
			int fd = all[i].data.fd;
			// 有新连接
			if(fd == lfd)
			{
				int cfd = accept();
				// 将cfd加入epoll树
				ev.events = EPOLLIN;
				ev.data.fd = cfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
			}
			// 新的消息
			else 
			{
				// 只处理客户端发送的数据
				if(all[i].events & EPOLLIN)
				{
					int len = recv();
					if(len == 0)
					{
						close(fd);
						// 将fd从树上删除
						epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
					}
					send();
				}
			}
		}
	}
}

4.4.2 epoll代码实现

#include <stdio.h>
#include <sys/epoll.h>
#include <poll.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>

#define MAX_LISTEN 128
#define EPOLL_NUM 100

int main(int argc, const char* argv[])
{
	if(argc < 2)
	{
		printf("eg: ./server port\n");
		exit(-1);
	}
	struct sockaddr_in serv_addr;
	socklen_t serv_len = sizeof(serv_addr);
	int port = atoi(argv[1]);
	
	// 创建套接字
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	// 初始化服务器sockaddr_in
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	// 绑定端口和IP
	bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
	// 设置监听上限
	listen(lfd, MAX_LISTEN);
	struct sockaddr_in client_addr;
	socklen_t client_len = sizeof(client_addr);
	printf("Start accept...\n");

	// 创建epoll树
	int epfd = epoll_create(EPOLL_NUM);
	// 初始化epoll树
	struct epoll_event ev;
	ev.data.fd = lfd;
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	// 需要处理的数组
	struct epoll_event all[EPOLL_NUM];

	while(1)
	{
		// 使用epoll
		int n = epoll_wait(epfd, all, EPOLL_NUM, -1);
		if(n == -1)
		{
			perror("epoll_wait error:");
			exit(-2);
		}
		for(int i = 0;i < n;++i)
		{
			int fd = all[i].data.fd;
			// 新连接
			if(fd == lfd)
			{
				int cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_len);
				if(cfd == -1)
				{
					perror("accept error:");
					exit(-3);
				}
				// 将cfd加入poll树
				struct epoll_event temp;
				temp.data.fd = cfd;
				temp.events = EPOLLIN;
				epoll_ctl(epfd,EPOLL_CTL_ADD, cfd,&temp);
				printf("new connect...\n");
			}
			// 新消息
			else
			{
				char buf[1024] = { 0 };
				if(!all[i].events & EPOLLIN)
				{
					// 其他事件处理
					continue;
				}
				int ret = recv(fd, buf, sizeof(buf), 0);
				if(ret == 0)
				{
					// 断开连接
					printf("client disconnected...\n");
					// 将fd从poll树中删除
					epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
					close(fd);
				}
				else if(ret == -1)
				{
					perror("recv error:");
					exit(-4);
				}
				else
				{
					printf("recv buf:%s\n", buf);
					write(fd, buf, ret);
				}
			}
		}
	}
	close(lfd);
	pthread_exit(0);
}

4.4.3 epoll工作模式

4.4.3.1 水平触发模式(默认)

只要fd对应的缓冲区有数据,epoll_wait就返回,返回的次数与发送数据的次数没有关系。
例如:如果缓冲区中的数据一次没有读取完毕,epoll_wait会再反回,继续读取。

4.4.3.2 边沿(阻塞)触发模式

client给server发送数据:

  • 每发送一次数据server的epoll_wait返回一次
  • 即使数据没有读取完毕也不会再返回

边沿模式可以提高效率
只需要将struct epoll_even中的events添加EPOLLET
示例:

int cfd = accept(lfd, (struct sockaddr*)cli, &len);
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
4.4.3.3 边沿非阻塞模式

该模式效率最高
解决recv/read的阻塞:

  • 使用open()
    – 设置flags
    – 使用O_WDRW | O_NONBLOCK
    – 终端文件:/dev/tty
  • 使用fcntl()
    int flag = fcntl(fd, F_GETFL);
    flg |= O_NOBLOCK;
    fcntl(fd, F_SETFL, flag);
// 将缓冲区数据完全读取
while(recv() > 0)
{
	printf();
}
// 需要判断errno 是否为 EAGAIN

示例代码:

#include <stdio.h>
#include <sys/epoll.h>
#include <poll.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>

#define MAX_LISTEN 128
#define EPOLL_NUM 100

int main(int argc, const char* argv[])
{
	if(argc < 2)
	{
		printf("eg: ./server port\n");
		exit(-1);
	}
	struct sockaddr_in serv_addr;
	socklen_t serv_len = sizeof(serv_addr);
	int port = atoi(argv[1]);
	
	// 创建套接字
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	// 初始化服务器sockaddr_in
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	// 绑定端口和IP
	bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
	// 设置监听上限
	listen(lfd, MAX_LISTEN);
	struct sockaddr_in client_addr;
	socklen_t client_len = sizeof(client_addr);
	printf("Start accept...\n");

	// 创建epoll树
	int epfd = epoll_create(EPOLL_NUM);
	// 初始化epoll树
	struct epoll_event ev;
	ev.data.fd = lfd;
	ev.events= EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	// 需要处理的数组
	struct epoll_event all[EPOLL_NUM];

	while(1)
	{
		// 使用epoll
		int n = epoll_wait(epfd, all, EPOLL_NUM, -1);
		if(n == -1)
		{
			perror("epoll_wait error:");
			exit(-2);
		}
		for(int i = 0;i < n;++i)
		{
			int fd = all[i].data.fd;
			// 新连接
			if(fd == lfd)
			{
				int cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_len);
				if(cfd == -1)
				{
					perror("accept error:");
					exit(-3);
				}
				// 设置为非阻塞
				int flag = fcntl(cfd, F_GETFL);
				flag |= O_NONBLOCK;
				fcntl(cfd, F_SETFL, flag);
				// 将cfd加入poll树
				struct epoll_event temp;
				temp.data.fd = cfd;
				temp.events = EPOLLIN | EPOLLET;
				epoll_ctl(epfd,EPOLL_CTL_ADD, cfd,&temp);
				printf("new connect...\n");
			}
			// 新消息
			else
			{
				char buf[2] = { 0 };
				if(!all[i].events & EPOLLIN)
				{
					// 其他事件处理
					continue;
				}
				int len;
				// 循环读取数据
				while((len = recv(fd, buf, sizeof(buf), 0)) > 0)
				{
					write(STDOUT_FILENO, buf, len);
					send(fd, buf, len, 0);
				}
				if(len == 0)
				{
					// 断开连接
					printf("client disconnected...\n");
					close(fd);
					// 将fd从poll树中删除
					epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
				}
				else if(len == -1 && errno != EAGAIN)
				{
					perror("recv error:");
					exit(-4);
				}
			}
		}
	}
	close(lfd);
	pthread_exit(0);
}

4.5 文件描述符突破1024

  • select - 突破不了,需要重新编译内核
    - 通过数组实现

  • poll和epoll可以突破1024
    - poll内部使用的链表实现
    - epoll内部使用红黑树实现

  • 查看计算机硬件限制的文件描述符上限
    cat /proc/sys/fs/file-max

  • 通过配置文件修改上限
    /etc/security/limits.conf
    在文件尾部添加:
    在这里插入图片描述
    将进程文件描述符上限设置为8000(重启或注销使配置生效)
    也可使用ulimit -n 8000进行修改

4.6 epoll反应堆

自定义epoll模型:

  • 在server - >创建树的根节点-> 在树上添加需要监听的节点->监听读事件->有返回-> 通信->epoll_wait
  • 在server - >创建树的根节点-> 在树上添加需要监听的节点->监听读事件->有返回-> 通信(接收数据)->将这个fd从树上删除->监听写事件->写操作->fd从树上摘下来->监听fd的读事件->epoll_wait

EPOllOUT

  • 水平模式:
    • struct epoll_event ev;
      • ev.events = EPLLOUT; - 检测写缓冲区是否
        可写
      • epoll_wait会一直返回, 缓冲区能写数据,该函数会返回, 缓冲区满的时候, 不返回
  • 边缘模式:
    • 第一次设置的时候epoll_wait会返回一次
    • 缓冲区从满->到不满的时候
  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT灰猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值