一.堵塞式与非堵塞式
在传统IO中,将数据由当前线程从客户端传入服务端,由服务端的内核进行判断传过来的数据是否合法,内核中是否存在数据。
如果不存在数据 ,并且数据并不合法,当前线程将会堵塞等待。当前线程将无法进行下一步传输,进行排队现象。降低系统性能。
为了解决这一步问题,调用资源开辟多个线程传输。
虽然线程的开辟解决了部分堵塞排队的问题,但由于并没有治理根本堵塞的原因,线程数量也是有限的。总会有堵塞的线程 ,形成排队现象。
为了根本解决堵塞的问题。NIO的非堵塞式成为了主要的传输方式。
在客户端和服务端之间将通道注册到selector选择器,由选择器进行监听channel是否进行什么操作(read()or write())。
当数据就绪或者准备完成时,由selector进行分配到服务端的一个(或多个)线程上进行相关运行操作。
在IO的堵塞后无脑调用线程下。NIO是在准备完成时,才被selector选择分配到一个或者多个线程上传输并被复制到内核地址空间中,由于数据已准备完成或者已就绪,内核就无须被堵塞。
二.Selector(选择器)
也称多路复用器,多条channel复用selector。channe通过注册到selector ,使selector对channel进行监听,
实现尽可能少的线程管理多个连接。减少了 线程的使用,降低了因为线程的切换引起的不必要额资源浪费和多余的开销。
也是网络传输非堵塞的核心组件。
三.Selector的使用
分为客户端和服务端两部分:
先实现客户端吧:
流程: 获取通道绑定主机端口 --> 切换非堵塞状态 --> 开辟buffer容量 --> 将当前时间作为数据写入buffer待传 --> 切换读写方式flip() --> 写入通道 -->清空并关闭
1 /* 2 * 客户端发送数据 通过channel通道 3 * */ 4 @Test 5 public void Client() throws IOException { 6 7 //获取channel通道 并设置主机号和端口号 8 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8080)); 9 10 //因为使用非阻塞NIO 所以必须切换为非阻塞 11 socketChannel.configureBlocking(false); //默认为true 需要改为非堵塞的 12 13 //开辟缓冲区进行存储数据 14 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); 15 16 //准备工作就绪后,准备发送数据给服务端 17 //打印当前日期转为Byte数据传出 18 byteBuffer.put(new Date().toString().getBytes()); 19 //切换读写模式 20 byteBuffer.flip(); 21 //写入通道 22 socketChannel.write(byteBuffer); 23 //完毕时,清除缓冲区内容 24 byteBuffer.clear(); 25 26 //==================== 27 //关闭相关流 28 socketChannel.close(); 29 30 }
在获取当前时间是用的new Date();还可以使用java8的获取时间的方法。
LocalDateTime.now().toString().getBytes() //转为Byte字节
因为是网络传输的心形式,所以在获取channel时,使用SocketChannel.open方法。实现方法:
1 public static SocketChannel open(SocketAddress remote) 2 throws IOException 3 { 4 SocketChannel sc = open(); 5 try { 6 sc.connect(remote); //打开一个新的channel时,绑定连接到主机和端口上 7 } catch (Throwable x) { 8 try { 9 sc.close(); //异常时关闭连接 10 } catch (Throwable suppressed) { 11 x.addSuppressed(suppressed); 12 } 13 throw x; 14 } 15 assert sc.isConnected(); 16 return sc; 17 }
new InetSocketAddress实例创建主机和端口。
*/ public InetSocketAddress(String hostname, int port) { checkHost(hostname); //检查主机号是否为空 为空返回异常。 InetAddress addr = null; String host = null; try { addr = InetAddress.getByName(hostname); } catch(UnknownHostException e) { host = hostname; } holder = new InetSocketAddressHolder(host, addr, checkPort(port)); //检查端口。 }
//检查端口方法
private static int checkPort(int port) {
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException("port out of range:" + port);
return port;
}
//检查主机号方法
private static String checkHost(String hostname) {
if (hostname == null)
throw new IllegalArgumentException("hostname can't be null");
return hostname;
}
服务端:
流程:使用ServerSocketChannel 的方法获取服务端额channel --> 切换为堵塞状态 --> 为buffer分配容量 --> 绑定端口号 --> 获取selector选择器 --> channel注册进选择器中,并进行监听 --> 选择器进行轮询,进行下一步读写操作。
1 /* 2 * 服务端接收客户端传来的数据 3 * */ 4 @Test 5 public void server() throws IOException { 6 7 //获取channel通道 8 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 9 //切换为非堵塞状态 10 serverSocketChannel.configureBlocking(false); 11 //分配服务端的缓冲区 12 ByteBuffer serverByteBuffer = ByteBuffer.allocate(1024); 13 //将客户端的InetSocketAddress绑定到通道,不绑定 不统一将获取不到数据 14 serverSocketChannel.bind(new InetSocketAddress(8080)); 15 //获取选择器 16 Selector selector = Selector.open(); 17 //将通道注册到选择器中,并且制定监听方式 18 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); 19 //进行轮询选择器上就绪成功的事件 当存在就绪成功的及进行下一步 20 while (selector.select() > 0){ 21 //对已存在的就绪事件进行迭代 22 Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator(); 23 24 //有元素就进行下一步 25 while (selectionKeyIterator.hasNext()){ 26 //获取到就绪事件 27 SelectionKey next = selectionKeyIterator.next(); 28 29 //对获取到的就绪事件判断是何种类型 30 if (next.isAcceptable()){ 31 32 //获取连接 33 SocketChannel accept = serverSocketChannel.accept(); 34 35 //将获取到的连接切换为非堵塞模式 36 accept.configureBlocking(false); 37 38 //将获取到的链接 注册金selector 39 accept.register(selector,SelectionKey.OP_READ); 40 41 //判断是否准备好读 42 }else if (next.isReadable()){ 43 44 //获取已就绪的通道 45 SocketChannel channel = (SocketChannel) next.channel(); 46 47 //分配缓冲区 48 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); 49 50 //读取数据 51 int length = 0 ; 52 while ((length = channel.read(byteBuffer)) > 0){ 53 byteBuffer.flip(); 54 System.out.println(new String(byteBuffer.array(),0,length)); 55 byteBuffer.clear(); 56 } 57 58 59 } 60 61 //完成传输需要取消选择键,防止下次出问题 62 selectionKeyIterator.remove(); 63 64 } 65 } 66 67 68 }
如何获取选择器?
Selector selector = Selector.open();
实现过程:
public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); } //首先进入此方法判断是否存在选择器 public static SelectorProvider provider() { synchronized (lock) { if (provider != null) //第一次为false return provider; return AccessController.doPrivileged( new PrivilegedAction<SelectorProvider>() { public SelectorProvider run() { if (loadProviderFromProperty()) return provider; if (loadProviderAsService()) return provider; provider = sun.nio.ch.DefaultSelectorProvider.create(); return provider; } }); } }
//false时 跳入如下方法。
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
随后将获取到的通道注册到获取到的选择器中,在注册时给定监听方式:
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //可多选监听操作项
selectionKey中定义了四个可操作项:
-
OP_READ 可读就绪
-
OP_WRITE 可写就绪
-
OP_CONNECT 连接就绪
-
OP_ACCEPT 接收就绪
迭代key中已就绪的元素。
Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
获取到当前就绪事件丛迭代器中获取。
selectionKeyIterator.next()
selectionKey包含四个方法:
-
isReadable():测试此选择键是否可读
-
isWritable():测试此选择键是否可写
-
isConnectable():测试此选择键是否完成
-
isAcceptable():测试此选择键是否可以接受一个新的连接
通过这些相应的方法,单独判断是否可以读写,和进行操作。
最后取消选择键,防止下次获取出现异常情况。(第一次判断可能会为true)
selectionKeyIterator.remove();
四.附加
在上面的例子中,把客户端的代码进行稍微改写一下,使之能够无限输入,并通过传输打印在服务端中。
public static void main(String[] args) throws IOException { //获取channel通道 并设置主机号和端口号 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8080)); //因为使用非阻塞NIO 所以必须切换为非阻塞 socketChannel.configureBlocking(false); //开辟缓冲区进行存储数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //附加输入: Scanner scanner = new Scanner(System.in); //通过控制台键入数据 while (scanner.hasNext()){ String str = scanner.next(); //准备工作就绪后,准备发送数据给服务端 //打印当前日期转为Byte数据传出 byteBuffer.put((new Date().toString()+":--->"+str).getBytes()); //切换读写模式 byteBuffer.flip(); //写入通道 socketChannel.write(byteBuffer); //完毕时,清除缓冲区内容 byteBuffer.clear(); } }
由于扫描流(scanner)不能用于测试类,所以在main方法下进行测试:
每次输入的内容都会被转为Byte字节进行传输。
客户端输入结果:
服务端输出结果:
每输入一次便传输一次。
//完成传输需要取消选择键,防止下次出问题
selectionKeyIterator.remove();