一、OIO
1、一些细节
1)传统的IO是面向,流,的
2)流在底层输入与输出都是传输的字节,字符流只是上层的封装
3)OIO中流要么是输入流,要么是输出流,不可能即是输入流又是输出流
2、流的分类
分类方式1:输入流、输出流
输入、输出是相对于应用程序来说的,应用程序向外传递数据为输出,外部向应用程序传递数据为输入
分类方式2:节点流、过滤流
节点流:从特定的地方读写的流,例如:磁盘或一块内存区域
过滤流:使用节点流作为输入或输出,过滤流是使用一个已经存在的输入流或输出流连接创建的,过滤流一定是依赖一个节点流的
3、java.io包中InputStream/OutputStream的类层次
其中蓝色为节点流、紫色为过滤流
4、java.io包中Reader/Writer的类层次
5、OIO流的创建包装过程体现的设计模式:装饰器模式
装饰模式说明
装饰器模式扩展的是:对象功能、动态的
继承 扩展的是:类的功能、静态的
- 装饰模式以对客户(使用这个对象的一方)透明的方式动态的给一个对象附加上更多的功能,客户端并不会觉得对象在装饰前和装饰后有什么不同。
- 装饰模式可以在不创建更多子类的情况下,将对象的功能加以扩展。
- 装饰模式把客户端的调用,委派到被装饰类。装饰模式的关键在于这种扩展完全是透明的。
- 装饰模式是在不必改变原类文件和使用继承的情况下,动态扩展一个对象的功能。他是通过创建一个包装对象,也就是装饰来包裹真实的对象
装饰模式的角色
1、抽象构件角色(Component):给出一个抽象接口,以规范准备接收附加责任的对象。
- OIO中对应InputStream
2、具体构件角色(Concrete Component):定义一个将要接收附加责任的类
- OIO中对应FileInputStream
3、装饰角色(Decorator):持有一个构件对象的引用,并定义一个与抽象构件接口一致的接口
- OIO中对应FilterInputStream
4、具体装饰角色(Concrete Decorator):负责给构件对象贴上附加的责任(功能)。
- OIO中对应BufferedInputStream
装饰模式的特点
- 装饰对象和真实对象有相同的接口。这样客户端对象就可以以和真实对象相同的方式调用装饰对象。
- 装饰对象包含了一个真实对象的引用
- 装饰对象接收所有来自客户端请求,把请求转发给真实对象
- 装饰对象可以在转发前 或者 后,增加一些附加功能。这样就确保了在运行时,不用修改给定对象的结构,就可以在外部增加附加的功能。在面向对象的设计中,通常是通过继承来实现对给定类的功能扩展。
举例:
其他角色略。
具体装饰角色:
客户端调用:
装饰模式与继承比较
装饰模式 | 继承 |
|
|
装饰模式的适用场景
- 想要透明并且动态的给对象增加新的功能而又不会影响其他对象
- 给对象增加的功能,在未来可能会发生改变
- 用子类(继承)扩展功能不实际的情况下
Jdk IO库使用装饰器模式带来的好处
- 即满足IO体系对输入输出流增加功能的要求
- 在运行期动态增加功能,避免继承方式会造成的类的数量庞大
二、NIO
OIO | NIO | |
核心概念(组件) | 1、Stream:流 | 1、Selector:选择器 2、Channel:通道 3、Buffer:缓冲区 |
特点 | OIO是面向【流stream】进行编程 | NIO是面向【块block】编程,buffer本身就是一块内存,底层实现上,上一个数组,数据的读、写都是通过buffer实现的 |
读写方式 | OIO中是可以直接读/写stream | NIO中数据是来自于Channel的,需要先将数据由Channel读/写至buffer,再读/写buffer中的数据 |
职责类比 | OIO中Stream只能是输出流或者输入流其中一种,不可能同时具备 | 与Stream不同的是,Channel是双向的 |
(一)Buffer
- NIO中数据是来自于Channel的,需要先将数据由Channel读/写至buffer,再读/写buffer中的数据
- 一个Buffer可以即用做读,也用作写,但是在(用于)读/写之间进行切换时,需要通过API中的【flip】方法进行切换读写状态。因为Buffer中维护了若干状态(capacity、limit、position)。
- 正是因为数据需要先读/写至buffer, 所以NIO中的缓冲区(buffer)可以同时具备读与写两个职责
- 除了数组之外,Buffer还提供了对于数据的结构化访问方式,并且可以追踪到系统的读写过程。意思是:读和写 在底层都是通过 相应的标识标记的读到什么位置,写到什么位置Buffer已经封装好了,调用flip翻转后,从哪开始读/往哪开始写
- Java中7种原生数据类型,都有对应的Buffer类型
其中没有boolean对应的buffer类型。
1、capacity
一个buffer的容量;一旦分配后,永远不会变化;不会为负
2、limit
JDK中说明:第一个不能读或者不能写的位置的索引;
对应一个索引,这个索引是第一个不会写/读的元素索引,不会为负、永远不会超过capacity,初始值是capacity
3、position
下一个准备被读/写的元素的索引,不会为负、永远不会超过limit,初始值是0
4、0<=mark<=position<=limit<=capacity
5、【绝对操作】与【相对操作】
每一个buffer子类都具备两种类型的读&写操作方法
- 相对操作:会改变position与limit值
- 绝对方法:给定义一个索引位置,直接读取或者设置对应元素,不会改变position与limit大小
6、只读buffer
isReadOnly方法判断buffer是否是只读byte
7、线程安全
Buffer不是线程安全的,需要在多线程环境下自己进行同步
8、调用链
以方法链(返回buffer本身的方法)的编程风格调用buffer方法
9、直接缓冲区(Direct Buffer)
JavaNIO的Buffer有两类(按照内存分配方式不同进行的分类)
I类:在堆上分配内存(由JVM控制)HeapByteBuffer
II类:在堆外内存分配(非JVM控制,由操作系统控制)DirectByteBuffer
通过直接缓冲区(Direct Buffer)可以实现zero-copy
1)内存模型:Direct Buffer占用的内存包括两部分:
address:在堆中的java对象中记录的,堆外内存的地址
根据jdk注释:address变量,只会被DirectByteBuffer中使用,正常来说,设计应该放在Direct Buffer中,但是设计上将此变量升级升级到放到了Buffer中,目的是提升性能,这个对象是引用的堆外内存的内存地址,这个对象就是JVM与堆外内存建立关系的桥梁。
2)优势:提升速度
Heap Buffer的读写过程:数据从【JVM堆】拷贝到【操作系统内存区域】再由操作系统内存区域与IO设备交互
Direct Buffer的读写过程:数据是在堆外分配的【操作系统内存区域】,所以可以直接与IO设备交互
这种读写方式称为:零拷贝zero-copy
zero-copy:在进行IO操作时,不必将你的buffer中内容,再去copy一份,放在操作系统内存中,也就是可以直接将分配的内存空间(直接缓冲区Direct Buffer)直接与IO设备打交道,减少内存拷贝中转造成的性能开销。
zero-copy详见我的博客。
10、类型化get/put方法
1、因为网络传输数据都是通过字节的形式,所以7种原生类型的Buffer底层都是以字节的形式进行的存储。
2、所以,ByteBuffer提供了其他6种原生数据类型的put/get方法,可以直接将int、char、double、float、long、short、byte类型数据直接存放到ByteBuffer里,或者直接从ByteBuffer里取出上述类型的数据。
注意:按照什么顺序放入什么类型数据,取出时就需要使用相同的顺序
package com.mzj.netty.ssy._09_nio;
import java.nio.ByteBuffer;
public class NioTest6 {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(64);//分配一个容量为64个字节的ByteBuffer
buffer.putInt(666);
buffer.putLong(50000000000L);
buffer.putDouble(3.1415926);
buffer.putChar('你');
buffer.putShort((short) 2);
buffer.putChar('我');
buffer.flip();
System.out.println(buffer.getInt());//按照放置的顺序依次取出
System.out.println(buffer.getLong());//这种行为、特性适合自定义协议
System.out.println(buffer.getDouble());
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
System.out.println(buffer.getChar());
}
}
输出:
11、分割、分片ByteBuffer(ByteBuffer的slice方法)
作用:返回原Buffer的部分数据(底层数据是一份,修改其中Buffer的数据会对另一个Buffer产生影响,position、limit、mark、capacity是各自的)
ByteBuffer的slice方法:创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。 新缓冲区的内容将从此缓冲区的当前位置(position)开始,到limit为止。此缓冲区内容的更改在新缓冲区中是可见的,反之亦然;这两个缓冲区的位置、界限和标记值是相互独立的。但是两个缓冲区底层的数据是一份。
package com.mzj.netty.ssy._09_nio;
import java.nio.ByteBuffer;
/**
* slice方法创建的buffer与底层buffer共有底层数据
* @Auther: mazhongjia
* @Description:
*/
public class NioTest7 {
public static void main(String[] args) {
//1.分配一个容量为10个字节的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(10);
//2.填充缓冲区
for (int i = 0; i < buffer.capacity(); ++i) {
buffer.put((byte) i);
}
//3.修改position、limit以准备调用slice
buffer.position(2);
buffer.limit(6);
/**
* 创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。
* 新缓冲区的内容将从此缓冲区的当前位置开始。此缓冲区内容的更改在新缓冲区中是可见的,反之亦然;这两个缓冲区的位置、界限和标记值是相互独立的。但是两个缓冲区底层的数据是一份
*/
ByteBuffer sliceBuffer = buffer.slice();
System.out.println(sliceBuffer);
System.out.println("-------------");
//4.修改创建新的字节缓冲区内容:原先ByteBuffer的每一个元素*2
for (int i = 0; i < sliceBuffer.capacity(); ++i) {
byte b = sliceBuffer.get(i);
b *= 2;
sliceBuffer.put(i, b);
}
//5.打印原缓冲区内容,结果也发生变化,证明两份缓冲区公用相同底层数据
//重新调整position、limit值,以便打印全部数据
buffer.position(0);
buffer.limit(buffer.capacity());
while (buffer.hasRemaining()) {
System.out.println(buffer.get());//get方法是相对类型,调用会影响position或者limit值
}
}
}
输出:
12、只读Buffer
1、作用:在将缓冲区传递给某个对象的方法时,无法知道这个方法是否会修改缓冲区中的数据,创建一个只读缓冲区可以保证该缓冲区不会被修改;
2、说明:
- 只读Buffer与原Buffer共享数据,有自己独立的position、limit、capacity、mark
- 如果试图修改只读Buffer,会抛异常:
- 如果原缓冲区的内容发生变化,只读缓冲区的内容也随之发生变化
- 可以随时将一个可读可写的buffer通过asReadOnlyBuffer方法转换成只读buffer,但是不可能将一个只读buffer转换成可读写的buffer
- 只读Buffer在jdk起名上约定叫:XXXR,比如:HeapByteBufferR、DirectByteBufferR
3、创建只读buffer的方式:调用正常buffer的asReadOnlyBuffer()
13、方法逻辑图示
1、新创建
ByteBuffer.allocate(10);
默认创建的是:HeapByteBuffer(堆ByteBuffer)
2、put操作
我们将代表“abcde”字符串的 ASCII 码载入一个名为 buffer 的ByteBuffer 对象中。当在图1 中所新建的缓冲区上执行以下代码后。
buffer.put((byte)'a').put((byte)'b').put((byte)'c').put((byte)'d').put((byte)'e');
缓冲区的结果状态如图2:
3、flip() 方法:准备从buffer中读取数据,读取之前,调用一次flip
反转缓冲区(设置limit指向当前position位置,position指向0,mark = -1):
我们已经写满了缓冲区,现在我们必须准备将其清空。我们想把这个缓冲区传递给一个通道,以使内容能被全部写出。但如果通道现在在缓冲区上执行 get(),那么它将从我们刚刚插入的有用数据之外取出未定义数据。如果我们将位置值重新设为 0,通道就会从正确位置开始获取,但是它是怎样知道何时到达我们所插入数据末端的呢?这就是上界属性被引入的目的。上界属性指明了缓冲区有效内容的末端。我们需要将上界属性设置为当前位置,然后将位置重置为 0。
flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态。在翻转之后,图 2 的缓冲区会变成图 3 中的样子。
flip方法完成: ①limit = position ②position重设为初始0位置 ③丢弃mark标记 |
扩展:如果连续两次调用flip方法:
limit位置指向当前postition位置0
postition指向位置0
补充:flip方法经常与compact方法一起使用,当从一个地方向另一个地方传输数据时(来自jdk)
4、rewind() 方法
只设置position=0,mark = -1:
rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。您可以使用 rewind()后退,重读已经被翻转的缓冲区中的数据。图2 的缓冲区调用 rewind() 方法会变成图4 中的样子。
rewind方法完成: ①position重设为初始0位置 ②丢弃mark标记 |
5、compact() 方法
压缩缓冲区:
有时,您可能只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这一点,未读的数据元素需要下移以使第一个元素索引为 0。尽管重复这样做会效率低下,但这有时非常必要,而 API 对此为您提供了一个 compact()函数。这一缓冲区工具在复制数据时要比您使用 get()和 put()函数高效得多。所以当您需要时,请使用 compact()。图 5显示了一个读取了两个元素(position 现在为2),并且现在我们想要对其进行压缩的缓冲区。
比如:执行compact() 方法前,缓冲区如下:
执行:
buffer.compact();
执行后:
可见,压缩后:
- 已经消费完的数据(97,98)被覆盖掉了;
- 尚未使用的数据(99,100,101)被复制到了缓冲区的最前面;
- limit设置到了capacity的位置
- position设置到了准备开始继续向缓冲区写入数据的位置
6、duplicate() 方法
复制一个与原缓冲区共享数据的缓冲区:
duplicate() 方法创建了一个与原始缓冲区一样的新缓冲区。两个缓冲区共享数据,拥有同样的 capacity ,但每个缓冲区都拥有自己的 position,limit 和 mark 属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。
7、asReadOnlyBuffer()方法
复制一个与原缓冲区共享数据的只读缓冲区:
您 可 以 使 用 asReadOnlyBuffer() 函 数 来 生 成 一 个 只 读 的 缓 冲 区 视 图 。 这 与duplicate()相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly()函数将 会 返 回 true 。 对 这 一 只 读 缓 冲 区 的 put() 函 数 的 调 用 尝 试 会 导 致 抛 出
ReadOnlyBufferException 异常。
实现上,通过HeapByteBufferR 进行实现:HeapByteBufferR 继承 HeapByteBuffer 类,并重写了所有的可修改 buffer 的方法。把所有能修改 buffer 的方法都直接 throw ReadOnlyBufferException,来保证只读。
8、slice() 方法
slice() 分割缓冲区:
创建一个从原始缓冲区的当前位置开始的新缓冲区,并且其容量capacity是原始缓冲区的剩余元素数量( limit-position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。
两个缓冲区共享数据,每个缓冲区都拥有自己的 position,limit 和 mark 属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。
9、clear方法
Clear操作很简单,就是清空缓冲区的所有数据。因此,所有标志位都会被恢复成默认值,包括mark值,但是记住,不是数据,写入缓冲区的数据仍然保留,如果这时候执行get操作,仍然可以将数据获取出来。直观的话,可以直接看源代码:
position = 0;
limit = capacity;
mark = -1;
clear方法完成: ①position重设为初始0位置 ②limit重设为初始capacity位置 ③丢弃mark标记 相当于重置buffer |
10、reset方法
说明:设置position为mark标记位置,如果尚未设置mark标记,则抛出异常,调用reset方法前需要通过mark方法设置mark位置
11、mark方法
说明:设置position为mark,对当前position做标记,以便之后从设置的position重读或重写数据
12、hasRemaining方法
说明:查看在当前位置和limit位置之间是否有元素
(二)Channel
- Channel指的是,可以向其写入数据或是从中读取数据的对象,类似于IO中stream,但是读写操作都需要通过buffer
- 由于Channel是双向的,因此它能更好的反映出底层操作系统的真是情况:在linux系统中,底层操作系统的通道就是双向的
- Channel指的是,可以向其写入数据或是从中读取数据的对象,类似于IO中stream,但是读写操作都需要通过buffer
开胃示例1:
package com.mzj.netty.ssy._09_nio;
import java.nio.IntBuffer;
import java.security.SecureRandom;
/**
* 示例1
*/
public class NioTest1 {
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.allocate(10);
for(int i=0;i<buffer.capacity();i++){
int randomNumber = new SecureRandom().nextInt(20);
buffer.put(randomNumber);
}
buffer.flip();
while (buffer.hasRemaining()){
System.out.println(buffer.get());
}
}
}
输出:
开胃示例2:
package com.mzj.netty.ssy._09_nio;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 示例2
*/
public class NioTest2 {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("NioTest2.txt");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);//最多读取512个字节
fileChannel.read(byteBuffer);
//由写到读转换需要调用一次flip进行反转
byteBuffer.flip();
while (byteBuffer.remaining() > 0){
byte b = byteBuffer.get();
System.out.println("Char..." + (char)b);
fileInputStream.close();
}
}
}
输出:
ServerSocketChannel
定义:服务器套接字通道Channel对象
SocketChannel
定义:客户端套接字通道Channel对象,代表服务端与客户端连接的通道对象
(三)Selector
1、传统的网络编程
传统的网络编程服务端:1客户端1线程的线程模型缺点
客户端连接过多时,一个线程对应一个客户端的方式消耗CPU资源,使得服务端无法支持更多客户端连接。
特殊说明
服务端供客户端连接的端口号,比如8899,客户端连接接后,之后进行的IO操作,其实是在服务端再随机分配一个未被占用的端口进行通信;这个服务端的8899端口仅用于客户端建立连接使用,而后续的数据发送,不是在这个端口号上
2、NIO中 基于Selector的 网络线程模型
1)特点:
NIO编程使得服务端使用一个线程处理可以处理与N多客户端的数据交互。
NIO编程模型特别适用于:客户端连接数非常多,但是消息量不是特别大,这种情况使用NIO编程性价比最高。
Selector官网文档说明(我整理后):
(1)通过调用Selector的open方法创建、调用close关闭,该方法将使用系统的默认选择器提供者(SelectorProvider)创建新的选择器。也可通过调用自定义选择器提供者的 openSelector 方法来创建选择器。
(2)Channel到Selector的注册,是通过SelectionKey进行标识的
(3)将Selector注册到NIO通道上进行使用,注册完形成SelectionKey对象
(4)Selector中会维护三种SelectionKey集合:
①keys:key set表示Channel注册到Selector上后,所注册的所有SelectionKey集合(注册的所有感兴趣事件类型),此集合由 keys 方法返回。
②selected-key set:表示已经准备就绪的事件类型,此集合由 selectedKeys 方法返回。已选择键集始终是key set的一个子集
③cancelled-key set是之前关心过,现在已经被取消关心的事件类型集合,也是key set的子集
(5)通过向Channel进行register一个Selector可以将这个Selector注册到Channel上,同时完成向Selector的key set集合添加一个SelectionKey
(6)每次调用Selector的select操作,会返回本次调用时触发的感兴趣操作集合:selected-key set
(7)通过关闭Channel 或者 调用SelectionKey的cancel方法,可以向Selector的cancelled-key set添加一个SelectionKey
(8)取消某个键会导致在下一次选择操作期间注销该键的通道,而在注销时将从所有选择器的键集中移除该键。
(9)通过选择操作将键添加到已选择键集中。可通过调用已选择键集的 remove 方法,或者通过调用从该键集获得的 iterator 的 remove 方法直接移除某个键。
(10)选择:在每次选择操作期间,都可以将键添加到选择器的已选择键集以及从中将其移除
(11)并发性:一般情况下,选择器的键和已选择键集由多个并发线程使用是不安全的。如果这样的线程可以直接修改这些键集之一,那么应该通过对该键集本身进行同步来控制访问。这些键集的 iterator 方法所返回的迭代器是快速失败 的:如果在创建迭代器后以任何方式(调用迭代器自身的 remove 方法除外)修改键集,则会抛出 ConcurrentModificationException。
SelectionKey官网文档说明(我整理后):
(1)SelectionKey代表了一件事情:Channel注册到Selector上
(2)每次,一个Channel注册到Selector上都会创建SelectionKey
(3)在通过调用某个SelectionKey的cancel方法、或者关闭其通道,或者通过关闭其选择器来取消该键之前,它一直保持有效。
(4)调用某个SelectionKey的cancel方法不会立即从其选择器中移除它;相反,会将该SelectionKey添加到选择器的已取消键集(cancelled-key set),以便在下一次进行选择操作时移除它。
(5)SelectionKey中维护了两个操作集合,通过整数表示的
①Ready集合:准备好的Selection事件集合
②Interest集合:感兴趣的Selection事件集合
(6)attach方法:可以将任意对象数据放置(关联)到SelectionKey对象上
(7)attachment方法:获取放置的数据
(8)channel方法:可以通过SelectionKey获取在其上面关联的Channel对象(特别重要的功能)
2)示例代码:
package com.mzj.netty.ssy._09_nio.selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* NIO开发的服务端程序
* <p>
* 服务端只用一个线程、一个selector,监听5个端口号上的连接请求、处理这5个客户端读/写
*
* @Auther: mazhongjia
* @Version: 1.0
*/
public class SelectorTest1 {
public static void main(String[] args) throws IOException {
//1、5个端口号
int[] ports = new int[5];
ports[0] = 5000;
ports[1] = 5001;
ports[2] = 5002;
ports[3] = 5003;
ports[4] = 5004;
//2、构造Selector
Selector selector = Selector.open();//Selector的创建方式:Selector.open()
//3、将一个Selector用于上述5个服务器的监听客户端连接的使用上
for (int i = 0; i < ports.length; i++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置Channel阻塞模式,false:非阻塞
serverSocketChannel.configureBlocking(false);
//获取到ServerSocket对象
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(ports[i]);
//服务器Socke绑定端口
serverSocket.bind(address);
//将Channel注册到Selector上,并返回SelectionKey
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//参数1:选择器,参数2:selectkey的集合(int),当前状态(服务端未开启连接时)只能选择接受连接这一个
System.out.println("监听端口:" + ports[i]);
}
while (true) {
System.out.println("服务端selector等待事件发生.....");
int numbers = selector.select();//返回准备好的(已经发生的事件)的SelectionKey的数量
System.out.println("numbers:" + numbers);
Set<SelectionKey> selectionKeys = selector.selectedKeys();//返回This selector's selected-key set,就是已经准备好的(已经发生的事件)集合
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey selectionKey = iter.next();
if (selectionKey.isAcceptable()) {//此处只处理客户端连接事件(因为不会有其他事件产生,只有这一种事件)
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();//服务端SocketChannel对象(在上面Open的时候已经配置了非阻塞)
//通过ServerSocketChannel的accept方法在有客户端端连接事件时(OP_ACCEPT事件产生时),获取客户端的SocketChannel对象
SocketChannel socketChannel = serverSocketChannel.accept();//获取客户端SocketChannel对象
socketChannel.configureBlocking(false);//设置客户端Socket为非阻塞
socketChannel.register(selector, SelectionKey.OP_READ);//客户端也使用同一个selector,注册感兴趣事件:读(这里的读还是写是相对于服务端来说的,这里的读是代表客户端发送给服务端)
System.out.println(selector.keys());
iter.remove();//特别重要:处理完一个selectionkey的事件后,必须将这个selectionkey从selectedKeys集合中移除(当前选择器关心的事件类型集合),不然会出问题
System.out.println("获得客户端连接:" + socketChannel);
//到此已经完成接收客户端连接过程的处理,并且注册了客户端
} else if (selectionKey.isReadable()) {//服务端的Read事件:处理来自客户端数据的,读入事件,相当于读取客户端 发 给服务端的数据,这里的Read(包括上面的OP_READ),是相对于服务端的。如果这里理解不好,们可以参考:com.shengsiyuan.nio.niosocket包里的示例理解
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int bytesRead = 0;
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
byteBuffer.clear();
int read = socketChannel.read(byteBuffer);
if (read <= 0) {
break;
}
byteBuffer.flip();
socketChannel.write(byteBuffer);//将从客户端读到的数据,再写回给客户端
bytesRead += read;
}
System.out.println("本次客户端读事件发生后,读取:" + bytesRead + ",来自于:" + socketChannel);
iter.remove();//非常重要:处理完一个selectionkey事件,就需要将其在selectionkey集合中删除(表示这个selectionkey事件已经用完、消费掉了),不然会出问题
}
}
}
}
}
运行示例:启动后使用telnet工具测试,连接服务端后,向服务端发送数据,服务端将相同的内容返回给客户端。
3)下面通过一个更加完整的NIO Server+Client示例,目的是学习NIO编程的一般模式
同时体会NIO编程与普通的IO编程上的区别.
server:
package com.mzj.netty.ssy._09_nio._03_niosocket;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* 比较完整的NIO Server+Client示例,目的是学习NIO编程的一般模式
*
* 实现多个客户端连接一个Server,某个client发送给server消息后,将其转发给其他客户端
*
* 本示例并不完善,待处理:某个客户端关闭了,没有从列表中删除
*/
public class NIOServer {
/**
* 选择器
*/
private Selector selector;
/**
* 通道
*/
ServerSocketChannel serverSocketChannel;
/**
* 在服务端记录客户端信息,使得可以在后续向客户端发送数据
*/
private Map<String, SocketChannel> clients = new HashMap<>();
public void initServer(int port) throws IOException {
//------------使用NIO进行socket编程,以下四步创建服务器端socket是模板代码---------------
//1、打开一个通道
serverSocketChannel = ServerSocketChannel.open();
//2、一定要把通道设置非阻塞(必须)
serverSocketChannel.configureBlocking(false);
//3、serverSocketChannel.socket()绑定端口号
serverSocketChannel.socket().bind(new InetSocketAddress("localhost", port));
//4、注册
this.selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//根据选择器模型,这里的注册是将Channel注册到Selector上
//注册完后就开始事件的处理
}
public void listen() throws IOException {
System.out.println("Server started succeed!");
while (true) {
try {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.forEach(selectionKey -> {
final SocketChannel client;
try {
if (selectionKey.isAcceptable()) {
// ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();//通过产生accept事件的SelectionKey的channel方法获取到的channel对象就是serverSocketChannel,可以通过强制类型转换成ServerSocketChannel,因为只有ServerSocketChannel才有accept类型的事件产生
client = serverSocketChannel.accept();//SocketChannel:服务端与客户端连接的通道对象
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
//在服务端记录客户端信息,使得可以在后续向客户端发送数据
String key = "[" + UUID.randomUUID() + "]";
clients.put(key, client);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();//因为这里只有客户端发送数据,所以read事件产生时,可以直接强制类型转换成SocketChannel
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = client.read(readBuffer);//向bytebuffer中写入客户端发送来的数据(以字节方式写入)
if (count > 0) {//如果有数据
readBuffer.flip();//反转ByteBuffer(准备读取bytebuffer中数据)
Charset charset = Charset.forName("utf-8");//创建uft-8的字符集
String receiveMessage = String.valueOf(charset.decode(readBuffer).array());//将读到的字节素组以utf-8形式进行解码
System.out.println(client + ": " + receiveMessage);
//获取发送消息客户端的key: String key = "[" + UUID.randomUUID() + "]";
String senderKey = null;
for (Map.Entry<String, SocketChannel> entry : clients.entrySet()) {
if (client == entry.getValue()) {
senderKey = entry.getKey();
break;
}
}
//向所有client发送消息
for (Map.Entry<String, SocketChannel> entry : clients.entrySet()) {
SocketChannel value = entry.getValue();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((senderKey + " : " + receiveMessage).getBytes());
writeBuffer.flip();//写->读 之前,需要flip
value.write(writeBuffer);
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
});
selectionKeys.clear();//处理完selectionKey后,将selected-key set集合清空也可以,不非得 通过iterator删除
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void recvAndReply(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(256);
int i = channel.read(buffer);
if (i != -1) {
String msg = new String(buffer.array()).trim();
System.out.println("NIO server received message = " + msg);
System.out.println("NIO server reply = " + msg);
channel.write(ByteBuffer.wrap(msg.getBytes()));
} else {
channel.close();
}
}
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8080);
server.listen();
}
}
client:
package com.mzj.netty.ssy._09_nio._03_niosocket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 客户端
*
* 客户端输入来自标准输入流(输入动作肯定不能放在主线程里,因为如果没有通过键盘输入则会一直阻塞当前线程)
*/
public class NIOClient {
public static void main(String[] args) {
try {
//1、创建一个客户端Channel
SocketChannel socketChannel = SocketChannel.open();
//2、设置Channel非阻塞
socketChannel.configureBlocking(false);
//3、创建一个Selector
Selector selector = Selector.open();
//4、将客户端Channel注册到Selector上,关心事件为连接事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
//5、发起向服务端的连接
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
//如果客户端为长连接,则客户端线程也是不会退出的
while (true) {
//阻塞等待事件产生
selector.select();
//产生事件后,获取selectionKey集合
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
//处理事件
Iterator<SelectionKey> iterable = selectionKeySet.iterator();
while (iterable.hasNext()) {
SelectionKey selectionKey = iterable.next();
if (selectionKey.isConnectable()) {//客户端连接成功事件(isConnectable返回true表示已经与服务端建立好了连接)
//连接成功后,客户端通过selectionKey获取的SelectableChannel类型一定是Socketchannel类型
SocketChannel client = (SocketChannel) selectionKey.channel();
//判断连接是否处于正在进行的状态
if (client.isConnectionPending()) {
//完成这个连接
client.finishConnect();//需要主动调用出发的
//建立成功后,向服务器发送连接成功的消息
ByteBuffer writerBuffer = ByteBuffer.allocate(1024);
writerBuffer.put((LocalDateTime.now() + "连接成功").getBytes());
writerBuffer.flip();//写->读 之前,需要flip
client.write(writerBuffer);
//客户端通过一个线程,一直监听标准输入流,将输入数据发送给服务端
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
while (true) {
try {
writerBuffer.clear();
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(inputStreamReader);
String sendMessage = br.readLine();
writerBuffer.put(sendMessage.getBytes());
writerBuffer.flip();
client.write(writerBuffer);
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
}
//客户端连接成功后,注册客户端数据读取事件(用于接收服务端发送给客户端的数据)
//这里的read,是相对于客户端来说的,也就是客户端读取到(来自服务端的)数据
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {//处理客户端读取事件(读取服务端向客户端发送的数据)
SocketChannel client = (SocketChannel) selectionKey.channel();//第一件事永远是将selectionKey获取到的channel进行强制转换
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = client.read(readBuffer);
if (count > 0) {//如果读取到数据,则构造成字符串形式输出标准输出流
String receiveMessage = new String(readBuffer.array(), 0, count);
System.out.println(receiveMessage);
}
}
//下面这一行代码特别重要:处理完一个事件后,需要将事件在selectedKeys集合中删除~~~!!
iterable.remove();
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
(四)其他特性
1、内存映射文件
1、说明
- 所谓内存映射文件,是指把文件的全部或者部分内容映射到内存中,只操作内存就可以完成对文件的操作,不需要直接操作文件。
- 对内存的操作都会反映到硬盘文件中(由操作系统保证)
- 用于内存映射文件的内存,位于JVM堆外
- 应用程序只需要处理内存的数据,可以实现非常快的IO操作
- 内存映射文件最多支持java整形int 最大值长度的字节数,也就是2GB文件大小,2147483647个字节
package com.mzj.netty.ssy._09_nio;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* 内存映射文件
*
* @Auther: mazhongjia
* @Description:
*/
public class NioTest10 {
public static void main(String[] args) throws IOException {
System.out.println(Integer.MAX_VALUE);
//创建RandomAccessFile对象
RandomAccessFile randomAccessFile = new RandomAccessFile("MemoryMapFile.txt","rw");
//通过RandomAccessFile对象获取:NIO的文件Channel对象(1.4版本与NIO一起增加到RandomAccessFile中)
FileChannel fileChannel = randomAccessFile.getChannel();
//创建内存映射文件对象,映射的内容是文件位置0,长度5个字节长度,最大支持映射整形int的最大值长度的字节数,也就是2GB文件大小
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);
//修改第0个元素,内容改成a
mappedByteBuffer.put(0, (byte) 'a');
//修改第3个元素,内容改成b
mappedByteBuffer.put(3, (byte) 'b');
//关闭文件
randomAccessFile.close();
}
}
原文件内容:
运行后文件内容:
2、文件锁
1、说明
文件锁类型:
- 共享锁:读锁
- 排他锁:写锁
共享锁与排他锁交替使用共享资源的逻辑关系:
- 一个JVM如果获取了共享锁(未释放),另一个JVM可以继续获取共享锁
- 一个JVM如果获取了共享锁(未释放),另一个JVM不可以继续获取排他锁
- 一个JVM如果获取了排他锁(未释放),另一个JVM不可以继续获取共享锁
- 一个JVM如果获取了排他锁(未释放),另一个JVM不可以继续获取排他锁
2、使用场景
文件锁实际工作中用的较少
3、使用
package com.mzj.netty.ssy._09_nio._01_buffer;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
/**
* 文件锁
*
* @Auther: mazhongjia
* @Description:
*/
public class NioTest11 {
public static void main(String[] args) throws IOException {
//1、创建RandomAccessFile对象
RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw");
//2、通过RandomAccessFile对象获取:NIO的文件Channel对象(1.4版本与NIO一起增加到RandomAccessFile中)
FileChannel fileChannel = randomAccessFile.getChannel();
//3、获取文件锁对象
FileLock fileLock = fileChannel.lock(3,6,true);//第三个参数:true共享锁,false:排他锁,前两个参数:从文件位置3开始,锁定6个字节长度
System.out.println("valid:" + fileLock.isValid());//判断锁有效性(是否成功锁定)
System.out.println("locak type : " + fileLock.isShared());//判断锁的类型,是否是共享锁
fileLock.release();//释放锁
//4、关闭文件
randomAccessFile.close();
}
}
执行结果:
3、【Scattering】与【Gathering】
1、说明
- Scattering
- 作用:把一个Channel中的数据,读到多个buffer中,按照顺序,一个buffer满了之后向下一个buffer中继续读入数据.....
- 特点:按照buffer的顺序,一个满了,才下一个....
- Gathering
- 作用:与Scattering正好是反过来的,把多个buffer中数据,按照先后顺序写入一个Channel中,写完一个,写下一个....
- 这两个东西“Scattering”“Gathering”是指NIO中API方法中接收字节数组作为参数的若干API
2、使用场景
- 网络交互操作中比如自定义协议:协议头-part1长度10个字节,协议头-part2长度20个字节,协议体body长度是可变的。这样场景下,可以使用Scattering,创建三个buffer,第一个buffer长度10个字节,第二个buffer长度20个字节,第三个buffer长度为从头中取出的体长度的字节数。然后通过Scattering,就可以把协议头-part1读到第一个Buffer、将协议头-part2读到第二个Buffer中。这样,好处是:天然的、自动的实现消息中每个部分的分门别类。不必只传递一个buffer,将头1、头2、体的数据都先读这一个buffer中,然后再去解析这个buffer。
- Gathering的使用场景也是如此。
3、代码示例
package com.mzj.netty.ssy._09_nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
/**
*
* 功能描述: NIO中Scattering与Gathering示例
*
* 创建个服务端用于测试,客户端向服务端写入数据,服务的通过三个buffer接收数据
*
* @Auther: mazhongjia
* @Description:
*/
public class NioTest12 {
public static void main(String[] args) throws IOException {
//1、创建服务端并绑定端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress(8899);
serverSocketChannel.socket().bind(address);
//2、定义三个buffer的长度,总共为9个字节:第一个buffer长度2,第二个长度3,第三个长度4
int messageLength = 2 +3 +4;
//3、创建三个buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[3];
//4、按照上面长度分配大小初始化buffers
byteBuffers[0] = ByteBuffer.allocate(2);
byteBuffers[1] = ByteBuffer.allocate(3);
byteBuffers[2] = ByteBuffer.allocate(4);
//5、服务端等待客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
while (true){
//一次循环读取messageLength个字节
System.out.println("【开始一次读入/写出....】");
int bytesRead = 0;
//6、3个buffer全部写满,才会退出内层循环
while(bytesRead < messageLength){
//每一次循环读取多少个字节,也就是每次打印r是多少,与是什么客户端调用有关,windows上telnet客户端调用时只要有键盘输入就会向服务端write并flush,一次只能是read到1个,ios的nc工具上按回车键才flush,一次read可以随意
long r = socketChannel.read(byteBuffers);//------------Scattering-------------
System.out.println("r..........."+r);
bytesRead += r;
System.out.println("bytesRead : " +bytesRead);//打印实际读到的字节信息
//每次读完都会打印每次读入后,三个buffer的状态(position与limit)
Arrays.asList(byteBuffers).stream().map(buffer -> "position : " + buffer.position() +",limit : "+buffer.limit()).forEach(System.out::println);
}
//7、3个buffer读满后,将buffer进行翻转,准备读取其中的数据,将数据写回客户端
Arrays.asList(byteBuffers).stream().forEach(buffer -> buffer.flip());
//8、将读到的数据写回客户端
//执行到这里时,三个buffer的已经满了
long bytesWrite = 0;
while(bytesWrite < messageLength){
System.out.println("这里应该只循环一次,三个buffer全部写出");
long r = socketChannel.write(byteBuffers);//------------Gathering-------------
bytesWrite += r;
}
//9、一次读入buffer/写出buffer后,清空buffer(将position = 0 limit = capacity mark = -1)
Arrays.asList(byteBuffers).stream().forEach(byteBuffer -> byteBuffer.clear());
}
}
}
使用telnet工具进行测试。