概述

上篇中已经讲到Java中的NIO类库,Java中也称New IO,类库的目标就是要让Java支持非阻塞IO,基于这个原因,更多的人喜欢称Java NIO为非阻塞IO(Non-Block IO),称“老的”阻塞式Java IO为OIO(Old IO)。

总体上说,NIO弥补了原来面向流的OIO同步阻塞的不足,它为标准Java代码提供了高速的、面向缓冲区的IO。

了解上一篇讲到的四种I/O模型的话,我们可以很容易看出Java NIO采用的是IO多路复用(IO Multiplexing)模型。

NIO特征:

NIO中引入了Channel(通道)和Buffer(缓冲区)的概念。读取和写入,只需要从通道中读取数据到缓冲区中,或将数据从缓冲区中写入到通道中,读取Buffer的数据可以是无序的读取。

NIO使用了通道和通道的多路复用技术 来实现非阻塞的操作,当我们调用read方法时,如果此时有数据,则read读取数据并返回;如果此时没有数据,则read直接返回,而不会阻塞当前线程。

NIO有选择器,NIO的选择器实现,是基于底层的选择器的系统调用,需要底层操作系统提供支持

Java NIO 核心组件

Java NIO由以下三个核心组件组成:

Channel(通道)

Buffer(缓冲区)

Selector(选择器)

1. 通道(Channel)

在NIO中,同一个网络连接使用一个通道表示,所有NIO的IO操作都是从通道开始的。一个通道类似于OIO中的两个流的结合体,既可以从通道读取,也可以向通道写入。

2. 缓冲区(Buffer)

应用程序与通道(Channel)主要的交互操作,就是进行数据的read读取和write写入,这里就需要依赖NIO Buffer(NIO缓冲区),它是数据的载体。

通道的读取,就是将数据从通道读取到缓冲区中;通道的写入,就是将数据从缓冲区中写入到通道中

3. 选择器(Selector)

在上一篇中提到过文件句柄数,这里的文件句柄其实就是文件描述符,它标识的就是一个网络连接。

一个进程/线程可以同时监视多个文件描述符。在NIO中通过选择器(Selector)对这些文件描述符进行监视,监视哪些文件描述符是可读或者可写的。

selector

IO多路复用,从具体的开发层面来说,首先把通道注册到选择器中,然后通过选择器内部的机制,可以查询(select)这些注册的通道是否有已经就绪的IO事件(例如可读、可写、网络连接完成等)。

一般来说,一个单线程处理一个选择器,一个选择器可以监控很多通道。通过选择器,一个单线程可以处理数百、数千、数万、甚至更多的通道。在极端情况下(数万个连接),只用一个线程就可以处理所有的通道,这样会大量地减少线程之间上下文切换的开销。

由于Java NIO的Selector组件和操作系统底层的IO多路复用的支持,我们可以很简单地使用一个线程,通过选择器去管理多个通道。

NIO Buffer(缓冲区)

在NIO中有8种缓冲区类,分别如下:

ByteBuffer

CharBuffer

DoubleBuffer

FloatBuffer

IntBuffer

LongBuffer

ShortBuffer

MappedByteBuffer

※MappedByteBuffer是专门用于内存映射的一种ByteBuffer类型

这些Buffer类在其内部,有一个byte[]数组内存块,作为内存缓冲区。

查看其中源码,如下

//ByteBuffer类 代码片段

//

final byte[] hb; // Non-null only for heap buffers

Buffer类的重要成员属性:capacity(容量)、position(读写位置)、limit(读写的限制)、mark(标记)

※说明:capacity容量不是指内存块byte[]数组的字节的数量。capacity容量指的是写入的数据对象的数量。

通过简单地使用Buffer示例加深对这四个属性的印象,创建BufferTest.java

package com.zhxin.nettylab.nio.chapter1;

import java.nio.CharBuffer;

/**

* @ClassName BufferTest

* @Description //BufferTest

* @Author singleZhang

* @Email 405780096@qq.com

* @Date 2020/12/3 0003 下午 4:03

**/

