前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java网络编程——NIO三大组件Buffer、Channel、Selector

Java网络编程——NIO三大组件Buffer、Channel、Selector

作者头像
DannyHoo
发布2022-08-04 18:33:20
2780
发布2022-08-04 18:33:20
举报
文章被收录于专栏:Danny的专栏Danny的专栏

Java NIO(Java Non-Blocking IO)也就是非阻塞IO,说是非阻塞IO,其实NIO也支持阻塞IO模型(默认就是),相对于BIO来说,NIO最大的特点是支持IO多路复用模式,可以通过一个线程监控多个IO流(Socket)的状态,来同时管理多个客户端,极大提高了服务器的吞吐能力。

在NIO中有3个比较重要的组件:Buffer、Channel、Selector

Buffer

Buffer顾名思义,缓冲区,类似于List、Set、Map,实际上它就是一个容器对象,对数组进行了封装,用数组来缓存数据,还定义了一些操作数组的API,如 put()、get()、flip()、compact()、mark() 等。在NIO中,无论读还是写,数据都必须经过Buffer缓冲区,如下图:

随便创建一个Buffer,再put两个字节:

代码语言:javascript
复制
ByteBuffer byteBuffer = ByteBuffer.allocate(12);
byteBuffer.put((byte)'a');
byteBuffer.put((byte)'b'); 

发现这两个字节是被存到Buffer中一个叫hb的数组中了:

Buffer是所有缓存类的父类,对应实现有ByteBuffer、CharBuffer、IntBuffer、LongBuffer等跟ava基本数据类型对应的几个实现类:

一般最长用的就是ByteBuffer,创建 ByteBuffer 有两种方式:HeapByteBuffer 和 DirectByteBuffer:

(1)HeapByteBuffer:占用JVM堆内内存,不用考虑垃圾回收,属于用户空间,相对于DirectByteBuffer来说拷贝数据效率较低,会受到Full GC影响(Full GC后,可能需要移动数据位置)。创建HeapByteBuffer的方式为:

代码语言:javascript
复制
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

(2)DirectByteBuffer:占用堆外内存,读写效率高(读数据可以减少一次数据的复制),初次分配效率较低(需要调用系统函数),不受JVM GC的影响,但使用时要注意垃圾回收,使用不当可能造成内存泄漏。创建DirectByteBuffer的方式为:

代码语言:javascript
复制
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); 

为了可以更灵活地读/写数据,Buffer中有几个比较重要的属性:

● 容量(capacity):即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变 ● 位置(position):当前读/写到哪个位置,下一次读/写就会从下一个位置开始,每次读写缓冲区数据时都会改变(累加),为下次读写作准备 ● 上限(limit):表示缓冲区的临时读/写上限,不能对缓冲区超过上限的位置进行读写操作,上限是可以修改的(flip函数)。读的时候读的是从position到limit之间的数据,写的时候也是从position位置开始写到limit位置。 ● 标记(mark):调用mark函数可以记录当前position的值(mark = position),以后再调用reset()可以让position重新恢复到之前标记的位置(position = mark)

用个例子来看下这几个属性在读/写数据过程中的变化

