NIO
1、IO模型
目前IO模型实际指数据进行收发的通道模型,常见磁盘IO、网络IO等,是互联网发展到现在各架构体系中最常讨论的话题之一。因为IO的瓶颈限制尝尝导致开发者们不得不通过扩容来解决,而好的IO模型无疑能减少因IO瓶颈而造成的资源浪费。
JAVA目前支持的3中网络变成IO模型:BIO、NIO、AIO
2、BIO(blocking io)
2.1 BIO介绍
同步或异步阻塞式收发模型,客户端的请求需要服务端线程等待处理。
- 同步阻塞模型:则现成阻塞其他客户端的连接,直至完成当前连接的accept()连接事件或read()读写事件。
- 异步阻塞模型:常见于在服务端开启线程池,主线程用于接收客户端的accept()连接事件,再有线程池处理客户端的读写事件。但当客户端连接过多,或长连接过多时,服务端仍无法满足非阻塞的需求,而只能让溢出的连接保持阻塞等待,直至超时或中断放弃连接。
2.2 源码使用demo
//BIO客户端Demo
public class BioSocketClient {
public static void main(String[] args) throws IOException {
Socket client = new Socket("127.0.0.1",9090);
client.getOutputStream().write("hello".getBytes(StandardCharsets.UTF_8));
client.getOutputStream().flush();
System.out.println("向服务端发送数据");
byte[] bytes = new byte[1024];
//接收服务端的响应
client.getInputStream().read(bytes);
System.out.println("接收服务端数据:"+ new String(bytes));
client.close();
}
}
//服务端阻塞等待连接
public class BioSocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9090);
while (true){
System.out.println("等待客户端连接...");
//accept 阻塞方法
Socket client = serverSocket.accept();
System.out.println("有客户端完成连接");
new Thread(new Runnable() {
@Override
public void run() {
try {
handleRequest(client);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
//服务端阻塞等到客户端的读事件
public static void handleRequest(Socket socket) throws IOException {
byte[] bytes = new byte[1024];
System.out.println("阻塞读取客户端消息");
//read 阻塞方法,接受客户端发送的数据
int read = socket.getInputStream().read(bytes);
System.out.println("read数据完成");
if(read != -1){
System.out.println("客户端消息:"+ new String(bytes,0,read));
}
//处理服务器的响应
socket.getOutputStream().write("hello client".getBytes(StandardCharsets.UTF_8));
socket.getOutputStream().flush();
}
}
2.3 BIO优缺点及应用场景分析
2.3.1 缺点
- accept()、read()都是阻塞操作,在连接后不做读写会导致线程资源阻塞浪费
- 当需要更多的连接时,线程数、内存都要飙升,服务端压力很大,常见client10000的C10k问题
2.3.2 使用场景
BIO适用于连接数少且连接稳定的场景,对服务器的压力小
3、NIO
3.1 NIO介绍
NIO被称为Non Blocking io 或new io。可以实现非阻塞式请求,通过将客户端发送的连接注册到多路复用器(selector),保证一个线程可以处理多个连接的连接及读写事件。
3.2 NIO线程模型
应用:目前的nio已被广泛应用,技术路线可见
Nio —> Netty —> vert等等,在产品应用中也非常广泛。
3.2 Nio-client/server module
在上图Nio异步非阻塞模型中引入了多个概念,SocketChannel(通道)、ServerSocketChannel、Selector(多路复用器)、SelectorKeys、Buffer(缓冲区)等,我们先行使用基本功能,构建简单的客户端、服务端交互代码,随后对其概念及源码做剖析。
客户端:
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.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
public class NioClient {
//通道管理器-> 多通道复用
private Selector selector;
public static void main(String[] args) throws IOException {
NioClient nioClient = new NioClient();
nioClient.initClient();//初始化建立连接
nioClient.connect();//连接后读和发送消息
}
public void initClient() throws IOException {
SocketChannel channel = SocketChannel.open();
//channel通道设置为非阻塞式
channel.configureBlocking(false);
this.selector = Selector.open();
//配置服务器地址
channel.connect(new InetSocketAddress("127.0.0.1",9090));
//将通道管理和通道绑定,并且监听连接事件
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
* 监听建连事件,发生交互
*/
public void connect() throws IOException {
while (true){
//非阻塞式等待事件驱动
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = (SelectionKey) iterator.next();
iterator.remove();
//连接事件 -> 发送消息给客户端
if (key.isConnectable()){
SocketChannel channel1 = (SocketChannel) key.channel();
//事件监听结果为正在连接,设置为连接完成
if (channel1.isConnectionPending()){
channel1.finishConnect();
}
channel1.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.wrap("hello server".getBytes(StandardCharsets.UTF_8));
channel1.write(buffer);
//客户端与服务器建立连接后,监听读写事件
channel1.register(selector,SelectionKey.OP_READ);
}else if (key.isReadable()){
//读事件 -> 接收服务器响应内容
SocketChannel channel = (SocketChannel) key.channel();
//创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read != -1){
System.out.println("客户端接受消息:"+ new String(buffer.array()));
}
}
}
}
}
}
服务端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NioSelectorServer {
public static void main(String[] args) throws IOException {
//创建Nio channel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9090));
//将服务侧channel设置为非阻塞式
serverSocket.configureBlocking(false);
//打开Selector 处理channel
Selector selector = Selector.open();
//将channel注册到Selector上,并由selector监听accept()事件
SelectionKey selectionKey = serverSocket.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("服务器启动并监听accept事件成功");
while (true){
//阻塞等待需要处理的事件
selector.select();
// 获取selector中注册的全部事件 selectionKey实例
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeySet.iterator();
//遍历KeySet对事件进行处理
while (iterator.hasNext()){
SelectionKey next = iterator.next();
//监听到发生了OP_ACCEPT事件
if (next.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) next.channel();
SocketChannel accepted = channel.accept();
//设置非阻塞式读事件监听
accepted.configureBlocking(false);
accepted.register(selector,SelectionKey.OP_READ);
System.out.println("客户端连接成功");
}else if (next.isReadable()){
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int read = channel.read(byteBuffer);
//有数据则读取并打印
if (read > 0){
System.out.println("接收数据:"+new String(byteBuffer.array()));
}else if (read == -1){
System.out.println("客户端断开连接");
channel.close();
}
}
//完成连接事件或读时间 从select中移除
iterator.remove();
}
}
}
}
3.4 Nio及EPoll源码解析
3.4.1 JVM跨平台特性
针对不同操作系统,hotspot源码分别不同的类进行处理,得以实现jvm跨平台的能力。如图DefaultSelectorProvider在Windows、macosx、solaris(SunOS、Linux)等有不同的实现。
3.4.2 核心代码
Selector.open() //创建多路复用器
socketChannel.register() //将channel注册到多路复用器上
selector.select() //阻塞等待需要处理的事件发生
- SocketChannel 通道,java调用linux内核函数,创建客户端或服务端在linux系统中的socket文件描述器fd。
- Selector 多路复用器,实际是用于封装linux系统epoll文件描述器的java对象。
- 将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上,进行事件的异步通知
3.4.3 linux内核函数
linux内核的epoll_create(256)函数
linux内核的epoll_ctl()函数。linux内核将监听新的fd(socket)的ADD、MODIFY、DELETE事件
linux内核的epoll_wait()函数。linux内核查询epoll的就绪列表rdlist,等待事件发生。