public class BufferTest {

public static void main(String[] args){

//创建Buffer,capacity为10

CharBuffer cbf = CharBuffer.allocate(10);

System.out.println(cbf.capacity()); //容量:10

System.out.println(cbf.limit()); //读写限制:10

System.out.println(cbf.position()); //读写位置:0 起始值

cbf.put("a");

cbf.put("b");

cbf.put("c");

System.out.println(cbf.position()); //输出3

cbf.flip(); //buffer从写入转换成读取,把limit设置为position,把position还原成0

System.out.println(cbf.position());

System.out.println(cbf.limit());

//取值

System.out.println(cbf.get()); //取第一个元素 a

System.out.println(cbf.position()); //读写位置变为1

cbf.clear(); //clear方法将limit设置成capacity,position设置成0

System.out.println(cbf.limit());

System.out.println(cbf.position());

System.out.println(cbf.get(2)); //读取第三个元素c

System.out.println(cbf.position());//读写位置不变,get方法加了索引值,根据索引来取值不影响position

System.out.println(cbf.get());

System.out.println(cbf.get());

cbf.mark(); //标记

System.out.println(cbf.position()); //标记后的位置为2

System.out.println(cbf.get());

System.out.println(cbf.position());

cbf.reset();//返回标记

System.out.println(cbf.position()); //返回标记的位置2

cbf.clear();//读取完成后,调用Buffer.clear() 或Buffer.compact()方法,将缓冲区转换为写入模式

System.out.println(cbf.limit());

System.out.println(cbf.position());

System.out.println(cbf.capacity());

}

}

除了前面的3个属性,第4个属性mark(标记)比较简单。就是相当一个暂存属性,暂时保存position的值,方便后面的重复使用position值

Buffer四个重要成员属性

NIO Channel(通道)

NIO中一个连接就是用一个Channel(通道)来表示。更广泛的层面来说,一个通道可以表示一个底层的文件描述符。

JavaNIO的通道还可以更加细化。例如,对应不同的网络传输协议类型,在Java中都有不同的NIO Channel(通道)实现。

四种重要的Channel类型,分别如下:

FileChannel

文件通道,用于文件的数据读写

SocketChannel

套接字通道,用于Socket套接字TCP连接的数据读写

ServerSocketChannel

服务器嵌套字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道

DatagramChannel

数据报通道,用于UDP协议的数据读写

通过简单的FileChannel示例加深印象,其他的Channel可以自行举一反三,创建ChannelTest.java

package com.zhxin.nettylab.nio.chapter2;

import java.io.*;

import java.nio.CharBuffer;

import java.nio.MappedByteBuffer;

import java.nio.channels.FileChannel;

import java.nio.charset.Charset;

import java.nio.charset.CharsetDecoder;

/**

* @ClassName BufferTest

* @Description //BufferTest FileChannel文件通道 demo

* @Author singleZhang

* @Email 405780096@qq.com

* @Date 2020/12/3 0003 下午 2:43

**/

public class ChannelTest {

public static void main(String[] args){

File bt = new File("E:/project/nettylab/src/main/resources/buffer.txt");

//当try语句块运行结束时,FileInputStream 会被自动关闭

// 这是因为FileInputStream 实现了java中的java.lang.AutoCloseable接口

// 所有实现了这个接口的类都可以在try-with-resources结构中使用

// 以FileInputStream、FileOutputStream 文件输入流和文件输出流来创建FileChannel

try(FileChannel inCnl = new FileInputStream(bt).getChannel();

FileChannel outCnl = new FileOutputStream("E:/project/nettylab/src/main/resources/buffer1.txt").getChannel()){

MappedByteBuffer bf = inCnl.map(FileChannel.MapMode.READ_ONLY,0,bt.length()); //从Channel获取数据

Charset crt = Charset.forName("UTF-8");

outCnl.write(bf); //向Channel写数据

bf.clear();

CharsetDecoder cd = crt.newDecoder();

//decode 把ByteBuffer 转 CharBuffer

CharBuffer cb = cd.decode(bf);

System.out.println(cb);

} catch (IOException e) {

e.printStackTrace();

}

}

}

NIO Selector(选择器)

选择器是NIO中非常重要的角色,选择器的使命是完成IO的多路复用。一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO(输入输出)状况。选择器和通道的关系,是监控和被监控的关系。

通道和选择器之间的关系,通过register(注册)的方式完成。

调用通道的Channel.register(Selector sel, int ops)方法,可以将通道实例注册到一个选择器中。

register方法有两个参数:

第一个参数,指定通道注册到的选择器实例;

第二个参数,指定选择器要监控的IO事件类型,它包括以下四种类型:SelectionKey.OP_READ(可读)、SelectionKey.OP_WRITE(可写)、SelectionKey.OP_CONNECT(连接)、SelectionKey.OP_ACCEPT(接收)

