1)传统的IO是面向,流,的
2)流在底层输入与输出都是传输的字节,字符流只是上层的封装
3)OIO中流要么是输入流,要么是输出流,不可能即是输入流又是输出流
分类方式1:输入流、输出流
输入、输出是相对于应用程序来说的,应用程序向外传递数据为输出,外部向应用程序传递数据为输入
分类方式2:节点流、过滤流
节点流:从特定的地方读写的流,例如:磁盘或一块内存区域
过滤流:使用节点流作为输入或输出,过滤流是使用一个已经存在的输入流或输出流连接创建的,过滤流一定是依赖一个节点流的
其中蓝色为节点流、紫色为过滤流
装饰模式说明
装饰器模式扩展的是:对象功能、动态的
继承 扩展的是:类的功能、静态的
- 装饰模式以对客户(使用这个对象的一方)透明的方式动态的给一个对象附加上更多的功能,客户端并不会觉得对象在装饰前和装饰后有什么不同。
- 装饰模式可以在不创建更多子类的情况下,将对象的功能加以扩展。
- 装饰模式把客户端的调用,委派到被装饰类。装饰模式的关键在于这种扩展完全是透明的。
- 装饰模式是在不必改变原类文件和使用继承的情况下,动态扩展一个对象的功能。他是通过创建一个包装对象,也就是装饰来包裹真实的对象
装饰模式的角色
1、抽象构件角色(Component):给出一个抽象接口,以规范准备接收附加责任的对象。
- OIO中对应InputStream
2、具体构件角色(Concrete Component):定义一个将要接收附加责任的类
- OIO中对应FileInputStream
3、装饰角色(Decorator):持有一个构件对象的引用,并定义一个与抽象构件接口一致的接口
- OIO中对应FilterInputStream
4、具体装饰角色(Concrete Decorator):负责给构件对象贴上附加的责任(功能)。
- OIO中对应BufferedInputStream
装饰模式的特点
- 装饰对象和真实对象有相同的接口。这样客户端对象就可以以和真实对象相同的方式调用装饰对象。
- 装饰对象包含了一个真实对象的引用
- 装饰对象接收所有来自客户端请求,把请求转发给真实对象
- 装饰对象可以在转发前 或者 后,增加一些附加功能。这样就确保了在运行时,不用修改给定对象的结构,就可以在外部增加附加的功能。在面向对象的设计中,通常是通过继承来实现对给定类的功能扩展。
举例:
其他角色略。
具体装饰角色:
客户端调用:
装饰模式与继承比较
- 用来扩展特点对象的功能
- 不需要子类
- 动态
- 允许时增加功能
- 防止由于子类而导致的复杂和混乱
- 更多的灵活性
- 对于一个给定的对象,同时可能有不同的装饰对象,客户端可以通过他的需要选择合适的装饰对象
- 用来扩展类的功能
- 需要子类
- 静态
- 编译时增加功能
- 导致很多子类产生
- 缺乏灵活性
装饰模式的适用场景
- 想要透明并且动态的给对象增加新的功能而又不会影响其他对象
- 给对象增加的功能,在未来可能会发生改变
- 用子类(继承)扩展功能不实际的情况下
Jdk IO库使用装饰器模式带来的好处
- 即满足IO体系对输入输出流增加功能的要求
- 在运行期动态增加功能,避免继承方式会造成的类的数量庞大
1、Selector:选择器
2、Channel:通道
3、Buffer:缓冲区
OIO是面向【流stream】进行编程
NIO是面向【块block】编程,buffer本身就是一块内存,底层实现上,上一个数组,数据的读、写都是通过buffer实现的
与Stream不同的是,Channel是双向的
- 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里取出上述类型的数据。
注意:按照什么顺序放入什么类型数据,取出时就需要使用相同的顺序
输出:
11、分割、分片ByteBuffer(ByteBuffer的slice方法)
作用:返回原Buffer的部分数据(底层数据是一份,修改其中Buffer的数据会对另一个Buffer产生影响,position、limit、mark、capacity是各自的)
ByteBuffer的slice方法:创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。 新缓冲区的内容将从此缓冲区的当前位置(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、新创建
默认创建的是:HeapByteBuffer(堆ByteBuffer)
2、put操作
我们将代表“abcde”字符串的 ASCII 码载入一个名为 buffer 的ByteBuffer 对象中。当在图1 中所新建的缓冲区上执行以下代码后。
缓冲区的结果状态如图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() 方法前,缓冲区如下:
执行:
执行后:
可见,压缩后:
- 已经消费完的数据(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操作,仍然可以将数据获取出来。直观的话,可以直接看源代码:
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指的是,可以向其写入数据或是从中读取数据的对象,类似于IO中stream,但是读写操作都需要通过buffer
- 由于Channel是双向的,因此它能更好的反映出底层操作系统的真是情况:在linux系统中,底层操作系统的通道就是双向的
- Channel指的是,可以向其写入数据或是从中读取数据的对象,类似于IO中stream,但是读写操作都需要通过buffer
开胃示例1:
输出:
开胃示例2:
输出:
ServerSocketChannel
定义:服务器套接字通道Channel对象
SocketChannel
定义:客户端套接字通道Channel对象,代表服务端与客户端连接的通道对象
1、传统的网络编程
传统的网络编程服务端:1客户端1线程的线程模型缺点
客户端连接过多时,一个线程对应一个客户端的方式消耗CPU资源,使得服务端无法支持更多客户端连接。
特殊说明
服务端供客户端连接的端口号,比如8899,客户端连接接后,之后进行的IO操作,其实是在服务端再随机分配一个未被占用的端口进行通信;这个服务端的8899端口仅用于客户端建立连接使用,而后续的数据发送,不是在这个端口号上
2、NIO中 基于Selector的 网络线程模型
1)特点:
NIO编程使得服务端使用一个线程处理可以处理与N多客户端的数据交互。
NIO编程模型特别适用于:客户端连接数非常多,但是消息量不是特别大,这种情况使用NIO编程性价比最高。
Selector官网文档说明(我整理后):
SelectionKey官网文档说明(我整理后):
2)示例代码:
运行示例:启动后使用telnet工具测试,连接服务端后,向服务端发送数据,服务端将相同的内容返回给客户端。
3)下面通过一个更加完整的NIO Server+Client示例,目的是学习NIO编程的一般模式
同时体会NIO编程与普通的IO编程上的区别.
server:
client:
1、内存映射文件
1、说明
- 所谓内存映射文件,是指把文件的全部或者部分内容映射到内存中,只操作内存就可以完成对文件的操作,不需要直接操作文件。
- 对内存的操作都会反映到硬盘文件中(由操作系统保证)
- 用于内存映射文件的内存,位于JVM堆外
- 应用程序只需要处理内存的数据,可以实现非常快的IO操作
- 内存映射文件最多支持java整形int 最大值长度的字节数,也就是2GB文件大小,2147483647个字节
原文件内容:
运行后文件内容:
2、文件锁
1、说明
文件锁类型:
- 共享锁:读锁
- 排他锁:写锁
共享锁与排他锁交替使用共享资源的逻辑关系:
- 一个JVM如果获取了共享锁(未释放),另一个JVM可以继续获取共享锁
- 一个JVM如果获取了共享锁(未释放),另一个JVM不可以继续获取排他锁
- 一个JVM如果获取了排他锁(未释放),另一个JVM不可以继续获取共享锁
- 一个JVM如果获取了排他锁(未释放),另一个JVM不可以继续获取排他锁
2、使用场景
文件锁实际工作中用的较少
3、使用
执行结果:
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、代码示例