一、Java NIO
1.为什么要有NIO
1.基于java.net.ServerSocket包和java.net.Socket包下的API封装的是同步阻塞IO模型
2.但是Java.nio包下封装的API是支持同步非阻塞IO模型以及IO多路复用模型
2.java NIO概述
1.NIO称为New IO,也可以成为非阻塞IO
2.三大组件:Channel(通道)、Buffer(缓冲区)、Selector(选择器/多路复用器)
二、Channel
1.什么是Channl
1.Channel(通道):就是一个数据的通道,通过Channel对数据的读写
2.Channel与传统的流不同之处在于Channel是双向的,即Channel是全双工模式
2.传统的流如下图:
3.Channel流如下图
4.Channel实现体系
可分为网络读写Channel和文件读写Channel
5.Channel关系图
1.ServerSocketChannel底层代表的是一个服务器的Socket
2.SocketChannel底层代表了一个客户端Socket
注意:ServerSocketChannel与SocketChannel是网络读写,fileChannel是文件读写
6.网络Channel的核心功能
1.ServerSocketChannel:绑定端口,开启监听,接收连接
2.SocketChannel:连接服务端,进行读写操作
三、Buffer
1.什么是Buffer
1.是一个缓冲区,也是一个容器,在java NIO中数据都是用Buffer存储的
2.通过Channel读取接收的数据先写到Buffer中,应用程序在从Buffer中读取
3.通过Channel写数据也是先将数据写到Buffer中,再将Buffer交给Channel,Channel读取然后写到网络中
2.图示:
2.Buffer集成关系图示:
注意:ByteBuffer是最受欢迎的
3.Buffer如何创建
ByteBuffer是一个抽象类,提供了静态工厂方法来创建一个实例
3.1:allocation方法
1.使用allocate方法,我们将得到一个非直接缓冲区
2.真实创建的是一个HeapByteBuffer的实例
3.数据是存储在堆区按容量开辟空间的字节数组中
allocate(int capacity):
3.2:allocateDirect方法
1.使用allocateDirect方法,我们将得到一个直接缓冲区
2.真实创建的是一个DirectByteBuffer的实例
3.数据是存储在一块操作系统直接内存中(堆外)
4.DirectByteBuffer中持有内存地址用以操作这块内存
allocateDirect(int capacity)
4.ByteBuffer如何读取数据
不管buffer的数据真实存储在哪,对于数据的操作行为是一样的,在Buffer类中定义了四个索引,这些索引记录了底层数据元素的状态
capacity:缓冲区可以容纳的数据元素的最大数量
limit:停止读写的索引
position:当前要读或写的索引,读写操作是从position开始,到limit停止
mark:可用于记录position位置,默认-1
4.1:图示
5.ByteBuffer操作索引可以分为4类
6.ByteBuffer读写API
1,向ByteBuffer 中写数据:核心方法是put
put(byte[] src)
put(byte[] src, int offset, int length)
put(ByteBuffer byteBuffer)
2,从ByteBuffer中读数据:核心方法是get
get(byte[] dst)
get(byte[] dst, int offset, int length)
6.1put(byte[] src)
6.2put(byte[] src, int offset, int length)
6.3put(ByteBuffer byteBuffer)
6.4get(byte[] dst)
6.5get(byte[] dst, int offset, int length)
四、Selector
1.什么是Selector
Selector是多路复用器,也可以说是多路选择器,底层封装的是多路IO模型,在linux上采用的是epoll
2.核心功能
1.检测:将Channel注册到selector上并指定要监听的事件,selector会不断轮询注册在其上的Channel,检测其事件是否就绪
2.获取key:Selector每次返回已就绪Channel对应的SelectionKey,通过SelectionKey可获取事件信息,然后进行后续操作
3.工作流程
1.封装IO多路复用的相关操作,采用的是epoll实现,主要用于检测Channel事件的就绪状态
2.将channel注册给selector并指定监听的事件,
3.selector会不断轮询检测,当channel就绪后会返回该channel
4.图示
五、基于JAVA NIO 编写服务端程序
1.核心逻辑分析
1,创建ServerSocketChannel,绑定地址端口,设置阻塞标识
2,打开一个Selector,将ServerSocketChannel注册到selector上,监听OP_ACCEPT事件
3,在一个loop中执行selector的select
4,根据返回的SelectionKey信息判断事件类型,根据事件类型完成不同操作
5,如果是ACCEPT事件则accept一个客户端连接SocketChannel,设置阻塞标识并将其注册到selector上,监听OP_READ事件
6,如果是读写事件则进行读写操作
2.案例的核心要点
1,ServerSocketChannel绑定地址端口开启监听并注册到selector
2,selector轮询执行select,然后根据返回的SelectionKey进行分支逻辑
3,读写数据时基于Buffer操作
3.代码如下:
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.util.Iterator;
import java.util.logging.Logger;
/**
* @author Andy
* NIO 服务端程序
*/
public class NIOServerDemo {
public static final Logger log= Logger.getLogger(NIOServerDemo.class.getName());
public static void main(String[] args) throws IOException {
// 创建ServerSocketChannel,,
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定地址端口
serverSocketChannel.bind(new InetSocketAddress(9999));
// 设置非阻塞标识
serverSocketChannel.configureBlocking(false);
// 打开一个Selector,,监听OP_ACCEPT事件
Selector selector = Selector.open();
// 将ServerSocketChannel注册到selector上
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 在一个loop中执行selector的select
while (true) {
// select就是检测是否有就绪的事件
int select = selector.select();
// 有多少事件已经就绪
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 处理key
processSelectedKey(key,selector);
}
}
}
private static void processSelectedKey(SelectionKey key, Selector selector) throws IOException {
// 判断key是否有效
if (key.isValid()) {
// 监听类型
if (key.isAcceptable()) {
// 获取服务端ServerSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
// 获取客户端SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
// 设置非阻塞的
socketChannel.configureBlocking(false);
// 监听读数据
socketChannel.register(selector,SelectionKey.OP_READ);
}
// 读取类型
if (key.isReadable()) {
// 获取客户端SocketChannel
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
// 有数据
if (read > 0){
// 读写模式切换flip
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
// 获取数据
byteBuffer.get(bytes);
// 解码
String s = new String(bytes, "UTF-8");
log.info("收到客户端数据"+s);
// 给客户端发送数据
byte[] serverByte = "我是服务端:已收到客户端数据".getBytes();
ByteBuffer serverByteBuffer = ByteBuffer.allocate(serverByte.length);
serverByteBuffer.put(serverByte);
serverByteBuffer.flip();
socketChannel.write(serverByteBuffer);
}
}
}
}
}
4.测试
4.1服务端
4.2客户端
六、Reactor线程模型
1.NIO编写的服务端性能瓶颈分析
1.服务端单线程selector轮询后处理了所有的事情,包括接收连接,读写数据,业务处理,职责不清晰
2.根源:线程模型没有划分好
2线程模型指导思想
Reactor线程模型,是一种并发编程线程模型,一种具有指导意义的编程思想,并非java专属
3.目前对应哪种reactor模型(单线程)
Reactor单线程模型:接收连接,IO处理,业务处理均在一个线程中完成
4.单线程图示
5.优化思路(多线程)
1.将复杂且耗时的数据编解码及业务处理独立出来,扔给业务线程池来进行处理
2.Reactor多线程模型:连接的接收和IO处理在reactor线程中完成,编解码和业务处理提交给业务线程
6.多线程图示
7.单线程与多线程的总结
Reactor单线程模型的弊端:
1、职责不清晰,接收连接,IO操作,业务处理均在一个线程中完成
2、性能低下,无法支撑高并发场景
Reactor多线程模型的特点
1、将编解码和业务处理剥离到单独的线程池中执行
2、一定程度提升了性能
8.如何优化多线程模型 —> 主从Reactor多线程模型工作模式
1.思路:接收连接和IO处理进一步切分,分别用单独的线程处理
2.主从Reactor多线程模型:mainReactor线程只负责连接的接收,subReactor线程负责IO处理,编解码等业务操作提交给业务线程进行处理,subReactor线程可以根据需要有多个。
主从多线程图示:
9.主从线程模式如何划分
1,1个MainReactor线程负责接收连接,并将连接交给其中一个subReactor线程
2、subReactor线程负责检测事件和IO操作,并将读到的数据提交给工作线程进行业务处理
3,工作线程负责数据的编解码及业务处理
10.主从Reactor多线程的优劣
优势:
1,分工明确:MainReactor 线程与 SubReactor 线程的职责分工明确,MainReactor 线程只需要接收新连接,SubReactor 线程完成事件检测和IO操作,工作线程完成具体业务处理
2,交互简单:MainReactor 线程与 SubReactor 线程的数据交互简单, MainReactor 线程只需要把新连接传给SubReactor 线程,SubReactor 线程无需返回数据
3,多个SubReactor 线程能够应对更高的并发请求
优劣:编程复杂度较高
11.主从模式的总结
这种模式也被叫做服务器的 1+M+N 线程模式,即使用该模式开发的服务器包含一个(或多个,一般来说只需要一个)连接建立线程+M 个 IO 线程+N 个业务处理线程。这是业界成熟的服务器程序设计模式,由于其优点明显,在许多项目中被广泛使用,包括 Nginx、Netty 等
12.代码
代码正在加急编写中ing…写完会上传…
1,基于JAVA NIO API 实现一个服务端应用程序,在9999端口开启服务
2,服务端要求基本满足主从reactor多线程模式