查看SelectionKey类源码,如下:

//SelectionKey.java 部分源码

public static final int OP_READ = 1 << 0;

public static final int OP_WRITE = 1 << 2;

public static final int OP_CONNECT = 1 << 3;

public static final int OP_ACCEPT = 1 << 4;

SelectionKey选择键

通道和选择器的监控关系注册成功后,就可以选择就绪事件。这些IO事件类型指的就是通道的某个IO操作的一种就绪状态,表示通道具备完成某个IO操作的条件。

例如,某个SocketChannel通道,完成了和服务端的握手连接,则处于“连接就绪”(OP_CONNECT)状态;

某个ServerSocketChannel服务器通道,监听到一个新连接的到来,则处于“接收就绪”(OP_ACCEPT)状态。

SelectableChannel类

※FileChannel文件通道就不能被选择器监控或选择,判断一个通道能否被选择器监控或选择,有一个前提:判断它是否继承了抽象类SelectableChannel(可选择通道)

Java NIO中所有网络链接Socket套接字通道,都继承了SelectableChannel类,都是可选择的。

选择器使用流程

使用选择器,主要有以下三步:

获取选择器实例;

将通道注册到选择器中;

轮询感兴趣的IO就绪事件(选择键集合)

通过示例加深一下印象,创建服务端demo SelectorTest.java

package com.zhxin.nettylab.nio.chapter3;

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;

/**

* @ClassName SelectorTest

* @Description //选择器 使用 服务器端示例

* @Author singleZhang

* @Email 405780096@qq.com

* @Date 2020/12/4 0004 上午 9:17

**/

public class SelectorTest {

private static Selector selector;

public static void main(String[] args){

try{

/*

* 获取选择器示例

* Selector选择器的类方法open()的内部,是向选择器SPI(SelectorProvider)发出请求,

* 通过默认的SelectorProvider(选择器提供者)对象,获取一个新的选择器实例。

* Java中SPI全称为(Service Provider Interface,服务提供者接口),是JDK的一种可以扩展的服务提供和发现机制。

* Java通过SPI的方式,提供选择器的默认实现版本

*/

selector = Selector.open();

/*

* 将通道注册到选择器实例

* */

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //打开ServerSocketChannel,获取通道

serverSocketChannel.configureBlocking(false); //设为非阻塞

serverSocketChannel.bind(new InetSocketAddress(8989)); //将该通道对于的serverSocket绑定到port端口

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//将通道注册到选择器上,监听"接收连接"事件

/*

* 选出感兴趣的IO就绪事件(选择键集合)

* 通过Selector选择器的select()方法,选出已经注册的、已经就绪的IO事件,保存到SelectionKey选择键集合中

* 遍历这些IO事件,进行对应的处理

*/

while (selector.select() > 0){

Set selectKeys = selector.selectedKeys();

Iterator keyIterator = selectKeys.iterator();

while(keyIterator.hasNext()){

SelectionKey key = keyIterator.next();

if(key.isAcceptable()){

// ServerSocketChannel服务器监听通道有新连接

handleAccept(key);

} else if(key.isReadable()){

// 传输通道可读

handleRead(key);

} else if(key.isWritable()){

//传输通道可读

handleWrite(key);

}

//移除处理完的选择键

keyIterator.remove();

}

}

}catch (Exception e){

e.printStackTrace();

}

}

/**

* 处理客户端新连接事件

*/

private static void handleAccept(SelectionKey key) throws IOException {

// 获取客户端连接通道

ServerSocketChannel server = (ServerSocketChannel) key.channel();

SocketChannel socketChannel = server.accept();

socketChannel.configureBlocking(false);

// 信息通过通道发送给客户端

String msg = "Hello Client!";

socketChannel.write(ByteBuffer.wrap(msg.getBytes()));

// 给通道设置读事件,客户端监听到读事件后,进行读取操作

socketChannel.register(selector, SelectionKey.OP_READ);

}

/**

* 监听到可读,处理客户端发送过来的信息

*/

private static void handleRead(SelectionKey key) throws IOException {

SocketChannel channel = (SocketChannel) key.channel();

// 从通道读取数据到缓冲区

ByteBuffer buffer = ByteBuffer.allocate(128);

channel.read(buffer);

// 输出客户端发送过来的消息

byte[] data = buffer.array();

String msg = new String(data).trim();

System.out.println("server received msg from client:" + msg);

}

private static void handleWrite(SelectionKey key){

}

}

创建客户端demo ClientTest.java

