Java网络编程-NIO
网络编程实际上是进程间的通信。
1.IO
计算机处理数据的基本单位是字节。如果我们想要表示一个字符,比如char类型的,就需要使用2个字节表示,或者汉字,在utf8编码中需要3个字节表示。为了让计算机能直接处理字符,io流中就提供了字符流,即数据源是字符,在计算机中再把这些字符转换成字节进行处理。
字符流
CharArrayReader,数据源是字符数组,从这里读取数据。
CharArrayWriter,数据源是字符数组,往里面写入数据
字节流
2.NIO
即非阻塞(non-blocking) IO,也可以叫做new IO
2.1 三大组件
2.1.1 Channel
Channel:读写数据的双向通道,可以从channel中将数据读取数据,即向buffer中写入数据,也可以将buffer中的数据写入Channel。
常见的Channel有以下四种,其中FileChannel主要用于文件传输,其余三种用于网络通信
- FileChannel(只能工作在阻塞模式)
- DatagramChannel
- SocketChannel
- ServerSocketChannel
1 |
|
Channel可以是阻塞的,也可以是非阻塞的,默认是阻塞的,可以将ServerSocketChannel手动设置为非阻塞模式。
1 |
|
2.1.2 ByteBuffer
Buffer迎来缓冲读写数据,支持不同数据类型的缓冲区,有以下几种,其中使用较多的是ByteBuffer
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
所以下面都以ByteBuffer为例
2.1.2.1 属性
ByteBuffer中重要的属性:capacity(缓冲区容量),position(当前的读写位置指针,指向下一次要读写的位置),limit(读入写入限制大小指针)
bytebuffer默认是写模式,当写入数据后,flip()切换为读模式
clean()是切换写模式从头开始写,不管上次是否读完
compact()也是切换为写模式,会把上次未读完的放到缓冲区前面
反正始终记住,读前面先切换为读模式,写前切换为写模式
2.1.2.2 常见方法
allocate(大小)
创建buffer并给buffer分配空间。
1
2ByteBuffer buffer = ByteBuffer.allocate(16); //HeapByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(16);//DirectByteBufferHeapByteBuffer(使用java堆内存分配空间,读写效率低,会受到GC影响)
DirectByteBuffer(使用直接内存分配空间,读写效率高,不会受到GC影响)
read()/put()
向buffer写入数据。
1
2
3// readBytes实际读到的字节数
int readByets = channel.read(buffer);//从通道中读取数据,写入buffer
buffer.put((byte)127);//调用buffer自己的put方法write()/get()
从buffer中读取数据。
1
2
3//writeBytes实际写入channel的字节数
int writeBytes = channel.write(buffer);//从buffer中读数据,写入通道中
byte b = buffer.get();//会让position读指针向后移动一位,get(i)只会获取索引为i的数,而不会移动指针,rewind(),把position置0,重新读bytebuffer和字符串的转换
第一种方法将字符串放入buffer后,buffer还是写模式,如果想读,需要切换为读模式。
第二种和第三种添加完会自动改为读模式。
2.1.3 Selector
管理Channel并监听Channel上是否有事件发生
1.阻塞直到绑定事件发生
1 |
|
2.阻塞直到绑定事件发生,或者超时,事件单位是ms
1 |
|
1 |
|
事件种类:
accpet:会在有连接请求时触发
connect:是客户端,连接建立时触发
read:可读事件
write:可写事件
2.2 阻塞&非阻塞&多路复用
阻塞模式下,一个方法会影响其他方法,阻塞住无法继续运行。
非阻塞模式下,如果没有事件发生,也会继续运行,会一直进行空循环,非常浪费cpu性能
多路复用模式下,只有事件发生了,selector.select()才会继续运行,处理事件,如果没有事件,这里会阻塞住,不会让事件白忙活~
引用:https://www.bilibili.com/video/BV1py4y1E7oA?p=39&spm_id_from=pageDriver
2.3 Stream vs Channel
- strean不会自动缓冲数据,channel会利用系统提供的发送缓冲区和接受缓冲区,将通道中的数据暂存到缓冲区中
- stream仅支持阻塞模式,channel可以支持阻塞和非阻塞,网络channel可以配合selector实现多路复用
- 二者均为全双工,即读写可以同时进行
2.4 IO模型
当用户调用一次channel.read(没有设置为非阻塞的情况)或stream.read时,会切换到操作系统内核来完成数据的真正读取,而读取又分为等待数据和复制数据阶段。
2.4.1 阻塞IO
用户线程阻塞住,阻塞IO在做一件事的时候不能做另一件事
2.4.2 非阻塞IO
在等待数据阶段,用户线程会一直询问是否有数据,如果没有就会返回0,然后继续询问,这之间多次进行用户态和内核态的切换,非常耗费CPU性能,当有数据后,还是会阻塞住,等待内核复制数据。
2.4.3 多路复用
多路复用和阻塞IO的区别
2.4.4 同步和异步
同步:线程自己去获取结果(只有一个线程)
异步:线程自己不去获取结果,而是由其他线程送结果(至少两个线程)
上面三种都是同步的,即由线程自己发起的动作,也是由它主动接收结果。
异步:用户线程自己发起动作,并且由操作系统调用回调方法,由操作系统将最终的结果通过回调方法返回给用户线程。如图:
2.5 零拷贝
零拷贝指的是数据无需拷贝到 JVM 内存(用户缓冲区)中,同时具有以下三个优点
- 更少的用户态与内核态的切换
- 不利用 cpu 计算,减少 cpu 缓存伪共享
- 零拷贝适合小文件传输
传统io
1 |
|
NIO优化
ByteBuffer.allocateDirect(10)
- 底层对应DirectByteBuffer,使用的是操作系统内存
Java 可以使用 DirectByteBuffer 将堆外内存映射到 JVM 内存中来直接访问使用
减少了一次数据拷贝,用户态与内核态的切换次数没有减少
以下两种方式都是零拷贝,即无需将数据拷贝到用户缓冲区中(JVM内存中)
①底层采用了 linux 2.1 后提供的 sendFile 方法,Java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据
②linux 2.4 对上述方法再次进行了优化