Java NIO 编程
基本介绍
-
java NIO 全称 Java non-blocking IO ,是指JDK 提供的新API .从Jdk1.4开始,java 提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO) ,是同步非阻塞的
-
NIO相关类都被放在java.nio包及子包下,并且对原java.io 包中的很多类进行改写
-
NIO有三大核心部分:Channel (通道) ,Buffer(缓冲区) ,Selector(选择器)
-
NIO是面向缓冲区,或者面向块编程的。数据读取到一个他稍后处理的缓冲区。需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
-
java NIO 的非阻塞模式,使一个线程从某个通道发送请求或者读取数据,但是他仅能得到目前可用的数据,如果目前没有数据可用时,就说明都不会获取,而不是保持线程阻塞,所以直到数据变的可以读取之前,改线程可以继续做其他的事情,非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待他完全写入,这个线程同时可以去做别的事情
-
通俗理解:NIO是可以做到一个线程处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO,非得分配10000个。
-
http2.0采用多路复用技术,做到同一个连接并发处理多个请求,而且并发请求的数量比http1.1大了好几个数量级
案例说明NIO的Buffer
public class Basic {
public static void main(String[] args) {
//给buffer当中放数据
IntBuffer intBuffer = IntBuffer.allocate(5);
for(int i = 0 ;i< intBuffer.capacity();i++){
intBuffer.put(i);
}
//从buffer当中拿数据
//buffer读写转换
intBuffer.flip();
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
NIO和BIO的比较
- BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O 的效率比流I/O高很多
- BIO 是阻塞的,NIO是非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基于Channel( 通道) 和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIO三大核心原理示意图
- 每个channel都会对应一个Buffer
- Selector对应一个线程,一个线程对应多个channel(连接)
- 程序切换到那个channel是有事件决定的,Event就是一个重要的概念
- Selector 会根据不用的事件,在各个通道上切换
- 数据的读取写入是通过Buffer,BIO当中的buffer要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip方法转换,channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的
缓冲区(Buffer)
基本介绍
缓冲区(Buffer):缓冲区本质上是一个可以读取数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer
Buffer类及其子类
-
在NIO中,Buffer是一个顶层父类,它是一个抽象类,类的层级关系:
- ByteBuffer,存储字节数据到缓冲区
- ShortBuffer,存储字符串数据到缓冲区
- CharBuffer,存储字符数据到缓冲区
- IntBuffer,存储长整型数据到缓冲区
- LongBuffer,存储小数到缓冲区
- DoubleBuffer,存储小数到缓冲区
- FloatBuffer,存储小数到缓冲区
-
Buffer类定义了所有缓冲区都具有的四个属性来提供关于其所包含的数据元素的消息:
-
Buffer类方法一览
-
ByteBuffer
java 当中的基本数据类型(boolean除外),都有一个Buffer类型与之相对应,最常用的自然是ByteBuffer类,该类的主要方法如下
通道(Channel)
基本介绍
-
NIO通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读写数据,也可以写数据到缓冲
-
BIO中的stream 是单向的,例如FileInputStream 对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。
-
Channel 在NIO中是一个接口
public interface Channel extends Closeable{}
-
常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。
ServerSocketChanne类似于ServerSocket,SocketChannel类似于Socket
-
FileChannel 用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChanel和SocketChannel 用于Tcp的数据读写。
FileChanel类
FileChannel主要用来对本地文件进行IO操作,常见的方法有
- public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中
- public int write(ByteBuffer src),把缓冲区的数据写到通道中
- public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
- public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
应用举例-本地文件写数据
使用ByteBuffer(缓冲)和FileChannel(通道),将"hello,world " 写入到file01.txt中
public class BasicFileWrite {
public static void main(String[] args) throws Exception{
String message="hello,world";
FileOutputStream fileOutputStream = new FileOutputStream(System.getProperty("user.dir") + "/file01.txt");
FileChannel channel = fileOutputStream.getChannel();
//设置ByteBuffer的初始容量
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将str放入缓冲区
byteBuffer.put(message.getBytes(StandardCharsets.UTF_8));
//对ByteBuffer,进行读写转换
byteBuffer.flip();
//将byteBuffer 写入fileChannel
channel.write(byteBuffer);
fileOutputStream.close();
}
}
应用举例-本地文件读数据
public class BasicRead {
public static void main(String[] args) throws Exception{
File file = new File(System.getProperty("user.dir") + "/file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//将通道中的数据读取到缓冲区
channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
}
}
应用举例-使用一个Buffer读写数据
public class ReadAndWrite {
public static void main(String[] args) throws Exception{
File file = new File(System.getProperty("user.dir") + "/file01.txt");
File outfile = new File(System.getProperty("user.dir") + "/file02.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream(outfile);
FileChannel outputStreamChannel = fileOutputStream.getChannel();
FileChannel channel = fileInputStream.getChannel();
//缓冲区大小不一定要比文件大,加入假如文件1000G,缓冲区不可能也为1000G,使用采用while逐步读取
ByteBuffer byteBuffer = ByteBuffer.allocate(5);
while(channel.read(byteBuffer)!=-1){
byteBuffer.flip();
outputStreamChannel.write(byteBuffer);
byteBuffer.flip();
}
fileOutputStream.close();
fileInputStream.close();
}
}
选择器Select
基本介绍
-
java 的NIO ,才用非阻塞的IO方式,可以用一个线程处理多个客户端的连接,就会使用到Selector(选择器)
-
Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Select),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求
-
只有在连接通道有读写事件发生时。才会进行读写,就大大减少了系统开销,并且不必每个连接都创建一个线程,不用去维护多个线程
-
避免了多线程之间的上下文切换导致的开销
特点说明
- Netty的IO线程NIOEventLoop聚合了Select(选择器,也叫多路复用器),可以同时并发处理成千上百个客户端连接。
- 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,改线程可以进行其他任务。
- 线程通常将非阻塞IO的空闲时间用于其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
- 由于读写操作都是非阻塞的,这就充分提升IO线程的运行效率,避免了由于频繁I/O阻塞导致的线程挂起。
- 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O 一连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升。
注意事项
- NIO 中的ServerSocketChanel 功能类似于ServerSocket,SocketChanel功能类似Socket
- select 相关方法说明
- selector.select()//阻塞
- selector.select(1000) //阻塞1000毫秒,在1000毫秒后返回
- selector.wakeup(); //唤醒 selector
- selector.selectNow();//不阻塞,立马返还
NIO非阻塞 网络编程原理分析图
对上图的说明:
- 当客户端连接时,会通过ServerSocketChannel 得到SocketChannel
- Selector 进行监听 select 方法,返回有事件发生的通道的个数
- 将socketChannel 注册到Selector上,一个selector 上可以注册多个SocketChannel
- 注册后返回一个SelectionKey,会和该Selector 关联
- 进一步得到各个SelectionKey(有事件发生)
- 在通过SelectionKey反向获取 SocketChannel,方法channel()
- 通过得到的Channel,完成业务处理
NIO 非阻塞 网络编程快速入门
Server端
public class NioServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个selector对象
Selector selector = Selector.open();
//绑定一个端口6666,在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把serverSocketChannel 注册到Selector中
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while(true){
//这里我们等待一秒,如果没有事件发生,返回
if(selector.select(1000)==0){
System.out.println("服务器等待了一秒,无连接");
continue;
}
//通过SelectionKey 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//使用迭代器遍历Set<SelectionKey>
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey selectionKey = keyIterator.next();
//根据key对应的通道进行处理
if(selectionKey.isAcceptable()){
//该客户端生成一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功得到一个socketChannel"+socketChannel.hashCode());
//将socketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(selectionKey.isReadable()){ //发生openRead
//通过key反向获取到对应channel
SocketChannel channel = (SocketChannel) selectionKey.channel();
//获取到channel关联的Buffer
ByteBuffer byteBuffer= (ByteBuffer)selectionKey.attachment();
channel.read(byteBuffer);
System.out.println("from 客户端"+new String(byteBuffer.array()));
}
//移除防止重复操作
keyIterator.remove();
}
}
}
}
Client端
public class NioClient {
public static void main(String[] args) throws Exception{
//得到一个网络通道
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 message= "hello world\n";
ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(byteBuffer);
System.in.read();
}
}