package com.zhxin.nettylab.nio.chapter3;

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.SocketChannel;

import java.util.Iterator;

import java.util.Set;

/**

* @ClassName ClientTest

* @Description //客户端

* @Author singleZhang

* @Email 405780096@qq.com

* @Date 2020/12/4 0004 上午 9:52

**/

public class ClientTest {

private static Selector selector;

public static void main(String[] args) throws IOException {

// 创建通道管理器(Selector)

selector = Selector.open();

// 创建通道SocketChannel

SocketChannel channel = SocketChannel.open();

// 将通道设置为非阻塞

channel.configureBlocking(false);

// 客户端连接服务器,其实方法执行并没有实现连接,需要在handleConnect方法中调channel.finishConnect()才能完成连接

channel.connect(new InetSocketAddress("localhost", 8989));

/**

* 将通道(Channel)注册到通道管理器(Selector),并为该通道注册selectionKey.OP_CONNECT

* 注册该事件后,当事件到达的时候,selector.select()会返回,

* 如果事件没有到达selector.select()会一直阻塞。

*/

channel.register(selector, SelectionKey.OP_CONNECT);

while (selector.select() > 0) {

Set selectKeys = selector.selectedKeys();

Iterator keyIterator = selectKeys.iterator();

while(keyIterator.hasNext()){

SelectionKey key = keyIterator.next();

if(key.isConnectable()){

// 传输通道连接成功 一般用在客户端

handleConnect(key);

} else if(key.isReadable()){

// 传输通道可读

handleRead(key);

} else if(key.isWritable()){

//传输通道可读

handleWrite(key);

}

//移除处理完的选择键

keyIterator.remove();

}

}

}

/**

* 处理 和服务器端连接成功事件

* */

private static void handleConnect(SelectionKey key) throws IOException {

// 获取与服务端建立连接的通道

SocketChannel channel = (SocketChannel) key.channel();

if (channel.isConnectionPending()) {

// channel.finishConnect()才能完成连接

channel.finishConnect();

}

channel.configureBlocking(false);

// 数据写入通道

String msg = "Hello Server!";

channel.write(ByteBuffer.wrap(msg.getBytes()));

// 通道注册到选择器,并且这个通道只对读事件感兴趣

channel.register(selector, SelectionKey.OP_READ);

}

/**

* 监听到可读,处理服务端发送过来的信息

*/

private static void handleRead(SelectionKey key) throws IOException {

SocketChannel channel = (SocketChannel) key.channel();

// 从通道读取数据到缓冲区

ByteBuffer buffer = ByteBuffer.allocate(128);

channel.read(buffer);

// 输出服务端响应发送过来的消息

byte[] data = buffer.array();

String msg = new String(data).trim();

System.out.println("client received msg from server:" + msg);

}

private static void handleWrite(SelectionKey key){

}

}

总结

到这里已经算是踏入了JAVA NIO的大门了,以上都是比较简单的demo实践,没有看到“粘包”和“拆包”等复杂问题,后续会接触到。

Java NIO编程大致的特点如下:

在NIO中,服务器接收新连接的工作,是异步进行的。不像Java的OIO那样,服务器监听连接,是同步的、阻塞的。NIO可以通过选择器(也可以说成:多路复用器),后续不断地轮询选择器的选择键集合,选择新到来的连接。

在NIO中,SocketChannel传输通道的读写操作都是异步的。如果没有可读写的数据,负责IO通信的线程不会同步等待。这样,线程就可以处理其他连接的通道;不需要像OIO那样,线程一直阻塞,等待所负责的连接可用为止。

在NIO中,一个选择器线程可以同时处理成千上万个客户端连接,性能不会随着客户端的增加而线性下降。

