java NIO (1)

173 阅读5分钟

网络编程

互联网的基础是TCP/IP 协议, I/O 读写: 数据的流向 -网卡-内核缓存区-用户缓存区。数据是通过缓冲区进行交换的,优化i/o就是加快这个读写数据的过程

  • 阻塞 等待是否结束
  • 同步异步:调用请求是否立刻返回
  1. 同步阻塞:大部分I/O 请求调用都是同步阻塞的,一个应用调用以后就阻塞,并且等待应用的返回,直到返回以后才停止。
  2. 同步非阻塞: 调用以后没有阻塞,会把时间片让渡给别人,循环查询是否返回,返回以后再结束
  3. 异步阻塞: 一个请求调用以后就阻塞,然后监听应用的返回,直到应用回调以后结束
  4. 异步非阻塞 :调用以后把时间片也让渡给别人,不再阻塞,并且监听回调

java常见的4中I/O模型: 同步阻塞 BIO。同步非阻塞NIO ,I/O多路复用 I/O multiplexing(利用套接字 socket) 异步I/O ASynchronous I/O

经典服务器设计

  • 每个连接由一个线程处理(比如tomcat),不需要处理并发
  • 简化处理并发过程
  • Servlet 非线程安全 (spring 里面使用了threadLocal)而且连接都有一个线程处理,解决了这个问题
  • 连接一多资源占用非常多,所有请求都被阻塞住了

Reactor 模式简介

组成: 由Reactor线程和Handler处理器组成

  • Reactor线程 :负责响应I/O事件,并将事件分发给Handler处理器
  • Handler处理器:非阻塞的执行业务处理逻辑 Netty和nginx等等高性能的服务器都采取这种设计

优缺点:

优点:

  • 高性能的I/O处理
  • 编程相对比较简单,避免了复杂的多线程同步
  • 可以扩展,只要加机器就可以提高工作的性能 缺点
  • 依赖于操作系统I/O多路复用的支持
  • Handler不能被阻塞,会导致整个系统都被阻塞

Proactor 模式简介

异步网络模式

  • 感知的是已完成的读写事件
  • 发起异步读写请求时,需传入数据缓冲区 ,系统内核会自动完成数据读写
  • 和 Reactor 不同,需要应用进程主动发起 read/write 来读写数据
  • 需操作系统支持 AIO
  • Windows 使用 IOCP
  • Linux 下的异步 I/O 尚不完善

java NIO

流式stream都是面向socket编程的思路,封装了对不同操作系统的I/O处理 java NIO 是java 1.4 加入的非阻塞方式处理I/O,采用了缓冲区Buffer和通道Channel 多路复用了socket, 在jdk 1.7 引入了NIO2代

Netty

  • 基于java nio的技术体系,并且做了修复和改造
  • 异步和事件驱动
  • 保证高负载情况下的性能最优和可伸缩性
  • 将应用程序逻辑从网络层解耦
  • 提升可测试性、模块化、代码重用性 EchoServerHandler回调业务逻辑 EchoServer main启动服务器 创建ServerBootstrap实例引导服务 EchoBootstrapSever 服务器具体的执行逻辑

缓冲区操作

buffer 类

在BIO的环境下,开发者用字节数组管理读取I/O 信息

在NIO的环境下提供了完备的BUffer实现方法,一整套buffer的类 ByteBuff,CharBuffer。IntBuffer, wrap静态方法

缓冲区标识

0<=mark <=posititon <=limit〈=capcaity capcaity 容量 limit 限制 position 位置 mark 标记

缓冲区处理

写入数据以后 position指针就会自动的移动偏移,如果剩余空间(remaining)limit -position为0以后,就是写满了以后就抛异常。

mark是设置的标记,如果调用reset以后,就会把position重置到mark的地点

如果limit或者position被指向小于mark的位置时,mark将会被抛弃,并且跑出了无效标记异常

设置isreadonly只读,clear 只还原缓冲区状态,不清除数据,

直接缓冲区:

标题ByteBuffer
非直接缓冲区allocate(int capacity)
直接缓冲区allocateDirect(int capacity)
非直接缓冲区wrap(byte[] array)
非直接缓冲区wrap(byte[] array, int offset, int length)

allocate方式创建的ByteBuffer对象我们称之为非直接缓冲区,这个ByteBuffer对象(和对象包含的缓冲数组)都位于JVM的堆区。wrap方式和allocate方式创建的ByteBuffer没有本质区别,都创建的是非直接缓冲区。

