1.Selector(多路复用)
原先的bio中,一个客户端连接,就为它分配一个线程。这样的问题,当用户激增时候,线程会增加很多,增加服务器开销。
所以后来使用了线程池进行管理线程,但是有个弊端,如果线程池有100个线程,这个时候第101个就会等待。传统的bio(Server/Client)如下图:
有这个弊端,Nio就用selector解决。
NIO中非阻塞I/O 采用了基于Reactor模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定I/O 事件,如可读数据到
达,新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO中实现非阻塞I/O的核心对象就是Selector,Selector 就是
注册各种I/O 事件地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:
从图中可以看出,当有读或写等任何注册的事件发生时,可以从Selector 中获得相应的SelectionKey,同时从 SelectionKey中可
以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
使用NIO中非阻塞I/O 编写服务器处理程序,大体上可以分为下面三个步骤:
1. 向Selector 对象注册感兴趣的事件。
2. 从Selector 中获取感兴趣的事件。
3. 根据不同的事件进行相应的处理。
/* * 注册事件 */
private Selector getSelector() throws IOException {
// 创建 Selector 对象
Selector sel = Selector.open();
// 创建可选择通道,并配置为非阻塞模式
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
// 绑定通道到指定端口
ServerSocket socket = server.socket();
InetSocketAddress address = new InetSocketAddress(port);
socket.bind(address);
// 向 Selector 中注册感兴趣的事件
server.register(sel, SelectionKey.OP_ACCEPT); return sel;
}
创建了ServerSocketChannel对象,并调用 configureBlocking()方法,配置为非阻塞模式,接下来的三行代码把该通道绑定到指定端口,最后向Selector 中注册事件,此处指定的是参数是OP_ACCEPT,即指定我们想要监听accept 事件,也就是新的连接发 生时所产生的事件,对于ServerSocketChannel 通道来说,我们唯一可以指定的参数就是OP_ACCEPT。
当Selector 中获取感兴趣的事件,即开始监听,进入内部循环:
public void listen(){
System.out.println("listen on " + this.port + ".");
try {
//轮询主线程
while (true){
//大堂经理再叫号
selector.select();
//每次都拿到所有的号子
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
//不断地迭代,就叫轮询
//同步体现在这里,因为每次只能拿一个key,每次只能处理一种状态
while (iter.hasNext()){
SelectionKey key = iter.next();
iter.remove();
//每一个key代表一种状态
//没一个号对应一个业务
//数据就绪、数据可读、数据可写 等等等等
process(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
在非阻塞I/O 中,内部循环模式基本都是遵循这种方式。首先调用select()方法,该方法会阻塞,直到至少有一个事件发生,然后
再使用selectedKeys()方法获取发生事件的SelectionKey,再使用迭代器进行循环。
最后根据不同事件进行不同处理:
private void process(SelectionKey key) throws IOException {
//针对于每一种状态给一个反应
if(key.isAcceptable()){
ServerSocketChannel server = (ServerSocketChannel)key.channel();
//这个方法体现非阻塞,不管你数据有没有准备好
//你给我一个状态和反馈
SocketChannel channel = server.accept();
//一定一定要记得设置为非阻塞
channel.configureBlocking(false);
//当数据准备就绪的时候,将状态改为可读
key = channel.register(selector,SelectionKey.OP_READ);
}
else if(key.isReadable()){
//key.channel 从多路复用器中拿到客户端的引用
SocketChannel channel = (SocketChannel)key.channel();
int len = channel.read(buffer);
if(len > 0){
buffer.flip();
String content = new String(buffer.array(),0,len);
key = channel.register(selector,SelectionKey.OP_WRITE);
//在key上携带一个附件,一会再写出去
key.attach(content);
System.out.println("读取内容:" + content);
}
}
else if(key.isWritable()){
SocketChannel channel = (SocketChannel)key.channel();
String content = (String)key.attachment();
channel.write(ByteBuffer.wrap(("输出:" + content).getBytes()));
channel.close();
}
}
2.Channel
通道是一个对象。我们用来读取和输出对象。里面的数据我们不是用bio中的字节流处理,而是用buffer缓冲区。是将数据从通
道读入缓冲区,再从缓冲区获取这个字节。
在NIO 中,提供了多种通道对象,而所有的通道对象都实现了 Channel 接口。它们之间的继承关系如下图所示: