java nio多路复用 selector

房东的猫 提交于 2020-02-12 19:36:59

多路复用selector

多路复用

I/O多路复用,I/O是指网络I/O, 多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程;简单来说:就是使用一个或者几个线程处理多个TCP连接;最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程;多路复用分为三种形式select/epoll/poll,在 Java 中, Selector 这个类是 select/epoll/poll 的外包类, 在不同的平台上, 底层的实现可能有所不同, 但其基本原理是一样的, 其原理图如下所示:

unix内核中的select/epoll/poll

select
函数:
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

返回值:就绪描述符的数目,超时返回0,出错返回-1
maxfdp1:描述符个数
*readset、*writeset、*exceptset:读、写和异常条件的描述字
*timeout:超时时间
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0

基本原理:
调用后select函数会阻塞住,等有数据 可读、可写、出异常 或者 超时 就会返回,以下是调用过程:
select中的fds(文件描述符集合,unix中一切皆为文件描述符)需要拷贝到内核中(这里会有一定的性能损失),所以select的fds不能太大一般要求最大1024个fds
监视文件3类描述符: writefds、readfds、和exceptfds
等select调用完后(这里可能是因为有描述符时间或者超时时间到),只要返回值不等于0,就必须遍历所有socket进行查看到底哪个socket有fds事件(因为返回值中并没有事件的fds列表)
select函数正常返回后,通过遍历fdset整个数组才能发现哪些句柄发生了事件,来找到就绪的描述符fd,然后进行对应的IO操作
几乎在所有的平台上支持,跨平台支持性好

缺点:
1)select采用轮询的方式扫描文件描述符,全部扫描,随着文件描述符FD数量增多而性能下降
2)每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
2)最大的缺陷就是单个进程打开的FD有限制,默认是1024 (可修改宏定义,但是效率仍然慢)
static final int MAX_FD = 1024

poll

select() 和 poll() 系统调用的大体一样,处理多个描述符也是使用轮询的方式,根据描述符的状态进行处理
一样需要把 fd 集合从用户态拷贝到内核态,并进行遍历。
最大区别是: poll没有最大文件描述符限制(使用链表的方式存储fd)

函数:
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
struct pollfd {
    int fd;         /* 文件描述符 */
    short events;   /* 等待的事件 */
    short revents;  /* 实际发生了的事件 */
};

每个结构体的 events 域是监视该文件描述符的事件掩码, 由用户来设置这个域. revents 域是文件描述符的操作结果事件掩码, 内核在调用返回时设置这个域.
events 域中请求的任何事件都可能在 revents 域中返回. 事件如下:

描述
POLLIN 有数据可读
POLLRDNORM 有普通数据可读
POLLRDBAND 有优先数据可读
POLLPRI 有紧迫数据可读
POLLOUT 写数据不会导致阻塞
POLLWRNORM 写普通数据不会导致阻塞
POLLWRBAND 写优先数据不会导致阻塞
POLLMSGSIGPOLL 消息可用
POLLER 指定的文件描述符发生错误
POLLHUP 指定的文件描述符挂起事件
POLLNVAL 指定的文件描述符非法

poll() 可以监视多个文件描述符.
如果返回值是 3, 我们需要逐个去遍历出返回值是 3 的 socket, 然后在做对应操作,与上边讲的select的文件描述符集市一样的

epoll

poll和select 方法都有一个非常大的缺陷. 函数的返回值是一个整数, 得到了这个返回值以后, 我们还是要逐个去检查, 比如说, 有一万个 socket 同时 poll, 返回值是3, 我们还是只能去遍历这一万个 socket, 看看它们是否有IO动作。这就很低效了, 于是, 就有了 epoll 的改进, epoll可以直接通过“输出参数”(可以理解为C语言中的指针类型的参数), 一个 epoll_event 数组, 直接获得有事件的 socket, 这就比较快了。
基本原理:
在2.6内核中提出的,对比select和poll,epoll更加灵活,没有描述符限制,用户态拷贝到内核态只需要一次然后使用内存映射的方式进行操作共享内存
使用事件通知,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用callback的回调机制来激活对应的fd

Linux内核核心函数
epoll_create()  在Linux内核里面申请一个文件系统 B+树,返回epoll对象,也是一个fd
epoll_ctl()   操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数
epoll_wait()  判断并完成对应的IO操作,相当于select

优点:
1)没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
2)效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降
2)通过callback机制通知,内核和用户空间mmap同一块内存实现

缺点:
编程模型比select/poll 复杂

代码样例

服务端:

package com.zkl.netty.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class WebServer {
	private static final int READ_SIZE = 1024;

	public static void main(String[] args) {
		try {
			ServerSocketChannel ssc = ServerSocketChannel.open();
			ssc.socket().bind(new InetSocketAddress(8080));
			ssc.configureBlocking(false);// 配置非阻塞通道

			// 创建一个选择器
			Selector selector = Selector.open();
			// 注册 channel,并且指定感兴趣的事件是 Accept,这里其实是在内核中进行注册
			ssc.register(selector, SelectionKey.OP_ACCEPT);

			ByteBuffer readBuff = ByteBuffer.allocate(READ_SIZE);
			ByteBuffer writeBuff = ByteBuffer.allocate(128);
			writeBuff.put(("received").getBytes());
			writeBuff.flip();

			while (true) {
				// select方法返回上次调用select和这次调用select之间就绪的键个数;
				//此处方法阻塞,一般来讲只要返回则nReady就不会为0。
				int nReady = selector.select();
				// 选择select的所有键集,这里就是从内核态拷贝到用户态
				Set<SelectionKey> keys = selector.selectedKeys();
				Iterator<SelectionKey> it = keys.iterator();

				// 此处会轮询发生io事件的键(unix中叫fd(文件描述符))
				while (it.hasNext()) {
					SelectionKey key = it.next();
					it.remove();

					if (key.isAcceptable()) {
						// 创建新的连接,并且把连接注册到selector上,而且,
						// 声明这个channel只对读操作感兴趣。
						SocketChannel socketChannel = ssc.accept();
						socketChannel.configureBlocking(false);
						socketChannel.register(selector, SelectionKey.OP_READ);
					} else if (key.isReadable()) {
						SocketChannel socketChannel = (SocketChannel) key.channel();
						readBuff.clear();
						int readNum = socketChannel.read(readBuff);
						readBuff.flip();

						// 如果客户端关闭也就是read方法返回-1则关闭socketChannel
						if (-1 != readNum) {
							System.out.println("received : " + new String(readBuff.array()).trim());
							key.interestOps(SelectionKey.OP_WRITE);
						} else {
							socketChannel.close();
						}
					} else if (key.isWritable()) {
						writeBuff.rewind();
						SocketChannel socketChannel = (SocketChannel) key.channel();
						socketChannel.write(writeBuff);
						key.interestOps(SelectionKey.OP_READ);
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

客户端:

package com.zkl.netty.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class WebClient {
	public static void main(String[] args) throws IOException {
		SocketChannel socketChannel = null;
		try {
			socketChannel = SocketChannel.open();
			socketChannel.connect(new InetSocketAddress(8080));

			ByteBuffer writeBuffer = ByteBuffer.allocate(32);
			ByteBuffer readBuffer = ByteBuffer.allocate(32);

			writeBuffer.put("hello".getBytes());
			writeBuffer.flip();

			writeBuffer.rewind();
			socketChannel.write(writeBuffer);
			readBuffer.clear();
			socketChannel.read(readBuffer);
			System.out.println(new String(readBuffer.array()));
		} catch (IOException e) {
		} finally {
			if (socketChannel != null) {
				try {
					socketChannel.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!