NIO
- Java中的IO介绍:
- BIO:BlockingIO,同步组阻塞式IO,即传统的IO,是Java最早期的流。
- NIO:Non-BlockingIO,又称NewIO,同步式非阻塞式IO,sinceJDK1.4的stream
- AIO:AsynchronousIO,异步式非阻塞式IO,可以认为式NIO的二代版本,sinceJDK1.8提供的stream
- NIO组件:Buffer(缓存),Channel,Selector
- BIO的缺点
- 阻塞:导致效率整体降低,单向流
- 一对一的连接:客户端每过来一个请求,那么在服务器端就需要有一个线程去处理这个请求。如果客户端产生大量请求,回导致服务器端也产生大量的线程去处理请求,服务器端的线程数量一旦过多,会导致服务器卡顿甚至崩溃。
- 如果客户端连接之后不产生任何操作,依然会占用服务器端的线程,则会导致服务器资源浪费。
- Buffer-缓冲区
- 作用:存储数据
- 容器:数组,针对基本类型提供了其中对应的Buffer-ByteBuffer(字符缓冲区,继承了Buffer类,底层依靠字节数组来存储数据,本身是个抽象类,需要利用其子类创建对象或者是利用提供的allocate(初始容量,固定)或者wrap方法来创建ByteBuffer对象),ShortBuffer, IntBuffer, LongBuffer, FloatBuffer,DoubleBuffer,CharBuffer
- 重要位置:capacity>=limit>=position>=mark
- capacity:容量位。用域标记缓冲区的大小,指定以后就不可变。
- limit:限制位。用域限制操作位所能达到的最大位置。缓冲区刚创建的时候,limit和capacity重合
- position:操作位。类似于数组的下标,用域执行要读写的位置。默认0
- mark:标记位。往往是确定数据没有问题的前提下进行标记。后续操作如果出错则从标记位重新开始即可。默认情况下。mark=-1。默认不启用。
- 重要操作
- flip:翻转缓冲区:将limit挪到position上,然后将position归零。mark重置为-1
public final Buffer filp(){ limit=position; position=0; mark=-1; return this; }
-
clear:清空缓冲区,回归缓冲区最开始的状态
public final Buffer clear(){ position=0; limit=capacity; mark=-1; return this; }
-
reset:重置缓冲区,将position挪到mark上
public final Buffer reset(){ int m=mark; if(m<0) throw new InvalidMarkException(); position=m; return this;}
-
rewind:重绕缓冲区,将position归零,将mark重置为1
public final Buffer rewind(){ position=0; mark=-1; return this; }
-
ByteBuffer重要方法
-
方法
作用
allocate(int capacity)
创建缓冲区的时候指定缓冲区容量的大小,实际上是指定缓冲区底层的字节数组的大小
wrap(byte[] array)
利用传入的字节数组来构建缓冲区
array()
获取缓冲区底层的字节数组
get()
获取缓冲区中position位置上的字节
get(byte[] dst)
将缓冲区中的数据写到传入的字节数组中
get(int index)
获取指定下标上的字节
put(byte b)
向position位置上放入指定的字节
put(byte[] src)
向position位置上放入指定的字节数组
put(byte[] src, int offset, int length)
向position位置上放入指定的字节数组的部分元素
put(ByteBuffer src)
将字节缓冲区放入
put(int index, byte b)
向指定位置插入指定的字节
capacity()
获取容量位
clear()
清空缓冲区:position = 0; limit = capacity; mark = -1;
flip()
反转缓冲区:limit = position; position = 0; mark = -1;
hasRemaing()
判断position和limit之间是否还有空余
limit()
获取限制位
limit(int newLimit)
设置限制位
mark()
设置标记位
position()
获取操作位
position(int newPosition)
设置操作位
remaining()
获取position和limit之间剩余的元素个数
reset()
重置缓冲区:position = mark
rewind()
重绕缓冲区:position = 0; mark = -1
-
- flip:翻转缓冲区:将limit挪到position上,然后将position归零。mark重置为-1
4.Channel-管道
- 作用:传输数据
- 分类:
- 文件-FileChannel
- UDP-DatagramChannel
- TCP-SocketChannel,ServerSocketChannel
- 通道默认式阻塞的,可以手动设置为非阻塞
- 通道可以进行双向传输
- 操作的时候是面向缓冲区进行的,在NIO中用域完成数据的传输
- FileChannel
- 概述:面向文件的通道
- 可以利用FileChannel万册灰姑娘对文件的读写操作
- 利用FileChannel读取文件的时候,是将文件中的内容映射到虚拟内存中,然后再读取到程序的缓冲区
- FileChannel不能直接创建,可以利用FileInputStream,FileOutStream,RandomAccessFile对象中的个体Channel()方法获取
- 如果是通过FileInputStream获取FileChannel,那么只能进行读取操作
-
如果是通过FileOutputStream获取FileChannel,那么只能进行写入操作
- 如果是通过RandomAccessFile获取FileChannel,那么可以进行读写操作
- 示例:
读取过程 @Test public void readFile() throws Exception { // 创建RandomAccessFile对象。指定模式为读写模式 RandomAccessFile raf = new RandomAccessFile("F:\\a.txt", "rw"); // 获取FileChannel对象 FileChannel fc = raf.getChannel(); // 创建缓冲区用于存储数据 ByteBuffer buffer = ByteBuffer.allocate(10); // 记录读取的字节个数 int len; // 读取数据 while ((len = fc.read(buffer)) != -1) { System.out.println(new String(buffer.array(), 0, len)); buffer.flip(); } // 关流 raf.close(); } 写入过程 @Test public void writeFile() throws Exception { // 创建RandomAccessFile对象。指定模式为读写模式 RandomAccessFile raf = new RandomAccessFile("F:\\test.txt", "rw"); // 获取FileChannel对象 FileChannel fc = raf.getChannel(); // 创建缓冲区,并且将数据放入缓冲区 ByteBuffer src = ByteBuffer.wrap("hello".getBytes()); // 利用通道写出数据 fc.write(src); // 关流 raf.close(); } 复制文件 @Test public void copyFile() throws Exception { // 创建流对象指向对应的文件 FileInputStream in = new FileInputStream("F:\\a.txt"); FileOutputStream out = new FileOutputStream("E:\\a.txt"); // 获取FileChannel对象 FileChannel src = in.getChannel(); FileChannel dest = out.getChannel(); // 准备缓冲区 ByteBuffer buffer = ByteBuffer.allocate(10); // 读取数据,将读取到的数据写出 while (src.read(buffer) != -1) { buffer.flip(); dest.write(buffer); buffer.clear(); } // 关流 in.close(); out.close(); }
-
UDP
-
概述:
-
用域进行UDP收发的通道
-
是无连接的网络协议,只能进行发送和接受的操作
-
基本类是DatagramChannel,是一个抽象
-
示例:
发送端 @Test public void send() throws IOException { // 开启通道 DatagramChannel dc = DatagramChannel.open(); // 准备数据 ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes()); // 发送数据 dc.send(buffer, new InetSocketAddress("localhost", 8090)); // 关闭通道 dc.close(); } 接收端 @Test public void recieve() throws IOException { // 开启通道 DatagramChannel dc = DatagramChannel.open(); // 绑定连接地址和端口号 dc.socket().bind(new InetSocketAddress(8090)); // 准备缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); // 接收数据 dc.receive(buffer); System.out.println(new String(buffer.array(), 0, buffer.position())); // 关闭通道 dc.close(); }
-
-
-
TCP
-
概述
-
用域进行TCP通信的通道
-
需要进行链接的网络协议
-
提供了链接,接受,读取,写入操作
-
客户端通道时SocketChannel,服务器端通道时ServerSocketChannel
-
-
示例:
客户端 @Test public void client() throws IOException { // 开启客户端通道 SocketChannel sc = SocketChannel.open(); // 可以手动设置为非阻塞模式 sc.configureBlocking(false); // 发起连接 sc.connect(new InetSocketAddress("localhost", 8090)); // 由于是非阻塞的,所以连接不一定建立了,所以要判断连接是否建立 while (!sc.isConnected()) // 如果连接没有建立,则试图重新建立连接 sc.finishConnect(); // 写出数据 sc.write(ByteBuffer.wrap("hello".getBytes())); // 关闭通道 sc.close(); } 服务器端 @Test public void server() throws IOException { // 开启服务器端通道 ServerSocketChannel ssc = ServerSocketChannel.open(); // 绑定要监听的端口 ssc.socket().bind(new InetSocketAddress(8090)); // 手动设置为非阻塞 ssc.configureBlocking(false); // 获取连接过来的通道 SocketChannel sc = ssc.accept(); // 判断连接是否真正建立 while (sc == null) // 如果没有建立则重新获取建立 sc = ssc.accept(); // 准备缓冲区用于存储数据 ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取数据 sc.read(buffer); System.out.println(new String(buffer.array(), 0, buffer.position())); // 关闭通道 ssc.close(); }
-
5.Selector-多路复用选择器
- 对通道进行选择,需要级域时间进行驱动
- 针对了四类时间:connect,accept,read,write,四类时间定义在Selectionkey中
- 可以实现利用一个或者少量线程的大量请求
- 适用于大量的短任务场景,不适用于长任务场景
- Selector针对的必须是非阻塞的通道
- 示例:
@Test public void server() throws IOException { // 开启服务器端的通道 ServerSocketChannel ssc = ServerSocketChannel.open(); // 设置为非阻塞 ssc.configureBlocking(false); // 绑定要监听的端口 ssc.socket().bind(new InetSocketAddress(8090)); // 开启选择器 Selector selector = Selector.open(); // 将当前通道注册到选择器上,并且为通道注册事件 ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { // select()是Selector提供的用于查询是否有事件触发的机制 // 如果没有事件触发,则会一直阻塞在这儿,如果有事件触发,则阻塞放开 // 引入Selector的好处是:线程不必每时每刻都去工作、去查询客户端是否有新事件,没有事件就一直阻塞 // 有事件发生,Selector则放开阻塞。这样一来,可以避免了线程无意义的空转,节省cpu资源,同时也不影响工作 selector.select(); // 能走到下面的代码,说明有事件发生,需要进行处理 // 在处理的时候需要根据事件类型来做相应的处理 // 对于服务端来说,事件分为: // SelectionKey.OP_ACCPET 新客户端接入 // SelectionKey.OP_WRITE 已接入的客户端给服务端传数据 // SelectionKey.OP_READ 服务端给已接入的客户端传数据 Set<SelectionKey> set = selector.selectedKeys(); // 迭代遍历进行处理 Iterator<SelectionKey> it = set.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); // 判断是否是一个可接受事件 if (key.isAcceptable()) { // 从事件中获取到通道 ServerSocketChannel ss = (ServerSocketChannel) key.channel(); // 接受事件 SocketChannel sc = ss.accept(); sc.configureBlocking(false); sc.register(selector, SelectionKey.OP_READ); } // 判断是否是一个可读事件 if (key.isReadable()) { SocketChannel s = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); s.read(buffer); // 处理完read事件后,需要把read事件从当前的SelectionKey键集里删除,避免重复处理 System.out.println("服务端接收到信息:" + new String(buffer.array())); s.register(selector, key.interestOps() ^ SelectionKey.OP_READ); } // 判断是否是一个可写事件 if (key.isWritable()) { SocketChannel s = (SocketChannel) key.channel(); s.write(ByteBuffer.wrap("hello client".getBytes())); s.register(selector, key.interestOps() ^ SelectionKey.OP_WRITE); } // 防止已处理完毕的SelectionKey再次被处理 it.remove(); } } }
6.常见通信框架:
1. Mina
a. 主导作者:Trustin Lee(韩国)
b. Mina(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架
c. 当前发行的 Mina 版本2.04支持基于 Java NIO 技术的 TCP/UDP 应用程序开发、串口通讯程序
d. 目前,正在使用Mina的应用包括:Apache Directory Project、AsyncWeb、AMQP(Advanced Message Queuing Protocol)、RED5 Server(Macromedia Flash Media RTMP)、ObjectRADIUS、 Openfire等等
2. Netty
a. 主导作者:Trustin Lee(韩国)
b. Netty是一款异步的事件驱动的网络应用框架和工具,用于快速开发可维护的高性能、高扩展性协议服务器和客户端
c. Netty是一个NIO客户端/服务器框架,支持快速、简单地开发网络应用,如协议服务器和客户端
d. 它极大简化了网络编程,如TCP和UDP套接字服务器
3. Grizzly
a. Grizzly是一种应用程序框架,专门解决编写成千上万用户访问服务器时候产生的各种问题
b. Grizzly使用JAVA NIO作为基础,并隐藏其编程的复杂性,提供了容易使用的高性能的API
c. Grizzly利用了非阻塞socket到协议处理层,使用了高性能的缓冲和缓冲管理使用高性能的线程池
三、Netty简介
1. Netty是目前最流行的由JBOSS提供的一个Java开源框架NIO框架
2. Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序
3. 相比JDK原生NIO,Netty提供了相对十分简单易用的API,非常适合网络编程
4. Netty是完全基于NIO实现的
5. 作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果
6. Netty的优点:
a. API使用简单,开发门槛低
b. 功能强大,预置了多种编解码功能,支持多种主流协议
c. 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展
d. 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优
e. 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼
f. 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入
g. 经历了大规模的商业应用考验
7. 与Mina相的比较:
a. 都是Trustin Lee的作品,Netty更晚
b. Mina将内核和一些特性的联系过于紧密,使得用户在不需要这些特性的时候无法脱离,相比下性能会有所下降,Netty解决了这个设计问题
c. Netty的文档更清晰,很多Mina的特性在Netty里都有
d. Netty更新周期更短,新版本的发布比较快
e. 它们的架构差别不大,Mina靠apache生存,而Netty靠jboss,和jboss的结合度非常高,Netty有对google protocal buf的支持,有更完整的ioc容器支持(spring,guice,jbossmc和osgi)
f. Netty比Mina使用起来更简单,Netty中可以自定义的处理upstream events或者downstream events,可以使用decoder和encoder来解码和编码发送内容
g. Netty和Mina在处理UDP时有一些不同,Netty将UDP无连接的特性暴露出来;而Mina对UDP进行了高级层次的抽象,可以把UDP当成"面向连接"的协议,而要Netty做到这一点比较困难
h. 从任务调度粒度上看,Mina会将有IO任务的session写入队列中,当循环执行任务时,则会轮询所有的session,并依次把session中的所有任务取出来运行。这样粗粒度的调度是不公平调度,会导致某些请求的延迟很高