NIO基础——三大组件

三大核心组件

NIO的三个最重要的核心分别为:Channel,Buffer和Selector。

1.Channel(通道)

Channel就像是通道,是一个关于程序与操作系统底层I/O服务交互的通道。
比如:我们的程序对系统中某一个文件进行连接,以便于我们对它进行后续的操作。

常见的Channel有以下四种:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel
    FileChannel主要用于文件传输,其他三种用于网络通信。

2.Buffer(缓冲区)

当我们有了连接通道,我们需要将拿到的数据放到一个缓冲区域,以便于程序对它的读取/写入操作

常见的Buffer有以下几种:

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer
  • 其中最常用的是ByteBuffer

3.Selector(选择器)

	Selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。
	我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。

在selector技术之前,处理socket连接有以下两种方法。
多线程技术
系统为每一个连接分配一个thread(线程),分别去处理对应的socket连接。
多线程技术
这种方法的弊端:

  • 内存占用高。每有一个socket连接,系统就要分配一个线程去对接。当出现大量连接时,会开辟大量线程,导致占用大量内存。
  • 线程上下文切换成本高。
  • 只适合连接数较少的场景。

什么是线程上下文切换?
一个CPU在同一个时刻是只能处理一个线程的,由于时间片耗尽或出现阻塞等情况,CPU 会转去执行另外一个线程,这个叫做线程上下文切换。

线程池技术
使用线程池,让线程池中的线程去处理连接
线程池技术
这种方法的弊端:

  • 在阻塞模式下,线程只能处理一个连接。线程池中的线程获取任务,只有当任务完成/socket断开连接,才会去获取执行
    下一个任务
  • 只适合短链接的场景。

选择器(Selector)技术

为每个线程配合一个选择器,让选择器去管理多个channel。(注:FileChannel是阻塞式的,因此无法使用选择器。)
让选择器去管理多个工作在非阻塞式下的Channel,获取Channel上的事件,当一个Channel没有任务时,就转而去执行别的Channel上的任务。这种适合用在连接多,流量小的场景。

选择器技术
若事件未就绪,调用 selector 的 select() 方法会阻塞线程,直到 channel 发生了就绪事件。这些事件就绪后,select 方法就会返回这些事件交给 thread 来处理

二、聊聊ByteBuffer

1.ByteBuffer的重要属性

首先了解ByteBuffer的四个属性:

  • capacity:缓冲区的容量,不可变。(在netty中可变哦~)
  • limit:缓冲区的界限。limit之后的数据不允许读写
  • position:读写指针。position不可大于limit,且position不为负数。
  • mark:标记。记录当前position的值。position被改变后,可以通过调用reset() 方法恢复到mark的位置。

2.ByteBuffer的方法

2-1.allocate方法

allocate是ByteBuffer的一个静态方法。通过allocate我们可以给ByteBuffer分配空间,但是这个空间不可以动态变换,如果想要改变ByteBuffer的大小只能重新分配一个。

2-2.allocateDirect方法

allocateDirect也是ByteBuffer的一个静态方法。通过allocateDirect我们也可以给ByteBuffer分配空间。

allocate 与 allocateDirect的区别在哪?
可以通过代码来看,首先我们通过这两个静态方法创建两个对象。

 public static void main(String[] args) {
        System.out.println(ByteBuffer.allocate(10).getClass());
        //class java.nio.HeapByteBuffer

        System.out.println(ByteBuffer.allocateDirect(10).getClass());
        //class java.nio.DirectByteBuffer
    }

控制打印的结果是:allocate创建出来的是HeapByteBuffer对象,allocateDirect创建出来的是DirectByteBuffer对象。
那就聊一下HeapByteBuffer和DirectByteBuffer的区别:

  • HeapByteBuffer是存在于JVM的堆内存中,DirectByteBuffer是存在于直接(系统)内存中。
  • HeapByteBuffer的读写效率低于DirectByteBuffer,因为HeapByteBuffer存在于jvm中的,自然会收到垃圾回收器的影响。
  • DirectByteBuffer使用不当,容易造成内存泄露。

2-3.put方法

put方法可以将数据放入到缓冲区中。操作完成后,position的值会+1,并指向下一个可存放的区域,limit=capacity。
在这里插入图片描述

2-4.flip方法

flip方法会切换对当前缓冲区的去操作,写/读->读/写。

当是写模式切换到读模式时,先是limit=position,然后position=0。
当是读模式切换到写模式时,恢复为put时的值。

2-5.get方法

  • get方法会读取缓冲区里的数据,一次只能读取一个。
  • 读取后,position的值会+1,指向下一个可读区。当position大于limit时,会报异常。
  • get方法如果传入指定的索引位置:get(i)。则position的值不会产生变动。

2-6.clean方法

clean方法就像初始化一样,会把ByteBuffer的里属性值都恢复到最初,并且清除缓冲区里的数据。
在这里插入图片描述

2-7.compact

compact方法会把已经读取的数据清除,后面未读取的数据向前压缩,然后切换到写模式。
数据前移后,原始位置的数据不会清楚,但是在后面的写入操作中会被覆盖。
在这里插入图片描述

2-8.rewind方法

rewind方法只能在读模式下使用,使用后,会恢复position、limit和capacity的值

在这里插入图片描述

2-9.mark方法和reset方法

这个两个方法通常都是搭配着使用。
mark会保存当前position的值,reset方法会把mark保存的值重新赋给position。

3.字符串与ByteBuffer的相互转换

3-1.方法一

        // 方法一
        // 编码:字符串的getByte方法
        ByteBuffer buffer = ByteBuffer.allocate(15);
        buffer.put(str.getBytes());
        // 解码:先切换到读模式,然后通过StandardCharsets的decoder来解码
        buffer.flip();
        String decodeStr = StandardCharsets.UTF_8.decode(buffer).toString();

3-2.方法二

        // 方法二
        // 编码:StandardCharsets的encode方法获取ByteBuffer
        ByteBuffer buffer2 = StandardCharsets.UTF_8.encode(str);
        // 解码: 通过StandardCharsets的decoder方法解码
        String decodeStr2 = StandardCharsets.UTF_8.decode(buffer2).toString();

3-3.方法三

        // 方法三:
        ByteBuffer buffer3 = ByteBuffer.wrap(str.getBytes());
        // 解码: 通过StandardCharsets的decoder方法解码
        String decodeStr3 = StandardCharsets.UTF_8.decode(buffer3).toString();

4.黏包和半包

场景

将数据打包发送给服务端,每条数据以"\n"做分割。
比如现在有这三段话,我要一次性发给服务器

  • Hello world!\n

  • I’m LIKEGAKKI!\n

  • How are you?\n
    经过传输后,服务端的产生了两个ByteBuffer:

  • Hello,world\nI’m LIKEGAKKI\nHo(黏包)

  • w are you?\n?(半包)

原因
黏包

发送方在发送数据时,并不是一条一条地发送数据,而是将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去。

半包

因为我们分配缓冲区的大小是固定,如果空间小于数据量,那就只能先把当前缓冲区里的数据读取完,再去接收剩下的的数据。数据就会出现被截断的断层现象。

解决方法
  • 在循环中用get(i)方法依次读取数据,当读取的数据匹配‘\n’时,说明之前的读取的是一段信息。(get(i)方法不会修改position的值)
  • 记录该段数据长度,以便于申请对应大小的缓冲区
    将缓冲区的数据通过get()方法写入到target中。
  • 调用compact方法切换模式,因为缓冲区中可能还有未读的数据。

本篇文章根据在学习《黑马程序员Netty实战》时所做的笔记总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值