网络编程-NIO 理论部分
NIO- no_block IO 或者叫NewIO JAVA 1.4引入的
1 、NIO和BIO的区别
1.1、面向流和面向缓存
- IO是面向流的,没有缓存去,所以如果需要前后移动从流中读取的数据,需要先将他缓存到一个缓存区。
- NIO是面向缓存区
1.2、阻塞与非阻塞
- java 的IO是阻塞模式的,当一个线程调用read()或者是write的时候,线程会被阻塞,
- NIO非阻塞模式,是一个线程从某个通道发送请求读取数据,但是仅仅能得到目前可用的数据。如果目前没有可用的数据的时候,就什么都不会获取,而不是保持线性阻塞,没有获取到想要的信息的时候可以干其他的事情。所以一个单独的线程可以管理多个输入和输出的通道。
1.3、Select
- NIO允许一个单独的线程来监视多个输入通道。可以注册多个通道使用一个选择器,然后使用一个单独的线程来选择通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
2、selector
selector
选择器,轮训代理器,事件订阅器,channel容
器管理器
应用程序将select
对象注册需要它关注的Channel
,以及具体的某一个Channel
会对哪些IO
事件感兴趣,Selector
中也会维护一个已经注册的Channel
的容器。
3、Channels
通道,被建立的一个应用程序和操作系统交互事件,传递内容的渠道,注意是连接到操作系统的 程序可以通过通道读取数据,也可以通过通道向操作系统写数据,而且可以同时进行读写。
- 所有被
select
注册的通道,只能是继承了selectableChanne l
类的子类 -
ServerSocketChannel
应用服务器程序的兼听监听通道。只有通过这个通道,程序才能向操组系统注册支持’多路复用IO
的端口监听。同时支持UDP,TCP
。 -
SocketChannel
TCPSocket
套接字的监听通道,一个Socekt
对应了一个客户端的IP
,端口到服务器IP
端口的通信连接
通道中的数据总是要先读到一个Buffer
或者,总是要从一个Buffer
中写入
4、SelectionKey
select
对象注册感兴趣的事件的时候,JAVA NIO
共定义了四种 OP_READ、OP_WRITE、OP_CONNECT、 OP_ACCEPT
- 服务器启动
ServerSocketChannel
关注OP_ACCEPT
事件 - 客户端启动
SocketChannel
连接服务器,关注OP_CONNECT
事件 - 服务器接受连接,启动一个服务器的SocketChannel,这个SocketChannel可以关注OP_READ OP_WRITE事件,一般连接后会直接关注OP_READ事件
- 客户端这边的客户端SOCKETCHANNEL发现连接建立之后,可以关注OP_READ OP_WRITE事件,一般需要客户端发送数据了才会关注OP_READ事件
四、Buffer
用来和NIO通道进行交互,数据是从通道读入缓存区,缓存去写入到通道中的。以写为例:应用程序都是将数据写入缓存,在通过通道把数据发出去,读也是一样的。缓存本质上就是一个可以写入数据,然后可以读取数据的内存。这块内存被包装成NIO Buffer的对象,提供了一组方法,用来方便的访问这块内存。
1、重要属性
- capacity
内存卡,Buffer又一个固定的大小,就是capacity,一旦满了,需要清空(读数据,清除数据) 才能继续往里写数据。
-
position
当你写数据到Buffer中的时候,position表示当前的位置,初始position值是0.当一个byte, long,等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1。读取数据的时候,也是也是从一个特定的位置开始读,Buffer写模式换成读模式的时候,position会重置成0档从Buffer的postion处读取数据的时候,position向前移动到下一个可读到位置。
-
limit
写模式下,Buffer的limit表示你最多能往Buffer里写多少的数据。写模式下,limit等于Buffer的capacity
读模式下,Buffer到读模式的时候,limit表示你最多能读多少数据。切换Buffer到读模式的时候,limit会被设置成写模式的下的position值。你能读到之前写入的所有的数据。
2、Buffer的分配
想要活的一个Buffer对象首先要进行分配,每一个Buffer类都有allocate方法,可以在堆上分配,也可以直接内存上分配。
ByteBuffer buf = ByteByffer.allocate(48);//分配48字节的
CharBuffer buf = CharBuffer.allocate(1024);
把一个byte数组或者是byte数组的一部分包装成ByteBuffer
ByteBuffer wrap(byte[] array)
ByteBuffer wrap(butep[] array,int office,int length);
HeapByteBuffer
和DirectByteBuffer
原理上,前者可以看出分配的buffer是在heap区域的,其实真正的flush到远程的时候,会先复制到直接内存,再做下一步操作。
NIO框架下,很多框架会采用DirectByteBuffer
来操作,这样内存不再是heap上,而是操作在系统的C heap上,经过性能测试,可以得到非常快的网络交互。大量的网络交互下,一般速度会比HeapByteBuffer
快速好几倍。
直接内存(Di re c t M e me o r y
)并不是虚拟机运行时候数据区的一部分,也不是java虚拟机规范中定义的内存区域。这部分内存频繁的被使用,也可以导致OurOfMemoryError
出现。NIO可以使用Native函数库直接分配对外内存,然后通过一个存储在java堆里边的DirectByteBuffer
对象作为这个内存的应用进行处理
>1 堆外内存优缺点
相比较堆内内存有几个优势:
- 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作。
- 加快了复制的速度。省略了堆内flush到远程的时候,先复制到直接内存的过程;
不好的一面
- 难以控制,如果内存泄露,很难排查
- 不适合存储很复杂的对象,一般简单的对象或者是扁平化的比较合适。
>2 直接内存(堆外内存)与堆内存的比较
直接内存申请空间耗费更多的性能。频繁处理的时候这一点很明显
直接内存IO速度性能优于普通内存。多次读写操作的情况下,差异明显
3、Buffer的读写
1. 向Buffer中写数据
两种方式:
-
读取
Channel
写到Buffer
。int bytesRead = inChannel.read(buf);//read into buffer
-
通过
Buffer的put()
方法到Buffer
里buf.put(127);
flip()
方法:
flip
方法将Buffer
从写模式切换到读模式,调用flip
会将position
改为0
并将limit
,设置成之前position
的值。
2. 从Buffer中读取数据
-
从Buffer读取数据写入到Channel
-
使用get方法从Buffer中读取数据。
int bytesWritten = inChannel.write(buf);//从Buffer读取数据到Channel bute aByte =buf.get();//从Buffer读取数据到Channel
使用Buffer
读写数据常见步骤
- 写入数据到
Buffer
- 调用
flip()
方法 - 从
Buffer
中读取数据 - 调用
clear()
方法或者compact()
方法,清除掉缓存中的数据。
-
clear()方法:position将被设回0 limit被设置成capacity的值,换句话说Buffer被清空了,Buffer中的数据并未清除。如果Buffer中又一些未读的数据,调用clear()方法,数据将被遗忘,以为不再有任何标记会告诉你哪些数据被读过,哪些还没有。
如果Buffer中仍然有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么使用conpact方法。
-
compact()
方法将所有未读的数据拷贝到Buffer
起始处,,然后将position
设置到最后一个未读元素正后面,limit
属性依然像clear()
方法一样,设置成capacity
。现在Buffer准备好些数据了,但是不会覆盖未读的数据。 -
equals()与compareTo()方法:比较两个Buffer
-
equals满足下列条件是,表示两个Buffer相等:
- 有相同的类型(byte char int)
- Buffer中剩余的byte,char等的个数
- Buffer中所有剩余的byte,char等都相同。
-
compareTo()方法:
compareTo方法比较两个Buffer的剩余元素,如果满足下列条件,认为一个Buffer小于另一个Buffer
- 第一个不相等的元素小于另一个Buffer中对应的元素。
- 所有元素都相等,但第一个Buffer比另一个先耗尽,第一个Buffer的元素个数比另一个少。