代码语言:javascript
复制
public class BufferTest {
    public static void main(String[] args) throws IOException {
        // 1、初始化Buffer,先初始化一个长度为12的ByteBuffer,也就是创建了类型为byte一个长度为12的数组:
        ByteBuffer byteBuffer = ByteBuffer.allocate(12);
        System.out.println("【初始化ByteBuffer】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());

        // 2、写数据,写的过程中,每写入一个字节,position自增1,当写入8个字节数据后,position=8
        for (int i = 0; i < 8; i++) {
            byteBuffer.put((byte) i);
        }
        System.out.println("【ByteBuffer写完数据】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());

        // 3、将Buffer由写模式转化为读模式
        byteBuffer.flip();
        System.out.println("【ByteBuffer调flip】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());

        // 4、读数据,如果依次读取了6个字节,那现在position就指向下标为6的位置,limit不变
        for (int i = 0; i < 6; i++) {
            if (i == 3) {
                byteBuffer.mark();
            }
            byte b = byteBuffer.get();
        }
        System.out.println("【ByteBuffer读完数据】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());

        // 5、重置position
        byteBuffer.reset();
        System.out.println("【ByteBuffer调reset】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());
    }
}

① 初始化Buffer,先初始化一个长度为12的ByteBuffer,也就是创建了类型为byte一个长度为12的数组:

② 写数据,写的过程中,每写入一个字节,position自增1,当写入6个字节数据后,position=6,如下图:

③ 写数据转读数据,现在Buffer中一共有8个字节的数据。因为对Buffer的读/写,都是从position位置到limit位置进行读/写的。如果现在想读取Buffer中的数据,需要执行一下Buffer的flip()函数,把limit置为8(position的值),position重新置为0,这时候position到limit之间的数据才是有效的(我们想要读取的)数据。所以通常将Buffer由写模式转化为读模式时需要执行flip()函数:

④ 读数据,如果依次读取了6个字节,那现在position就指向下标为6的位置,limit不变:

⑤ 重置position,前面在position=3的时候,调用byteBuffer.mark();标记了一下当时position的值(mark=3),当读取完6个字节后,position=6。这时调用一下byteBuffer.reset()可以把position重置为当时mark的值(position=mark),也就是3:

ByteBuffer中常用的的方法还有很多:

代码语言:javascript
复制
byteBuffer.put((byte) 'a'); //在position位置存入字符a对应的字节
byteBuffer.put(1, (byte) 5); //在1位置存入数字5对应的字节
byteBuffer.get();// 从position的位置读取一个字节的数据,读完后会导致position加1
byteBuffer.get(i);// 从position=i的位置读取一个字节的数据,读完后不会导致position加1
byteBuffer.reset(); // 重置position的值为mark
byteBuffer.position(5); // 重置position的值为5
byteBuffer.flip(); // 写完数据后,切换到读模式,把limit置为position的值,position置为0,
byteBuffer.clear(); // 清空ByteBuffer,position=0,limit=capacity
byteBuffer.compact(); // 读了一部分数据后,切换到写模式,会把未读的数据向前压缩,只留下有效数据(一般认为position~limit之间的数据为有效数据),比如原来pos=2,limit=8,capacity=12,执行compact()后,pos=6,limit=12,capacity=12

这里不再一一详细介绍。

Channel

Channel是对原 I/O 包中的流的模拟,到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象,通道是双向的(一个Channel既可以读数据,也可以写数据),BIO中的InputStream/OutputStream是单向的(InputStream/OutputStream只能读/写数据)。

Channel 有文件通道和网络通道,文件通道的实现主要是FIleChannel,网络通道的实现主要有ServerSocketChannel(主要用于服务器接收客户端请求,类似于BIO中的ServerSocket)、SocketChannel(主要用户服务器和客户端直接的数据读写,类似于BIO中的Socket)、DatagramChannel(用于基于UDP协议的数据读写)。

FileChannel可以对文件进行读写,下面是个简单的例子:

代码语言:javascript
复制
public class FileChannelTest {
    public static void main(String[] args) throws IOException {
        FileChannel fileChannel = FileChannel.open(Paths.get("/Users/danny/data/file/file.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);
        // 通过FileChannel从文件中读数据
        ByteBuffer readBuffer=ByteBuffer.allocate(10);
        while (fileChannel.read(readBuffer) != -1){
            while (readBuffer.hasRemaining()){
                byte b=readBuffer.get();
            }
        }
        // 通过FileChannel向文件中写数据
        ByteBuffer writeBuffer=ByteBuffer.allocate(10);
        writeBuffer.put("Data".getBytes());
        writeBuffer.flip();
        while (writeBuffer.hasRemaining()){
            fileChannel.write(writeBuffer);
        }
    }
}

ServerSocketChannel 主要用于服务器接收客户端请求,SocketChannel 主要用户服务器和客户端直接的数据读写,跟BIO中ServerSocket和Socket通信差不多: 服务端:

代码语言:javascript
复制
public class ServerSocketChannelTest {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(true); // 设置阻塞模式为阻塞,默认就是true
        serverSocketChannel.bind(new InetSocketAddress(8080));
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        SocketChannel socketChannel = serverSocketChannel.accept(); // 如果没有接收到新的客户端连接,这里会阻塞
        System.out.println("收到到客户端连接");
        int length = socketChannel.read(byteBuffer); // 如果读不到数据,这里会阻塞,无法处理其他Channel的读操作和连接请求
        System.out.println("读取到客户端数据:" + new String(byteBuffer.array(), 0, length));
    }
}

客户端:

代码语言:javascript
复制
public class SocketChannelTest {
    public static void main(String[] args) throws IOException {
    	Socket socket = new Socket();
    	socket.connect(new InetSocketAddress("127.0.0.1", 8080));
    	System.out.println("连接服务端完成");
    	socket.getOutputStream().write(Constant.MESSAGE_128B.getBytes());
    	System.out.println("向服务端发送数据完成");
    	socket.close();
	}
}

Selector

选择器Selector相当于管家,管理所有的IO事件,通过Selector可以使一个线程管理多个Channel(也就是多个网络连接),当一个或多个注册到Selector上的Channel发生可读/可写事件时,Selector能够感知到并返回这些事件。

一个Channel可以注册到多个不同的Selector上,多个Channel也可以注册到同一个Selector上。当某个Channel注册到Selector上时,会包装一个SelectionKey(包含一对一的Selector和Channel)放到该Selector中,这些后面看源码的时候再仔细画图分析。

根据理解画了一张Selector在整个服务端和客户端交互中的作用的图,大致如下:

Selector可以作为一个观察者,可以把已知的Channel(无论是服务端用来监听客户端连接的ServerSocketChannel,还是服务端和客户端用来读写数据的SocketChannel)及其感兴趣的事件(READ、WRITE、CONNECT、ACCEPT)包装成一个SelectionKey,注册到Selector上,Selector就会监听这些Channel注册的事件(监听的时候如果没有事件就绪,Selector所在线程会被阻塞),一旦有事件就绪,就会返回这些事件的列表,继而服务端线程可以依次处理这些事件。

NIO使用了Selector,IO模型就是属于IO多路复用(同步非阻塞),可以同事检测多个IO事件,即使某一个IO事件尚未就绪,可以处理其他就绪的IO事件。同步体现在在Selector监听IO事件(Selector.select()方法)时,如果没有就绪事件,就会等待,不能做其他事;非阻塞体现在当某一个IO事件尚未就绪时,可以处理其他就绪的IO事件,比如在上图中,如果客户端2一直不发送数据,服务端也可以正常处理其他客户端的请求,而在BIO中(单线程环境),如果某个客户端连接到了服务端而迟迟不写数据,那么服务器端就会一直等待而无法及时接收其他客户端的请求。正是因为Selector,才可以让NIO在单线程的环境就能处理多个网络连接,为高并发编程打下基础。


转载请注明出处——胡玉洋 《Java网络编程——NIO三大组件Buffer、Channel、Selector》

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-08-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Buffer
  • Channel
  • Selector
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档