allocateDirect方法创建的ByteBuffer我们称之为直接缓冲区,此时ByteBuffer对象本身在堆区,而缓冲数组位于非堆区, ByteBuffer对象内部存储了这个非堆缓冲数组的地址。在非堆区的缓冲数组可以通过JNI(内部还是系统调用)方式进行IO操作,JNI不受gc影响,机器码执行速度也比较快,同时还避免了JVM堆区与操作系统内核缓冲区的数据拷贝,所以IO速度比非直接缓冲区快。然而allocateDirect方式创建ByteBuffer对象花费的时间和回收该对象花费的时间比较多,所以这个方法适用于创建那些需要重复使用的缓冲区对象。

缓冲区反转

public final Buffer flip()
{ limit = position; 
position = 0; 
mark = -1; 
return this;
}

对于已经写满了缓冲区,如果将缓冲区内容传递给一个通道,以使内容能被全部写出。但如果通道现在在缓冲区上执行get(),那么它将从我们刚刚插入的有用数据之外取出未定义数据。如果我们通过翻转将位置值重新设为 0,通道就会从正确位置开始获取。

Flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态 例如我们定义了一个容量是10的buffer,并填入hello,如下图所示

屏幕快照 2022-02-14 上午10.47.40.png 翻转之后

屏幕快照 2022-02-14 上午10.48.06.png

Rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。可以使用 rewind()后退,重读已经被翻转的缓冲区中的数据。 翻转两次把上界设为位置的值,并把位置设为 0。上界和位置都变成 0,get()操作会导致 BufferUnderflowException 异常。而 put()则会导致 BufferOverflowException 异常。

Bytebuffer

缓冲区分片

在NIO中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切 出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是 现有缓冲区的一个视图窗口。调用slice()方法可以创建一个子缓冲区,让我们通过例子来看一下:

/**
 * 缓冲区分片
 */
public class BufferSlice {  
    static public void main( String args[] ) throws Exception {  
        ByteBuffer buffer = ByteBuffer.allocate( 10 );  
          
        // 缓冲区中的数据0-9  
        for (int i=0; i<buffer.capacity(); ++i) {  
            buffer.put( (byte)i );  
        }  
          
        // 创建子缓冲区  
        buffer.position( 3 );  
        buffer.limit( 7 );  
        ByteBuffer slice = buffer.slice();  
          
        // 改变子缓冲区的内容  
        for (int i=0; i<slice.capacity(); ++i) {  
            byte b = slice.get( i );  
            b *= 10;  
            slice.put( i, b );  
        }  
          
        buffer.position( 0 );  
        buffer.limit( buffer.capacity() );  
          
        while (buffer.remaining()>0) {  
            System.out.println( buffer.get() );  
        }  
    }  
}

在该示例中,分配了一个容量大小为10 的缓冲区,并在其中放入了数据0-9,而在该缓冲区基础之上又创建了一个子缓冲区,并 改变子缓冲区中的内容,从最后输出的结果来看,只有子缓冲区“可见的”那部分数据发生了变化,并且说明子缓冲区与原缓冲区是 数据共享的,输出结果如下所示: 0 1 2 30 40 50 60 7 8 9

其他操作

duplicate复制 compact 压缩缓冲区 压缩就是将已读取了的数据丢弃,保留未读取的数据并将保留的数据重新填充到缓冲区的顶部,然后继续向缓冲区写入数据

通道channel

  • 用来传输数据的通道
  • 表示数据到硬件设备,文件,网络的连接
  • channel接口和众多的继承接口
  • 实现了closeable接口,可以自动关闭 框架会对通道抽象成管道 tunnel

异步通道

AsynchronousChannel

  • 支持异步i/o操作的通道 两种异步操作 Future 异步方法 completionHandler

读取与写入通道

ReadableByteChannel:read 读取字节是同步的操作 如果一个Channel类实现了ReadableByteChannel接口,则表示其是可读的,可以调用read()方法读取;

如果一个Channel类实现了WritableByteChannel接口,则表示其是可写的,可以调用write()方法写入; 如果一个Channel类同时实现了ReadableByteChannel接口和WritableByteChannel接口则为双向通道,如果只实现其中一个,则为单向通道;

重要通道

filechannel 文件 SocketChannel socket tcp 数据读写 serverSocketChannel 服务器监听通道 DatagramChannel udp通道(效率更高,不需要三次握手)

高效访问

MappedByteBuffer map

锁定操作

FileLock lock(position,size, boolean shared)

  • 获取文件给定内容的锁定

  • 分为共享锁和独占锁

  • 共享锁自己和别人都不能写,自己和别人都可以读

  • 独占锁自己能读能写,但别人却不能读写

  • 可以重复获得共享锁,而独占锁之间、独占锁和共享锁之间都是互斥关系

  • tryLock 试图获得文件某个区域内容的锁定

  • 非阻塞方法(lock 为阻塞方法)