nio相关

写入和读出的问题

任何情况下,对一个区域来说都是写入,读出。IO流的read是流的方法,对流来说是读出,而不是对内存来说是读入。同理,bytebuffer也是,调用的channel的read方法对channel来说是读出,而不是针对bytebuffer是读入,对bytebuffer来说是写入。

 

注意:BIO,NIO,AIO都是网络编程,不是文件编程

 

NIO可以工作在阻塞和非阻塞模式下

 

NIO三大组件

(Channel):channel 有一点类似于 stream,它就是读写数据的**双向通道**,**可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel。而之前的 stream 要么是输入,要么是输出。

(Buffer):用余连接channel的缓冲区,用来存储发送数据和存储接收数据。

selectorselector 的作用就是配合一个线程来管理多个 channelfileChannel因为是阻塞式的,所以无法使用selector),获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,当一个channel中没有执行任务时,可以去执行其他channel中的任务。适合连接数多,但流量较少的场景

 

 

 

ByteBuffer

ByteBuffer和channel的基本读写流程

使用方式

  • 向 buffer 写入数据,例如调用 filechannel.read(buffer),Filechannel的read方法类似BIO中的read方法(只不过IO的buffer是我们在内存new的,这里的buffer是系统提供的),判断-1,把buffer写满。
  • 调用 flip() 切换至读模式
    • flip会使得buffer中的limit变为position,position变为0
  • 从 buffer 读取数据,例如调用 buffer.get(),把buffer都读出来
  • 调用 clear() 或者compact()切换至写模式
    • 调用clear()方法时position=0,limit变为capacity
    • 调用compact()方法时,会将缓冲区中的未读数据压缩到缓冲区前面
  • 重复以上步骤

 

ByteBuffer内部结构

capacity

position

limit

 

开始

写入,写完后position+1

flip,position = 0 , limit 指向最后一个元素的下一个位置,capacity不变

 

读出,position会+1

 

clear,position = 0, capacity = limit

 

compact

compact会把未读完的数据向前压缩,然后切换到写模式,数据前移后,原位置的值并未清零,写时会覆盖之前的值,compact方法position不在首位。

 

 

byteBuffer的方法

创建allocate

 

 buffer读入数据

调用 channel 的 read 方法

调用 buffer 自己的 put 方法

 buffer 写出数据

调用 channel 的 write 方法

调用 buffer 自己的 get 方法,读取缓冲区中的一个值

重复读取

调用 rewind 方法将 position 重新置为 0

调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针

调用mark 和 reset:

mark 是在读取时,在当前position做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置

clear

如果buffer内还有数据,他们处于“被遗忘”状态,下次进行写操作时会覆盖这些数据

 

 

Channel

channel在NIO中相当于BIO中的Stream,有read,write,close等方法。

 

filechannel

filechannel用于文件和内存之间的传输,本地传输,属于NIO的内容,但不能和selector一起用,因为只能非阻塞模式的channel才能配和selector

创建

必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  • 通过 FileInputStream 获取的 channel 只能读出
  • 通过 FileOutputStream 获取的 channel 只能写入
  • 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定

 

读取

通过read方法将数据写入到ByteBuffer中,Filechannelread方法类似BIO中的read方法(只不过IObuffer是我们在内存new的,这里的buffer是系统提供的)。

read方法的返回值表示读到了多少字节,若读到了文件末尾则返回-1

 

写入

因为channel也是有大小的,所以 write 方法并不能保证一次将 buffer 中的内容全部写入 channel。必须需要按照以下规则进行写入

关闭

通道需要close,一般情况通过try-fanally进行关闭

大小

使用 size 方法获取文件的大小