java nio 客户端_Java网络编程:Netty框架学习(二)---Java NIO,实现简单的服务端客户端消息传输...相关推荐

  1. Netty 框架学习(二):Netty粘包和拆包

    文章目录 一.什么是粘包和拆包 二.粘包和拆包示例代码 1.TimeServerHandler 2.TimeClientHandler 三.使用Netty解决粘包和拆包 1.TimeServerHan ...

  2. 网络云盘项目——HTTP接口介绍、功能介绍、服务端/客户端代码解析

    一.本文目的 本项目分为6篇博客文章完成: 1.项目总体介绍:https://blog.csdn.net/qq_41453285/article/details/107871393. 2.Redis部 ...

  3. java nio 海子_java 网络编程入门-NIO

    这篇来讲解java网络编程之后高性能模型.NIO,有些书成为Non-blocking IO 非阻塞IO,这个是相对于BIO来说的.还有一种说法,New IO,顾名思义新的IO,这个是相对于旧版io的定 ...

  4. Java网络编程(一)- 一个简单的服务端/客户端应用程序

    在Java中,我们使用java.net.Socket及其相关类来完成有关网络的相关功能.Socket类非常简单易用,因为Java技术隐藏了建立网络连接和通过连接发送数据的复杂过程.下面所说的内容只适用 ...

  5. 连接主机Java网络编程(一)- 一个简单的服务端/客户端应用程序

    新手发帖,很多方面都是刚入门,有错误的地方请大家见谅,迎欢批评指正 在Java中,我们应用java.net.Socket及其关相类来成完有关络网的关相功能.Socket类非常简略易用,因为Java术技 ...

  6. Java 高并发_JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程 没有项目经验的朋友不要错过!...

    JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程 没有项目经验的朋友不要错过! 1.JPG (37.82 KB, 下载次数: 0) 2018-12-3 09:40 上传 2.JPG (28 ...

  7. [C# 网络编程系列]专题十二:实现一个简单的FTP服务器

    引言: 休息一个国庆节后好久没有更新文章了,主要是刚开始休息完心态还没有调整过来的, 现在差不多进入状态了, 所以继续和大家分享下网络编程的知识,在本专题中将和大家分享如何自己实现一个简单的FTP服务 ...

  8. AgileEAS.NET SOA 中间件平台.Net Socket通信框架-简单例子-实现简单的服务端客户端消息应答...

    一.AgileEAS.NET SOA中间件Socket/Tcp框架介绍 在文章AgileEAS.NET SOA 中间件平台Socket/Tcp通信框架介绍一文之中我们对AgileEAS.NET SOA ...

  9. Netty 框架学习(二):DelimiterBasedFrameDecoder和FixedLengthFrameDecoder

    文章目录 一.DelimiterBasedFrameDecoder 服务端 1.EchoServer 2.EchoServerChannelHandler 3.EchoServerHandler 客户 ...

最新文章

  1. 【Python3_基础系列_006】Python3-set-集合
  2. 使用Dockerfile构建镜像
  3. 灵魂拷问:后端业务开发要会用 K8s 到什么程度?
  4. python flask服务_在python中Flask配置服务
  5. Python基础——PyCharm版本——第七章、面向对象编程
  6. 数据库面试题【二、MYSQL的两种存储引擎区别(事务、锁级别等等)】
  7. 一个包含嵌套递归数据结构的对象的排序实现
  8. VS2015无法打开包括文件corecrt.h 无法打开文件ucrtd.lib
  9. js 下拉框选中跳转
  10. AS技巧合集「调试技巧篇」
  11. 虚拟机android原生系统下载地址,Android x86开源项目最新版虚拟机镜像下载VM/Vbox...
  12. 电脑网易云音乐,我放弃了QQ音乐和网易云音乐,最终还是选择了Apple Music
  13. 一键自动下载百度美女图片
  14. JAVA基础之单例模式
  15. Win10电脑开机之后屏幕一直闪动解决方法
  16. NR基础篇上——均值滤波、高斯滤波、双边滤波、NLM
  17. 怎么更新opengl.dll文件_微信又更新了:群接龙怎么玩?文件如何备份?怎么发高清大视频?...
  18. 文章列表的显示 以及创建文章 还有文章详情的基本--react项目5
  19. 淘宝推广方法大全,教你如何做淘宝(转)
  20. html网页和cgi程序编程,十分钟搞懂什么是CGI

热门文章

  1. jest.conf.js_如何在Jest中正确模拟Moment.js / dates
  2. 如果成为一名高级安卓开发_想成为一名开发人员? 这是您需要考虑的。
  3. javascript功能_最新版本JavaScript仅具有2个新功能。 这是他们的工作方式。
  4. Java 统计字母个数
  5. mie散射理论方程_电磁散射工程应用基础
  6. idea怎么把代码放到git_在IntelliJ IDEA中同步代码到华为云git仓库 (on windows)
  7. Lecture_2_4 线性回归中的系数,衡量了什么?
  8. Pytorch入门(6)-图片风格迁移和GAN
  9. 在idea上配置hadoop开发环境,使用maven配置hadoop的依赖,自动管理相关jar包
  10. 案例实现jquery.cookie的操作