网络编程:IO多路复用

1 select

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

(1)nfds:被监听的文件描述符的总数,通常被设置为select监听的所有文件描述符中的最大值加1

(2)
readfds:可读的文件描述符集合
writefds:可写的文件描述符集合
exceptfds:异常的文件描述符集合

fd_set

#include<typesizes.h>
#define__FD_SETSIZE 1024
#include<sys/select.h>
#define FD_SETSIZE__FD_SETSIZE
typedef long int__fd_mask;
#undef__NFDBITS
#define__NFDBITS(8*(int)sizeof(__fd_mask))
typedef struct
{
#ifdef__USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->fds_bits)
#else
    __fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->__fds_bits)
#endif
}fd_set;

fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符,fd_set能容纳的文件描述符数量由FD_SETSIZE指定。

操作:

FD_ZERO(fd_set* fdset);/*清除fdset的所有位*/
FD_SET(int fd, fd_set* fdset);/*设置fdset的位fd*/
FD_CLR(int fd, fd_set* fdset);/*清除fdset的位fd*/
int FD_ISSET(int fd, fd_set* fdset);/*测试fdset的位fd是否被设置*/

(3)timeout:超时时间。

struct timeval
{
    long tv_sec;/*秒数*/
    long tv_usec;/*微秒数*/
};

如果给timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果给timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。

select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。

select失败时返回-1并设置errno。

select每次调用需要把fd_set集合,从用户空间copy到内核空间。

1.1 select使用范例

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(ip); 
servaddr.sin_port = htons(port);  
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
	printf("bind failed: %s\n", strerror(errno));
}
listen(sockfd, 10);
printf("listen finshed: %d\n", sockfd); // 3 
struct sockaddr_in  clientaddr;
socklen_t len = sizeof(clientaddr);

fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd;

while (1) {
	int nready = select(maxfd+1, &rfds, NULL, NULL, NULL);
	if (FD_ISSET(sockfd, &rfds)) { // accept
		int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
		printf("accept finshed: %d\n", clientfd);
		FD_SET(clientfd, &rfds);
		
		if (clientfd > maxfd) maxfd = clientfd;
	}
	// recv
	int i = 0;
	for (i = sockfd+1; i <= maxfd;i++) {
		if (FD_ISSET(i, &rfds)) {
			char buffer[1024] = {0};
			
			int count = recv(i, buffer, 1024, 0);
			if (count == 0) { // disconnect
				printf("client disconnect: %d\n", i);
				close(i);
				FD_CLR(i, &rfds);
				
				continue;
			}
			printf("RECV: %s\n", buffer);
			count = send(i, buffer, count, 0);
			printf("SEND: %d\n", count);
		}
	}
}
getchar();
printf("exit\n");
return 0;

上述代码中,select监听了两种fd,一种是listenfd,监听是否有连接,有连接后,select监听了clientfd,即监听是否有数据。

2 poll

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

(1)fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。

struct pollfd
{
    int fd;
    short events;
    short revents;
};

在这里插入图片描述

(2)nfds参数指定被监听事件集合fds的大小

(3)timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。

poll与select并没有太大差别,差别仅为select需传readfds、writefds、exceptfds这三个参数,而poll中只需要传fds。

3 epoll

epoll使用一组函数来完成任务,而不是单个函数。

epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。因此,epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

3.1 epoll_create

int epoll_create(int size);

创建内核时间表对应的fd。

3.2 epoll_ctl

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

操作事件:

  • EPOLL_CTL_ADD,往事件表中注册fd上的事件。
  • EPOLL_CTL_MOD,修改fd上的注册事件。
  • EPOLL_CTL_DEL,删除fd上的注册事件。
struct epoll_event
{
    __uint32_t events; /*epoll事件*/
    epoll_data_t data; /*用户数据*/ 
};

epoll支持的事件类型和poll基本相同,表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如epoll的数据可读事件是EPOLLIN。

3.3 epoll_wait

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

等待一组文件描述符上的事件。

成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。

epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中。

3.4 LT和ET模式

LT(Level Trigger,水平触发):如果某个文件描述符上有数据可读,内核会持续通知应用程序,直到应用程序处理完数据或者缓冲区不再有数据可读为止

ET(Edge Trigger,边缘触发):只有在文件描述符状态变化(对于EPOLLIN事件,只有在状态从未就绪变化为就绪,才叫做变化)发生时才会触发事件

对于listenfd,需要使用LT,因为每次有连接时,就会触发。

3.5 epoll使用范例

int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
	struct epoll_event events[1024] = {0};
	int nready = epoll_wait(epfd, events, 1024, -1);
	int i = 0;
	for (i = 0;i < nready;i ++) {
		int connfd = events[i].data.fd;
		if (connfd == sockfd) {
			
			int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
			printf("accept finshed: %d\n", clientfd);
			ev.events = EPOLLIN;
			ev.data.fd = clientfd;
			epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
		} else if (events[i].events & EPOLLIN) {
			char buffer[1024] = {0};
			
			int count = recv(connfd, buffer, 1024, 0);
			if (count == 0) { // disconnect
				printf("client disconnect: %d\n", connfd);
				close(connfd);
				epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
				
				continue;
			}
			printf("RECV: %s\n", buffer);
			count = send(connfd, buffer, count, 0);
			printf("SEND: %d\n", count);
		}
	}
}

3.6 epoll的优势

epoll更适用于高并发场景,因为select/poll每次都需要将参数从用户空间拷贝到内核空间,而epoll只需要注册事件,当有事件的时候内核会直接通知,避免了频繁的拷贝。

参考文献:
《Linux高性能服务器编程》

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值