基本介绍
- java的NIO,用非阻塞的IO方式,可以用一个线程,处理多个客户端连接,就会使用到selector(选择器)
- selector能够检测多个注册的通道上是否有事件的发生(注意: 多个Channel以事件的方式可以注册到同一个selector), 如果有事件发生,便获取事件然后针对每个事件进行相应的处理. 这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求
- 只有在连接/通道 真正有读写事件发生时, 才会进行读写, 就大大地减少了系统开销, 并且不必为每一个连接都创建线程,不用去维护多个线程
- 避免了多线程之间的上下文切换导致的开销
特点再说明:
- Netty的IO线程NioEventLoop聚合了Selector(选择器)也叫多路复用器, 可以同时并发处理成百上千个客户端连接
- 当线程从某客户端Socket通道进行数据读写时, 若没有线程可用时, 该线程可以进行其他任务
- 线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出
- 由于读写都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起
- 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统阻塞I/O一连接一线程的模型,架构性能,弹性伸缩能力和可靠性都得到了极大的提升
Selector类相关方法
selector类是一个抽象类,常用方法说明如下:
- public static Selector open() // 得到一个选择器对象
- public abstract int select(long timeout); // 监控所有注册的通道,当其中有IO操作可以进行时,将对应的selectorKey加入内部集合并返回,参数用来设置超时时间
- public abstract Set selectedKeys(); // 从内部集合中得到所有的SelectorKey
注意事项
- NIO中的ServerSocketChannel功能类似 ServerSocket, SocketChannel功能类似Socket
- selector相关方法说明
selector.select() // 阻塞获取直到下一次事件出现
selector.select(1000);// 阻塞1000毫秒,若期间无事件则1000毫秒后返回
selector.wakeup(); // 唤醒selector
selector.selectNow()// 不阻塞,立马返回
NIO非阻塞网络编程原理分析图
NIO非阻塞网络编程相关的(Selector, SelectionKey,ServerSocketChannel,和SocketChannel)关系梳理图
对上图说明
- 当客户端连接时,会通过serverSocketChannel得到SocketChannel
- selector进行监听select()方法,返回有事件发生的通道的个数
- 将socketChannel注册到selector上,
public final SelectionKey register(Selector sel, int ops)
一个selector可以注册多个socketChannel- 注册之后会返回一个selectionKey,会和该selector关联(集合)
- 进而得到各个 SelectionKey (有事件发生的)
- 在通过SelectionKey 反向获取socketChannel
- 可以通过获取的channel完成对应的业务处理
代码示例
- 编写一个NIO入门案例,实现服务端和客户端之间的简单数据通讯(非阻塞)
- 目的: 理解NIO非阻塞网络编程机制
server服务端代码
package com.ding.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
// 创建serverSocketChannel -> serverSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 得到一个selector对象
Selector selector = Selector.open();
// 绑定一个端口6666,在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 将 serverSocketChannel注册到selector,关心事件为op_Accept
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环等待客户端连接
while (true) {
// 这里等待一秒,若无事件发生就返回
if (selector.select(1000) == 0) {
// 等待一秒若无事件发生则跳出循环
System.out.println("服务器等待了一秒, 无连接");
continue;
}
// 如果有返回代表有事件发生 --> 获取相关的selectionKeys集合
// 1.如果返回大于0,表示已经有关注的事件发生
// 2.selector.selectedKeys() 获取关注事件的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 便利集合,使用迭代器
for (SelectionKey selectionKey : selectionKeys) {
// 获取到当前的selectionKey
// 查看这个key对应的通道发生的事件做不同的处理
if (selectionKey.isAcceptable()) {// OP_ACCEPT
// 有新的客户端连接 -- > 给该客户端生成socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成一个 socketChannel =="+ socketChannel.hashCode());
// 将socketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
// 将当前的socketChannel注册到selector上面, 关注事件为读OP_READ,并且关联一个buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {
// 发生OP_READ 可读的
// 通过key反向获取对应的channel
SocketChannel channel = (SocketChannel)selectionKey.channel();
// 获取到该channel关联的buffer
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
// 开始读取数据 --> 把channel数据读入buffer
channel.read(byteBuffer);
System.out.println("from 客户端=" + new String(byteBuffer.array()));
}
// 从集合中移出当前的selectionKey,防止重复
selectionKeys.remove(selectionKey);
}
}
}
}
客户端代码
package com.ding.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NioClient {
public static void main(String[] args) throws IOException {
// 得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 设置非阻塞模式
socketChannel.configureBlocking(false);
// 提供服务器的ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
// 连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
// 如果不成功
while (!socketChannel.finishConnect()) {
System.out.println("连接需要时间,客户端不会阻塞,可以做其他工作");
}
// 如果连接成功,就发送数据
String str = "hello world";
// 将字节数组包装到缓冲区中。
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
// 发送数据,将buffer数据写入cahnnel
socketChannel.write(byteBuffer);
System.in.read();
}
}
}
SelectionKey
相关方法
public abstract Selector selector(); // 得到与之关联的selector对象
public abstract SelectableChannel channel(); // 得到与之关联的通道channel
public final Object attachment() // 得到与之关联的共享数据, 例如Buffer
public abstract SelectionKey interestOps(int ops); // 设置或改变监听事件
public final boolean isAcceptable() // 是否可以accpet
public final boolean isReadable() // 是否可以读
public final boolean isWritable() // 是否可以写
ServerSocketChannel
ServerSocketChannel在服务器端监听客户端Socket连接
相关方法如下
- public static ServerSocketChannel open() // 得到一个ServerSocketChannel通道
- public final ServerSocketChannel bind(SocketAddress local) // 设置服务器端端口号
- public final SelectableChannel configureBlocking(boolean block) // 设置阻塞或非阻塞模式,取值false表示采用非阻塞模式
- public abstract SocketChannel accept() // 接受一个连接,返回代表这个连接的通道对象,为这个连接构建对应通道
- public final SelectionKey register(Selector sel, int ops) // 注册一个选择器并设置监听事件
- SelectionKey.OP_ACCEPT; // 建立连接
SelectionKey.OP_READ; // 读取客户端传来数据
SelectionKey.OP_CONNECT; // 连接建立成功
SelectionKey.OP_WRITE; // 给客户端写数据
SocketChannel
- SocketChannel, 网络通道,具体负责进行读写操作. NIO把缓冲区的数据写入通道,或者把通道中的数据读到缓冲区
- 相关方法如下
public static SocketChannel open(); //得到一个SocketChannel通道
public final SelectableChannel configureBlocking(boolean block); //设置阻塞或非阻塞模式, 取值false表示采用非阻塞模式
public abstract boolean connect(SocketAddress remote); // 连接服务器
public abstract boolean finishConnect(); // 如果上面的方法连接失败,接下来就要通过该方法完成连接操作
public abstract int write(ByteBuffer src); //往通道里面写数据
public abstract int read(ByteBuffer dst); //从通道里读数据
public final SelectionKey register(Selector sel, int ops,Object att);//注册一个选择器,并设置监听事件,最后一个参数可以设置共享数据
public final void close(); //关闭通道