channel之间的数据传输(只有FileChannel

transferTo/transferFrom方法:一次只能传输2G的内容,底层使用了sendFile零拷贝技术

 

SocketChannelServerSocketChannel

SocketChannel相当于BIO中的socketServerSocketChannel相当于BIO中的ServerSocket,只不过BIO中得获得流来read/writeNIOSocketChannel/ServerSocketChannel直接就能read/write

 

创建

open方法

读取

read方法,

read方法的返回值表示读到了多少字节,若读到了文件末尾则返回-1,如果是非阻塞模式返回0

写入

write方法

注意:限于网络传输能力,SocketChannel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件

关闭

close

大小

size

accept方法:ServerSocketChannel

connect方法:SocketChannel

configureBlocking方法:ServerSocketChannel和SocketChannel都有,非阻塞模式

 

NIO在不使用selector时工作模式

NIO阻塞模式

NIO在阻塞模式下其阻塞原理和BIO socket网络编程很像,

阻塞模式下,相关方法都会导致线程陷入阻塞:

  • ServerSocketChannel.accept 会在没有连接建立时让服务器线程一直等待
  • SocketChannel.read 会在通道中没有数据可读时让服务器线程一直等待当前客户端,而无法处理其他客户端的请求,因为read阻塞无法执行accept
  • write也会阻塞,参考阻塞IO模型

 

单线程下,多个客户端之间相互影响,几乎不能正常工作,需要多线程支持,

但多线程下,有新的问题,体现在以下方面

132 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致内存占用过大,并且线程太多,反而会因为频繁上下文切换导致性能降低

2可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多客户端连接,而线程池线程不够会导致长时间未响应,因此不适合长连接,只适合短连接

 

NIO在非阻塞模式下

服务器使用ServerSocketChannel和SocketChannel调用configureBlocking(false)开启非阻塞模式,此时accept方法不会一直等待客户端连接,如果没有客户端连接,返回socketchannelnull,继续往下运行。read方法也不会一直等待客户端发送数据,如果没有数据,执行到read时返回0,继续往下运行。

 

缺点CPU一直在运行,耗费CPU资源。

 

Selector

创建:open方法

获取发生的事件:select方法,注意select方法在没有获取到所有channel的事件的情况下才让该线程等待,陷入阻塞,而非只针对某个channel。但是如果事件不处理那么select方法还是能获取到即还是不会陷入阻塞,要么处理要么取消key.cancel

 

单线程多路复用(这里的非阻塞实际上是半阻塞,select方法)

单线程多路复用

服务器中单线程可以配合 Selector 完成对多个 Channel 读写连接事件的监控,这称之为多路复用,能够保证

有可连接事件时才去连接

有可读事件才去读取

有可写事件才去写入

什么事件都没有才进入阻塞等待

之前阻塞模型中单线程只能一直等待客户端连接,做不了其他事,一旦连接上,又必须一直等待当前客户端发送信息,也做不了其他事。多路复用保证没有客户端来连接,可以处理其他连接上的客户端的读写数据,没有其他客户端来读写数据,可以保证能让新来的客户端连接上

注意:NIO不完全等于多路复用,NIO提供了多路复用的必要条件非阻塞。除了多路复用,NIO还可以实现普通阻塞和普通非阻塞

 

创建

Selector selector = Selector.open();

设置非阻塞

channel.configureBlocking(false);只有非阻塞模式的channel才能注册selector

注册 Channel

SelectionKey key = channel.register(selector, 绑定事件);

SelectionKey相当于channel的管理者。

绑定的事件类型可以有

connect - 客户端连接成功时触发

accept - 服务器端成功接受连接时触发

read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况

write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

监听 Channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

方法1,阻塞直到绑定事件发生int count = selector.select();

方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)int count = selector.select(long timeout);

方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件int count = selector.selectNow();

获取 Channel 事件

selector.selectedKeys,返回发生事件的selectionKey的集合

遍历 Channel事件

遍历集合得到每个发生事件的SelectionKey

判断并处理事件

判断这个key发生了哪种事件,这个key反向获取到对应的channel,进而执行acceptreadwrite等方法。

移除selectionkey

itorator.remove()

 

处理accept事件

服务器端创建selector设置非阻塞注册serversocketchannel绑定ACCEPT事件select监听事件,获取key集合,遍历,判断是否是ACCEPT事件并反向获取channel其实serversocketchannel只有一个

 

 

处理read事件

 

注意:如果在服务器处理read事件中,客户端断开连接,要保证把事件取消cancel

 

 

NIO多线程多路复用单个worker

分两组选择器

单线程Boss配一个selector,专门处理 accept 事件

创建 cpu 核心数的worker线程,每个线程配一个selector,轮流处理 read 事件

 

产生一个问题:

产生一个问题:workerselect监控socketchannel注册到worker两个步骤在两个不同的线程中,如果单纯移动代码前后不能完全保证按照先注册再监控的顺序。如果顺序不对,先监控,后面无法注册,导致select一直处于阻塞中接收不到数据。

 

问题的本质上是要按照selector的创建,设置,注册,监控等顺序来,不能先监控再注册,这样无法注册成功

 

解决办法

1、使用队列

 

 

2、在worker类中这么做,上面两句代码在一个线程,select在一个线程,顺序会有三种情况

①先wakeup,再register,在select

②先wakeup,再select,再register

③先select,再wakeup,再register

三种方法都能保证要么register→select在第一轮循环中就成功,要么register在第一轮成功→select在第二轮循环中成功,因为select阻塞后wakeup,进入第二轮循环。

 

 

NIO多线程多路复用多个worker

多路复用+负载均衡,保证多个客户端连接的时候轮流使用selector

 

 

零拷贝

所谓的【零拷贝】,并不是真正无拷贝,而是不会拷贝重复数据到 jvm 用户缓冲区中,第三种第四种都叫零拷贝

DMA:直接内存拷贝,不使用CPU拷贝

 

 

传统BIO读写数据

 

  1. java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 cpu
    DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO
  2. 内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA
  3. 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝
  4. 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的

  • 用户态与内核态的切换发生了 4,这个操作比较重量级
  • 数据拷贝了共 4 次

 

NIO的优化mmap内存映射

  • 采用mmap内存映射映射到用户缓冲区
  • 使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲(user buffer)的过程
  • 用户态与内核态的切换发生了 4
  • 数据拷贝了共3 次

 

sendFile优化

底层过程如下,java 代码中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据。

  • 用户态与内核态的切换发生了 2
  • 数据拷贝了共3 次

 

 

sendFile最终优化

  • 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗。
  • 用户态与内核态的切换发生了 2
  • 数据拷贝了共2 次
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值