文章目录
参考文章
- http://www.jasongj.com/java/nio_reactor/
I/O模型基本说明
同步 vs. 异步
同步I/O 每个请求必须逐个地被处理,一个请求的处理会导致整个流程的暂时等待,这些事件无法并发地执行。用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行。
异步I/O 多个请求可以并发地执行,一个请求或者任务的执行不会导致整个流程的暂时等待。用户线程发起I/O请求后仍然继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞 vs. 非阻塞
阻塞 某个请求发出后,由于该请求操作需要的条件不满足,请求操作一直阻塞,不会返回,直到条件满足。
非阻塞 请求发出后,若该请求需要的条件不满足,则立即返回一个标志信息告知条件不满足,而不会一直等待。一般需要通过循环判断请求条件是否满足来获取请求结果。
需要注意的是,阻塞并不等价于同步,而非阻塞并非等价于异步。事实上这两组概念描述的是I/O模型中的两个不同维度。
同步和异步着重点在于多个任务执行过程中,后发起的任务是否必须等先发起的任务完成之后再进行。而不管先发起的任务请求是阻塞等待完成,还是立即返回通过循环等待请求成功。
而阻塞和非阻塞重点在于请求的方法是否立即返回(或者说是否在条件不满足时被阻塞)。
- I/O模型简单的理解,就是用什么样的通道进行数据的发送和接受,很大程度上决定了程序通信的性能
- Java支持三种网络编程模型:BIO、NIO、AIO
- BIO:同步并阻塞,服务器实现为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
- NIO:同步非阻塞,即服务器实现模式为一个线程处理多个请求,将客户端发送请求注册到多路复用上,多路复用器轮询到连接有I/O请求就进行处理。
- AIO:异步非阻塞
BIO
适用于连接数比较小且固定的架构
package io.netty.myexample.bio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws IOException {
ExecutorService executorService = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(555);
System.out.println("服务器启动");
while (true) {
Socket accept = serverSocket.accept();
System.out.println("创建一个连接");
executorService.execute(new Thread(() -> handler(accept)));
}
}
public static void handler(Socket socket) {
byte[] bytes = new byte[1024];
try (InputStream inputStream = socket.getInputStream()) {
while (true) {
//可能会阻塞
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println(new String(bytes, 0, read));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
BIO问题分析
- 每个请求都会创建独立的线程
- 并发数比较大时,需要创建大量线程来处理连接,占用系统资源
- 连接建立后,如果没有数据可read/write,线程会被阻塞
NIO
- 全程java non-blocking IO ,同步非阻塞
- 相关包在java.nio以及子包下
- 三大核心:Channel、Buffer、Selector
- NIO是面向缓冲区,数据在缓冲区可以移动,增加其灵活性,提供非阻塞式的高伸缩性网络
NIO和BIO的比较
- BIO是以流的方式处理数据,NIO是以块的方式处理数据
- BIO是阻塞的,NIO是非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基础channel、buffer进行操作,数据总是从通道进入缓冲区,或者从缓存区写入通道。selector用于监听多个通道的事件,因此单个线程就可以监听多个客户端通道。
三大核心组件关系
- 每个buffer都会对应一个channel
- selector对应一个线程,一个线程对应多个chananel
- 程序切换到哪个channel是由事件决定的
- Buffer就是一个内存块,底层是数组
- 数据读取写入都是通过Buffer,NIO是可以双向的,不同于字节流和字符流。
Buffer
- 本质上是一个可以读写数据的内存块,可以理解是一个容器对象,提供一组方法,可以更轻松使用内存块。
- BUffer提供了Byte(常用)、short、char、int、long、doule、float
public static void main(String[] args) {
IntBuffer intBuffer = IntBuffer.allocate(5);
intBuffer.put(10);
intBuffer.put(11);
intBuffer.put(12);
intBuffer.put(13);
//读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
Buffer基本属性
Buffer 中有 4 个非常重要的属性:capacity、limit、position、mark 。代码如下:
读写复用这些标记
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
// Netty就处理的比较好 : 0 <= readerIndex <= writerIndex <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
// ... 省略具体方法的代码
}
MappedByteBuffer
public static void main(String[] args) throws IOException {
//直接在内存种修改(堆外内存)
RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\a.txt", "rw");
FileChannel channel = randomAccessFile.getChannel();
// 使用读写模式 可以直接修改的起始位置 映射内存大小
// MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
mappedByteBuffer.put(5, (byte) '9');
randomAccessFile.close();
}
Buffer数组
public static void main(String[] args) throws IOException {
//scatter 将数据写入buffer 可以写入buffer数组 依次写入
//gather 依次读取buffer数组
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
serverSocketChannel.socket().bind(inetSocketAddress);
//服务端 buffer
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(3);
byteBuffers[1] = ByteBuffer.allocate(5);
SocketChannel socketChannel = serverSocketChannel.accept();
while (true) {
int read = 0;
if (read < 8) {
//数据从客户端写入 服务端的buffer
long read1 = socketChannel.read(byteBuffers);
read += read1;
}
Stream.of(byteBuffers).forEach(Buffer::flip);
long byteWrite = 0;
while (byteWrite < 8) {
// 服务端的buffer 写入channel
long write = socketChannel.write(byteBuffers);
byteWrite += write;
}
Stream.of(byteBuffers).forEach(Buffer::clear);
}
}
Channel
- 通道可以同时进行读写
- 可以异步读取写数据
- 可以从缓存读数据,也可以写数据到缓存
子类
- SocketChannel :一个客户端用来发起 TCP 的 Channel 。
- ServerSocketChannel :一个服务端用来监听新进来的连接的 TCP 的 Channel 。对于每一个新进来的连接,都会创建一个对应的 SocketChannel 。
- DatagramChannel :通过 UDP 读写数据。
- FileChannel :从文件中,读写数据。
Selector
- 能检测多个注册通道上是否有事件发送,如有获取事件进行相应处理,避免线程之间的开销
- 相关方法说明
- select() 阻塞
- select(long) 阻塞多少毫秒
- wakeup() 唤醒selector
- selectNow() 不阻塞 立马返回
- selector.keys 返回当前所有注册在selector中channel的selectionKey
- selector.selectedKeys() 返回注册在selector中等待IO操作(及有事件发生)channel的selectionKey。
NIO非阻塞网络编程原理分析图
- 客户端连接时,会通过ServerSocketChannel得到 SocketChannel
- 将SocketChannel注册到 Selector 上
- 注册后返回一个 SelectionKey
- Selector进行监听 select()返回有事件发生的通道个数
- 得到SelectionKey,获取SocketChannel 进行相应事件处理
//服务端
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置 非阻塞
serverSocketChannel.configureBlocking(true);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(1000) == 0) {
System.out.println("no connect");
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
if (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
}
iterator.remove();
}
}
}
//客户端
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
if(!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("connect loading------");
}
}
String str = "hello1q111 ";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(buffer);
System.in.read();
}
NIO群聊
服务端
package com.example.springBoot.NIO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* @author qiumeng
* @version 1.0
* @description
* @date 2021/4/13 22:31
*/
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel listenChannel;
public GroupChatServer() {
try {
selector = Selector.open();
listenChannel = ServerSocketChannel.open();
listenChannel.socket().bind(new InetSocketAddress(8888));
listenChannel.configureBlocking(false);
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
e.printStackTrace();
}
}
public void listen() {
try {
while (true) {
int count = selector.select(2000);
if (count > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
readAccept(key);
readData(key);
//删除当前key 这里为什么要删除?大家可以仔细了解下
iterator.remove();
}
}else {
System.out.println(" no change ");
}
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
private void readData(SelectionKey key) throws IOException {
if (key.isReadable()) {
SocketChannel readChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
if (readChannel.read(buffer) > 0) {
String msg = new String(buffer.array());
System.out.println("client:" + msg);
sendToOtherClient(readChannel, msg);
}
}
}
private void sendToOtherClient(SocketChannel readChannel, String msg) throws IOException {
System.out.println("server transfer msg");
for (SelectionKey selectionKey : selector.keys()) {
Channel channel = selectionKey.channel();
if (channel instanceof SocketChannel&&!readChannel.equals(channel)) {
ByteBuffer targetBUffer = ByteBuffer.wrap(msg.getBytes());
SocketChannel channel1 = (SocketChannel) channel;
channel1.write(targetBUffer);
System.out.println("server transfer success");
}
}
}
private void readAccept(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
SocketChannel acceptChannel = listenChannel.accept();
acceptChannel.configureBlocking(false);
acceptChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println(acceptChannel.getRemoteAddress() + "on line ");
}
}
public static void main(String[] args) {
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
客户端
package com.example.springBoot.NIO;
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.util.Iterator;
import java.util.Scanner;
/**
* @author qiumeng
* @version 1.0
* @description
* @date 2021/4/14 9:21
*/
public class GroupChatClient {
public static final String HOSTNAME = "127.0.0.1";
public static final int PORT = 8888;
private Selector selector;
private SocketChannel socketChannel;
private static String userName;
public GroupChatClient() throws IOException {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress(HOSTNAME, PORT));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
userName = socketChannel.getLocalAddress().toString();
System.out.println("init client");
}
private void sendMsg(String info) throws IOException {
String sendMsg = userName + "说" + info;
socketChannel.write(ByteBuffer.wrap(sendMsg.getBytes()));
}
private void readInfo() throws IOException {
if (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
if (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
if(null!=buffer){
socketChannel.read(buffer);
System.out.println("client receive server:" + new String(buffer.array()));
String msg = new String(buffer.array());
System.out.println(msg);
}
}
iterator.remove();
}
}
}
public static void main(String[] args) throws IOException {
GroupChatClient groupChatClient = new GroupChatClient();
new Thread(() -> {
while (true) {
try {
groupChatClient.readInfo();
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
groupChatClient.sendMsg(scanner.nextLine());
}
}
}
零拷贝(没有CPU拷贝)
- 传统IO 四次拷贝 三次切换
- mmap通过内存映射,将文件映射到内核缓冲区,用户空间可以共享内核空间的数据。少了一次拷贝
- sendFile 三次拷贝 二次切换
- 硬件DMA切换是没法避免的 ** 数据从硬盘进入内核,然后直接到协议栈**
- NIO 零拷贝用了transferTo方法, win8m 多余8M需要分段传输
AIO
- 异步不阻塞
Netty
- 原生NIO的问题
- NIO类库和API繁杂,需要熟练掌握Selector、Serversocketchannel、Socketchannel、buffer等
- 需要具备java多线程技能
- 开发工作和难度非常大:如 客户端重连、网络闪断、失败缓存等
- Epoll bug会导致Selector空轮询,最终导致CPU100%
- 是由JBOSS提供的一个Java开源框架
- 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。(目前是同步非阻塞)
- 主要针对在TCP协议下,面向Clients端的高并发应用,或者peer-to-peer场景下的大量数据持续传输的应用。
- 本质是一个NIO框架,适用于服务器通讯相关的各种应用场景。
- 优点
- 高性能、吞吐量高、延迟低、减少资源消耗、最小化不必要内存复制。
线程模型概述
- 传统I/O
- Reactor模式
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor单线程(Netty基于,做了一定改进)
- 多个连接共用一个阻塞对象,线程分发。()
- 通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动:每次有事件进行处理)
单Reactor单线程
单Reactor多线程
- Reactor对象通过select监听客户端请求,收到事件后,dispatch进行分发
- 建立连接请求,通过accept处理连接请求,然后创建一个handler处理完成连接后的各种事件
- handler只负责响应事件,不做具体的业务处理,会分化后面的worker线程池的某个线程进行处理。
- worker线程池分配独立线程进行处理,并将结果返回handler
优点: 可以利用cpu的能力
缺点: 多线程访问比较复杂,reactor运行在单线程中,处理所有事件和响应,高并发性能瓶颈
主从Reactor多线程
- Reactor主线程负责通过accept处理连接事件。
- accept处理连接事件后,将连接分给 SubReactor
- SubReactor将连接加入到连接队列进行监听,并创建handler进行各种事件进行处理。
- 当有新事件发生时,SubReactor会调用对应的handler进行处理
- handler分发wroker线程池进行处理。
- handler收到相应结果,返回client。
优点:父线程 子线程职责明确,父线程只需要把连接给子线程
缺点:编程复杂度较高
Netty线程模型
传送门:https://www.bilibili.com/video/BV1DJ411m7NR?p=43&spm_id_from=pageDriver
-
Netty抽象出2组线程池:BoosGroup 只负责客户端的连接;WorkerGroup 负责网络的读写
-
BoosGroup和WorkerGroup 都是 NioEventLoopGroup
-
NioEventLoopGroup:事件循环,每一个事件是 NioEventLoop
-
NioEventLoop:表示一个不断循环执行处理任务的线程,每一个NioEventLoop都有一个Selector,用于监听绑定在Selector上的网络通讯。一个taskQueue
-
NioEventLoopGroup:NioEventLoop=1:n (可以代码指定)
-
Boss下的NioEventLoop执行有三步
- 轮询accept事件
- 处理accept事件,与client建立连接,产生NiosocketChannel,并注册到某个Worker NioEventLoop上的Selector
- 处理任务队列的任务 即runAllTasks 阻塞队列
-
Worker下的NioEventLoop执行有三步
- 轮询 read/write事件
- 处理IO事件
- 处理任务队列的任务 即runAllTasks
-
Worker下的NioEventLoop会使用PipeLine,维护了很多处理器。
传送门:https://segmentfault.com/a/1190000017053730
- 自定义TaskQueue,提交任务到ScheduleTaskQueue