javaIO和netty的基本原理

This commit is contained in:
法然
2022-11-27 14:45:19 +08:00
parent f90e4ed166
commit 19d3b3e3e2
169 changed files with 5444 additions and 3691 deletions

201
Netty/01 Netty简介.md Normal file
View File

@@ -0,0 +1,201 @@
## 概述
### 是什么
Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Netty和Tomcat最大的区别就在于通信协议Tomcat是基于Http协议的Servlet容器他的实质是一个基于http协议的web容器但是Netty不一样他能通过编程自定义各种协议因为netty能够通过codec自己来编码/解码字节流完成类似redis访问的功能这就是netty和tomcat最大的不同。
![](image/2022-11-27-13-49-01.png)
### 对比
Netty vs NIO工作量大bug 多
* 需要自己构建协议
* 解决 TCP 传输问题,如粘包、半包
* epoll 空轮询导致 CPU 100%
* 对 API 进行增强,使之更易用,如 FastThreadLocal => ThreadLocalByteBuf => ByteBufferNetty更友好更强大1、JDK中NIO的一些API功能薄弱且复杂Netty隔离了JDK中NIO的实现变化及实现细节譬如ByteBuffer -> ByteBuf主要负责从底层的IO中读取数据到ByteBuf然后传递给应用程序应用程序处理完之后封装为ByteBuf写回给IO。
* Netty自身线程安全使用JDK原生API需要对多线程要很熟悉 因为N I O 涉及到Reactor设计模式得对里面的原理要相当的熟悉。
### 原理
Netty是一款基于NIONonblocking I/O非阻塞IO开发的网络通信框架对比于BIOBlocking I/O阻塞IO
当一个连接建立之后他有两个步骤要做第一步是接收完客户端发过来的全部数据第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。
1. 在BIO中等待客户端发数据这个过程是阻塞的这样就造成了一个线程只能处理一个请求的情况而机器能支持的最大线程数是有限的这就是为什么BIO不能支持高并发的原因。
![](image/2022-11-25-23-13-40.png)
2. 而NIO中当一个Socket建立好之后Thread并不会阻塞去接受这个Socket而是将这个请求交给SelectorSelector会不断的去遍历所有的Socket一旦有一个Socket建立完成他会通知Thread然后Thread处理完数据再返回给客户端——这个过程是不阻塞的这样就能让一个Thread处理更多的请求了
![](image/2022-11-25-23-13-51.png)
### 零拷贝
Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道Java的内存有堆内存、栈内存和字符串常量池等等其中堆内存是占用内存空间最大的一块也是Java对象存放的地方一般我们的数据如果需要从IO读取到堆内存中间需要经过Socket缓冲区也就是说一个数据会被拷贝两次才能到达他的的终点如果数据量大就会造成不必要的资源浪费。
Netty针对这种情况使用了NIO中的另一大特性——零拷贝当他需要接收数据的时候他会在堆内存之外开辟一块内存数据就直接从IO读到了那块内存中去在netty里面通过ByteBuf可以直接对这些数据进行直接操作从而加快了传输速度。
下两图就介绍了两种拷贝方式的区别
* 传统的数据拷贝方式
![](image/2022-11-26-00-06-12.png)
* 零拷贝
![](image/2022-11-26-00-06-19.png)
## 2 不同IO方式的Socket编程
### 阻塞IO
```java
public class PlainOioServer {
public void serve(int port) throws IOException {
final ServerSocket socket = new ServerSocket(port); //1
try {
for (;;) {
final Socket clientSocket = socket.accept(); //2
System.out.println("Accepted connection from " + clientSocket);
new Thread(new Runnable() { //3
@Override
public void run() {
OutputStream out;
try {
out = clientSocket.getOutputStream();
out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8"))); //4
out.flush();
clientSocket.close(); //5
} catch (IOException e) {
e.printStackTrace();
try {
clientSocket.close();
} catch (IOException ex) {
// ignore on close
}
}
}
}).start(); //6
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
### 非阻塞IO
* java原生的channel机制
```java
public class PlainNioServer {
public void serve(int port) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address); //1
Selector selector = Selector.open(); //2
serverChannel.register(selector, SelectionKey.OP_ACCEPT); //3
final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
for (;;) {
try {
selector.select(); //4
} catch (IOException ex) {
ex.printStackTrace();
// handle exception
break;
}
Set<SelectionKey> readyKeys = selector.selectedKeys(); //5
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) { //6
ServerSocketChannel server =
(ServerSocketChannel)key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_WRITE |
SelectionKey.OP_READ, msg.duplicate()); //7
System.out.println(
"Accepted connection from " + client);
}
if (key.isWritable()) { //8
SocketChannel client =
(SocketChannel)key.channel();
ByteBuffer buffer =
(ByteBuffer)key.attachment();
while (buffer.hasRemaining()) {
if (client.write(buffer) == 0) { //9
break;
}
}
client.close(); //10
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException cex) {
// 在关闭时忽略
}
}
}
}
}
}
```
### Netty实现Socket编程
```java
public class NettyOioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
EventLoopGroup group = new OioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); //1
b.group(group) //2
.channel(OioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {//3
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { //4
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
}
});
}
});
ChannelFuture f = b.bind().sync(); //6
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync(); //7
}
}
}
```

542
Netty/02 核心组件.md Normal file
View File

@@ -0,0 +1,542 @@
> 参考文献
> https://www.jianshu.com/p/b9f3f6a16911
> https://blog.csdn.net/weixin_53142722/article/details/124942857
> https://blog.csdn.net/qq_39339965/article/details/122344873
> https://www.cnblogs.com/lbhym/p/12753314.html
> https://zhuanlan.zhihu.com/p/514448867
> 为什么这样排版
> 因为这是bootstrap创建的步骤
> EventLoop/EventLoopGroup
> Channel
> ChannelFuture sync/addlistener
> Handler
> Pipeline/PipelineGroup
## 0 HelloWorld
### 服务器
```java
public static void main(String[] args) {
//1.启动器负责组转netty组件启动服务器
new ServerBootstrap()
// 2.加入nio组的监听器在里面包含selector选择器
.group(new NioEventLoopGroup())
// 3.选择服务器的ServerSocketChannel实现
.channel(NioServerSocketChannel.class)
//4.boss负责处理连接 worker(child)负责处理读写决定了worker(child)能执行哪些操作(handler)
.childHandler(
//5.channel代表和客户端进行数据读写通道initializer初始化负责添加其他的handler
new ChannelInitializer<NioSocketChannel>() {
//该方法是连接建立后才执行
protected void initChannel(NioSocketChannel ch) {
//6.添加具体的handler
ch.pipeline().addLast(new StringDecoder()); //将ByteBuf转换为字符串
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { //自定义handler
@Override //读事件
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
//打印上一步转换好的字符串
System.out.println(msg);
}
});
}
})
//7.绑定监听器端口
.bind(8080);
}
```
1. group()创建 NioEventLoopGroup可以简单理解为 线程池 + Selector
2. channel()选择服务 Scoket 实现类,其中 NioServerSocketChannel 表示基于 NIO 的服务器端实现,其它实现还有:
3. childHandler()接下来添加的处理器都是给 SocketChannel 用的,而不是给 ServerSocketChannel。ChannelInitializer 处理器(仅执行一次),它的作用是待客户端 SocketChannel 建立连接后,执行 initChannel 以便添加更多的处理器
4. bind()ServerSocketChannel 绑定的监听端口。返回ChannelFuture对象。
5. SocketChannel 的处理器,解码 ByteBuf => String
6. SocketChannel 的业务处理器,使用上一个处理器的处理结果
### 客户端
```java
//1.启动类
new Bootstrap()
//2.添加eventLoop
.group(new NioEventLoopGroup())
//3.选择客户端channel实现
.channel(NioSocketChannel.class)
//4.添加处理器
.handler(new ChannelInitializer<Channel>() {
@Override //建立连接后被调用
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
//连接服务器
.connect("127.0.0.1", 8080)
.sync()
.channel()
//向服务器发生数据
.writeAndFlush(new Date() + ": hello world!");
```
1. group()创建 NioEventLoopGroup同 Server
2. channel选择客户 Socket 实现类NioSocketChannel 表示基于 NIO 的客户端实现,其它实现还有
3. handler()添加 SocketChannel 的处理器ChannelInitializer 处理器(仅执行一次),它的作用是待客户端 SocketChannel 建立连接后,执行 initChannel 以便添加更多的处理器
4. connect()指定要连接的服务器和端口返回channelFuture对象可以进行后续的通信操作
5. sync()Netty 中很多方法都是异步的,如 connect这时需要使用 sync 方法等待 connect 建立连接完毕
6. channel获取 channel 对象,它即为通道抽象,可以进行数据读写操作
7. writeAndFlush写入消息并清空缓冲区
8. 消息会经过通道 handler 处理,这里是将 String => ByteBuf 发出,数据经过网络传输,到达服务器端,服务器端 5 和 6 处的 handler 先后被触发,走完一个流程
## 1 EventLoop&EventLoopGroup
### 基本概念
* EventLoop 本质是一个单线程执行器(同时维护了一个 Selector里面有 run 方法处理 Channel 上源源不断的 io 事件。
* EventLoopGroup是一个接口 是一组 EventLoopChannel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)
### 组件关系
EventLoop用于处理连接的生命周期中所发生的事件。 Channel、EventLoop、Thread 以及 EventLoopGroup 之间的关系。
![](image/2022-11-27-11-15-49.png)
1. 一个 EventLoopGroup 包含一个或者多个 EventLoop;
2. 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
3. 一个 Channel 在它的生命周期内只注册于一个 EventLoop;一个 EventLoop 可能会被分配给一个或多个 Channel。
### 主要方法
* EventLoop判断。inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
* EventLoop属组。parent 方法来看看自己属于哪个 EventLoopGroup
* EventLoopGroup 遍历
```java
DefaultEventLoopGroup group = new DefaultEventLoopGroup(2);
for (EventExecutor eventLoop : group) {
System.out.println(eventLoop);
}
```
* EventLoopGroup 关闭。shutdownGracefully 方法首先切换 EventLoopGroup 到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行。从而确保整体应用是在正常有序的状态下退出
* NioEventLoop 处理普通任务。除了可以处理 io 事件,同样可以向它提交普通任务;
```java
NioEventLoopGroup nioWorkers = new NioEventLoopGroup(2);
log.debug("server start...");
Thread.sleep(2000);
nioWorkers.execute(()->{
log.debug("normal task...");
});
```
* NioEventLoop 处理定时任务
```java
NioEventLoopGroup nioWorkers = new NioEventLoopGroup(2);
log.debug("server start...");
Thread.sleep(2000);
nioWorkers.scheduleAtFixedRate(() -> {
log.debug("running...");
}, 0, 1, TimeUnit.SECONDS); //第一个参数是任务,第二个参数是初始等待时间,第三个参数是时间间隔,第四个参数是时间单位
```
## 2 Channel&ChannelFuture
> 对应java NIO中的Channel
### 是什么
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作
它可以被打开或者被关闭,连接或者断开连接。
### 主要方法
* close() 可以用来关闭 channel
* closeFuture() 用来处理 channel 的关闭
* sync() 方法作用是同步等待 channel 关闭
* addListener() 方法是异步等待 channel 关闭
* pipeline() 方法添加处理器
* write() 方法将数据写入channel但是并不会直接发出而是先存在缓冲区中要刷新缓冲区才会发出去
* writeAndFlush() 方法将数据立刻写入并刷出
### 获取连接建立的ChannelFuture
* 服务器端
```java
new ServerBootstrap().bind()
```
* 客户端
```java
new Bootstrap().connect()
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("127.0.0.1", 8080);
```
* 利用 channel() 方法来获取 Channel 对象。
### ChannelFuture同步操作
sync 方法是同步等待连接建立完成阻塞当前线程直到nio线程建立连接完毕。connect 方法是异步的意味着不等连接建立方法执行就返回了拿到的可能是未初始化的channel。因此 channelFuture 对象中不能【立刻】获得到正确的 Channel 对象
```java
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("127.0.0.1", 8080);
System.out.println(channelFuture.channel()); // 1
channelFuture.sync(); // 2
System.out.println(channelFuture.channel()); // 3
```
### ChannelFuture异步通知
因为一个操作可能不会 立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。
Netty 提供了 ChannelFuture接口其addListener()方法注册了一个ChannelFutureListener以 便在某个操作完成时(无论是否成功)得到通知。
Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操 作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问
ChannelFuture提供了几种额外的方法这些方法使得我们能够注册一个或者多个 ChannelFutureListener实例。监听器的回调方法operationComplete(),将会在对应的 操作完成时被调用 。
![](image/2022-11-27-10-15-20.png)
```java
// 获取 CloseFuture 对象, 1) 同步处理关闭, 2) 异步处理关闭
//主要是做在关闭之后的善后操作
ChannelFuture closeFuture = channel.closeFuture();
/*log.debug("waiting close...");
closeFuture.sync(); //同步的处理方式,就是关闭之后才会继续往下执行善后操作
log.debug("处理关闭之后的操作");*/
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug("处理关闭之后的操作"); //这里的关闭以及善后的工作都是同一个LoopGroup来完成
group.shutdownGracefully(); //关闭连接该线程的eventLoopGroup里面的资源都会被释放
}
});
```
## 3 ChannelPipeline & ChannelHandler
### 基本概念
处理器ChannelHandlerChannelPipeline 提供了 ChannelHandler 链的容器。以服务端程序为例客户端发送过来的数据要接收读取处理我们称数据是入站的需要经过一系列Handler处理后如果服务器想向客户端写回数据也需要经过一系列Handler处理我们称数据是出站的。ChannelPipeline 和 ChannelHandler 之间是相互绑定的,也就是一对一关系。
![](image/2022-11-27-14-15-29.png)
每个 Channel 是一个产品的加工车间Pipeline 是车间中的流水线ChannelHandler 就是流水线上的各道工序,而后面要讲的 ByteBuf 是原材料,经过很多工序的加工:先经过一道道入站工序,再经过一道道出站工序最终变成产品
* 当一个回调被触发时,相关的事件可以被一个 interface ChannelHandler 的实现处理。
* 通过回调机制处理事件。当一个新的连接已经被建立时, ChannelHandler的 channelActive()回调方法将会被调用,并将打印出
![](image/2022-11-27-10-10-22.png)
### ChannelHandler分类
ChannelHandler 用来处理 Channel 上的各种事件,分为入站、出站两种。所有 ChannelHandler 被连成一串,就是 Pipeline
* 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果
* 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工
inbound/outbound对于数据的出站和入站有着不同的ChannelHandler类型与之对应
* ChannelInboundHandler 入站事件处理器、
* ChannelOutBoundHandler 出站事件处理器、
* ChannelHandlerAdapter提供了一些方法的默认实现可减少用户对于ChannelHandler的编写、
* ChannelDuplexHandler混合型既能处理入站事件又能处理出站事件。从下面的关系图可以看出如果我们需要自己的Handler的时候
* 读取事件可以继承ChannelInboundHandlerAbapter
* 写出事件可以继承ChannelOutBoundHandlerAbapter。
![](image/2022-11-27-14-17-35.png)
### 组件关系
数据传输流与channel相关的概念有以下四个上一张图让你了解netty里面的Channel。
* Channel表示一个连接可以理解为每一个请求就是一个Channel。ChannelFuture 一种基于回调的事件处理接口。
* ChannelHandler核心处理业务就在这里用于处理业务请求。ChannelHandlerContext用于传输业务数据。ChannelPipeline用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext。
![](image/2022-11-27-09-15-21.png)
### 体系结构
inbound/outboundinbound入站事件处理顺序方向是由链表的头到链表尾outbound事件的处理顺序是由链表尾到链表头。inbound入站事件由netty内部触发最终由netty外部的代码消费。outbound事件由netty外部的代码触发最终由netty内部消费。
![](image/2022-11-27-14-18-49.png)
* 服务器端
```java
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(1);
ctx.fireChannelRead(msg); //1 让消息传递给下一个handler不然下一个handler可能就会执行出错这是自定义handler尤其需要注意点
}
});
// 添加处理器 我们添加的处理器虽然这里是叫last但是真正收尾的handler是 tail
//head -> 我们自定义的处理器 -> tail
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(2);
ctx.fireChannelRead(msg); // 2
}
});
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(3);
ctx.channel().write(msg); // 3
}
});
//出站的handler要先外写数据才会触发不就是摆设
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(4);
ctx.write(msg, promise); // 4
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(5);
ctx.write(msg, promise); // 5
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(6);
ctx.write(msg, promise); // 6
}
});
}
})
.bind(8080);
```
* 客户端
```java
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("127.0.0.1", 8080)
.addListener((ChannelFutureListener) future -> {
//必须要flush刷新一下缓存
future.channel().writeAndFlush("hello,world");
});
```
* 执行结果
```
1
2
3
6
5
4
```
ChannelInboundHandlerAdapter 是按照 addLast 的顺序执行的,而 ChannelOutboundHandlerAdapter 是按照 addLast 的逆序执行的。ChannelPipeline 的实现是一个 ChannelHandlerContext包装了 ChannelHandler 组成的双向链表
![](image/2022-11-27-13-29-57.png)
* 入站处理器中ctx.fireChannelRead(msg) 是 调用下一个入站处理器
* 如果注释掉 1 处代码,则仅会打印 1
* 如果注释掉 2 处代码,则仅会打印 1 2
* 出站处理器中ctx.channel().write(msg) 和ctx.write(msg, promise) 会 从尾部开始触发 后续出站处理器的执行
* 如果注释掉 3 处代码,则仅会打印 1 2 3
* 如果注释掉 6 处代码,则仅会打印 1 2 3 6 注释掉6表示不会去寻找上一个出站处理器
* ctx.channel().write(msg) vs ctx.write(msg)
* 都是触发出站处理器的执行
* ctx.channel().write(msg) 从尾部开始查找出站处理器
* ctx.write(msg) 是从当前节点找上一个出站处理器
* 3 处的 ctx.channel().write(msg) 如果改为 ctx.write(msg) 仅会打印 1 2 3因为节点3 之前没有其它出站处理器了
* 6 处的 ctx.write(msg, promise) 如果改为 ctx.channel().write(msg) 会打印 1 2 3 6 6 6... 因为 ctx.channel().write() 是从尾部开始查找结果又是节点6 自己
## 4 ByteBuf
### 简介
> 对应java NIO中的Buffer
ByteBuf是一个存储字节的容器最大特点就是使用方便它既有自己的读索引和写索引方便你对整段字节缓存进行读写也支持get/set方便你对其中每一个字节进行读写。
ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从 ByteBuf 读取时, 它的 readerIndex 将会被递增已经被读取的字节数。同样地,当你写入 ByteBuf 时,它的 writerIndex 也会被递增。
![](image/2022-11-27-11-24-16.png)
### 三种模式
* Heap Buffer 堆缓冲区。堆缓冲区是ByteBuf最常用的模式他将数据存储在堆空间。
* Direct Buffer 直接缓冲区。直接缓冲区是ByteBuf的另外一种常用模式他的内存分配都不发生在堆jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存这样做有两个好处
* 通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
* DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.
* 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用。直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放。默认使用直接内存
* Composite Buffer 复合缓冲区。复合缓冲区相当于多个不同ByteBuf的视图这是netty提供的jdk不提供这样的功能。
### 组成
最开始读写指针都在 0 位置不用像NIO一样需要开发人员切换读写模式而且这个ByteBuf还是动态扩容扩容不能超过 max capacity 会报错。选择下一个 2^n容量。
![](image/2022-11-27-13-36-53.png)
### Codec
Netty中的编码/解码器通过他你能完成字节与pojo、pojo与pojo的相互转换从而达到自定义协议的目的。
在Netty里面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。
### 主要方法
* 创建
```
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10); //默认容量是256可以动态扩容
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
```
* 读写。名称以 read 或者 write 开头的 ByteBuf 方法,将会推进其对应的索引,而名称以 set 或 者 get 开头的操作则不会。后面的这些方法将在作为一个参数传入的一个相对索引上执行操作。
* 重复读写使用mark,或者get
```java
buffer.markReaderIndex();
System.out.println(buffer.readInt());
buffer.resetReaderIndex();
```
### retain & release
### slice零拷贝的体现
【零拷贝】的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 readwrite 指针
![](image/2022-11-27-13-44-43.png)
* 基本操作
```java
ByteBuf origin = ByteBufAllocator.DEFAULT.buffer(10);
origin.writeBytes(new byte[]{1, 2, 3, 4});
origin.readByte();
System.out.println(ByteBufUtil.prettyHexDump(origin));
```
* slice操作。这时调用 slice 进行切片,无参 slice 是从原始 ByteBuf 的 read index 到 write index 之间的内容进行切片,切片后的 max capacity 被固定为这个区间的大小,因此不能追加 write因为一旦追加就会导致切片前后的内容受到影响
* read操作互补干扰
* 修改操作互相可见。
### duplicate零拷贝的体现
【零拷贝】的体现之一,就好比截取了原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的;
### copy
会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关
## 5 实例
### 服务器
```java
new ServerBootstrap()
//更加细化的进行分工一个是专门用来处理accept请求的另外的是用来处理read和write的
.group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//这里监听的方法是根据我们自己的需求来的
@Override //因为没有进行转换字符集msg是ByteBuf类型
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = msg instanceof ByteBuf ? ((ByteBuf) msg) : null;
if (byteBuf != null) {
byte[] buf = new byte[16];
ByteBuf len = byteBuf.readBytes(buf, 0, byteBuf.readableBytes());
log.debug(new String(buf));
}
}
});
}
}).bind(8080).sync();
```
### 客户端
```java
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup(1))
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
System.out.println("init...");
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
})
.channel(NioSocketChannel.class).connect("localhost", 8080)
.sync()
.channel();
channel.writeAndFlush(ByteBufAllocator.DEFAULT.buffer().writeBytes("wangwu".getBytes()));
Thread.sleep(2000);
channel.writeAndFlush(ByteBufAllocator.DEFAULT.buffer().writeBytes("wangwu".getBytes()));
```

265
Netty/03 网络通信.md Normal file
View File

@@ -0,0 +1,265 @@
### 服务器
```java
package com.example.netty.io;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* netty 服务端
*
* @author lanx
* @date 2022/3/20
*/
public class Server {
public static void main(String[] args) throws InterruptedException {
//用户接收客户端连接的线程工作组
EventLoopGroup bossGroup = new NioEventLoopGroup();
//用于接收客户端连接读写操作的线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
//辅助类 帮我我们创建netty服务
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)//绑定两个工作组
.channel(NioServerSocketChannel.class)//设置NIO模式
//option 针对于服务端配置; childOption 针对于客户端连接通道配置
.option(ChannelOption.SO_BACKLOG, 1024)//设置tcp缓冲区
.childOption(ChannelOption.SO_SNDBUF, 32 * 1024)//设置发送数据的缓存大小
.childOption(ChannelOption.SO_RCVBUF, 32 * 1024)//设置读取数据的缓存大小
.childOption(ChannelOption.SO_KEEPALIVE, true)//设置保持长连接
//初始化绑定服务通道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//为通道进行初始化:数据传输过来的时候会进行拦截和执行 (可以有多个拦截器)
sc.pipeline().addLast(new ServerHandler());
}
});
ChannelFuture cf = b.bind(8765).sync();
//释放连接
cf.channel().closeFuture().sync();
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
```
```java
package com.example.netty.io;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
/**
* 服务端 监听器
*
* @author lanx
* @date 2022/3/20
*/
public class ServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当我们的通道被激活的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------服务端通道激活---------");
}
/**
* 当我们通道里有数据进行读取的时候触发的监听
*
* @param ctx netty服务上下文
* @param msg 实际传输的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
// NIO 通信 (传输的数据是什么? ---------> Buffer 对象)
ByteBuf buf = (ByteBuf) msg;
//定义byte数组
byte[] req = new byte[buf.readableBytes()];
// 从缓冲区获取数据到 req
buf.readBytes(req);
//读取到的数据转换为字符串
String body = new String(req, "utf-8");
System.out.println("服务端读取到数据:" + body);
//响应给客户端的数据
ctx.writeAndFlush(Unpooled.copiedBuffer("netty server response data".getBytes()));
// 添加 addListener 可以触发关闭通道监听事件(客户端短连接场景使用)
// .addListener(ChannelFutureListener.CLOSE);
}catch (Exception e){
e.printStackTrace();
}finally {
//释放数据
ReferenceCountUtil.release(msg);
}
}
/**
* 当我们读取完成数据的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------服务端数据读取完毕---------");
}
/**
* 当我们读取数据异常的时候触发的监听
*
* @param ctx
* @param cause
* @throws Exception
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("--------服务端数据读取异常---------");
ctx.close();
}
}
```
### 客户端
```java
package com.example.netty.io;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* netty 客户端
* @author lanx
* @date 2022/3/20
*/
public class Client {
public static void main(String[] args) throws InterruptedException {
//线程工作组
EventLoopGroup workerGroup = new NioEventLoopGroup();
//辅助类 帮我我们创建netty服务
Bootstrap b = new Bootstrap();
b.group( workerGroup)//绑定两个工作组
.channel(NioSocketChannel.class)//设置NIO模式
//初始化绑定服务通道
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//为通道进行初始化:数据传输过来的时候会进行拦截和执行 (可以有多个拦截器)
sc.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture cf = b.connect("127.0.0.1",8765).syncUninterruptibly();
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data".getBytes()));
//释放连接
cf.channel().closeFuture().sync();
workerGroup.shutdownGracefully();
}
}
```
```java
package com.example.netty.io;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
/**
* 客户端 监听器
*
* @author lanx
* @date 2022/3/20
*/
public class ClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当我们的通道被激活的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------客户端通道激活---------");
}
/**
* 当我们通道里有数据进行读取的时候触发的监听
*
* @param ctx netty服务上下文
* @param msg 实际传输的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
// NIO 通信 (传输的数据是什么? ---------> Buffer 对象)
ByteBuf buf = (ByteBuf) msg;
//定义byte数组
byte[] req = new byte[buf.readableBytes()];
// 从缓冲区获取数据到 req
buf.readBytes(req);
//读取到的数据转换为字符串
String body = new String(req, "utf-8");
System.out.println("客户端读取到数据:" + body);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放数据 (如果你读取数据后又写出去数据就不需要调用此方法,因为底层代码帮忙实现额释放数据)
ReferenceCountUtil.release(msg);
}
}
/**
* 当我们读取完成数据的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------客户端数据读取完毕---------");
}
/**
* 当我们读取数据异常的时候触发的监听
*
* @param ctx
* @param cause
* @throws Exception
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("--------客户端数据读取异常---------");
ctx.close();
}
}
```

View File

@@ -0,0 +1,57 @@
## Reactor与Netty
### Reactor概念
Reactor模型中定义了三种角色
* Reactor负责监听和分配事件将I/O事件分派给对应的Handler。新的时间包含连接建立就绪、读就绪、写就绪等。
* Acceptor处理客户端新连接并分派请求到处理器链中。
* Handler将自身与事件绑定执行费阻塞读/写人物完成channel的读入完成处理业务逻辑后负责将结果写出channel。
### 单Reactor - 单线程模型
NIO下Reactor单线程所有的接受连接处理数据的相关操作都在一个线程中完成性能上有瓶颈。
![](image/2022-11-27-14-00-48.png)
### 单Reactor - 多线程模型
把比较消耗时的数据的编解码运算操作放入线程池中执行,虽然提升了性能但是还不是最好的方式。
![](image/2022-11-27-14-05-04.png)
### 主从Reactor - 多线程
主从多线程,对于服务器来说,接收客户端的连接是比较重要的,因此将这部分操作单独用线程去操作。
![](image/2022-11-27-14-06-17.png)
这种模式的基本工作流程为:
1. Reactor主线程MainReactor对象通过select监听客户端连接事件收到事件后通过Acceptor处理客户端连接事件。
2. 当Acceptor处理完客户端连接事件之后与客户端建立号socket连接MainReactor将连接分配给subReactor。MainReactor只负责监听客户端的请求和客户端建立连接后将连接交给subReactor监听后面的IO事件。
3. subReactor将连接加入到自己的连接队列进行监听并创建Handler对各种事件进行处理。
4. 当连接上有新事件发生的时候subReactor将会调用对应的Handler处理。
5. Handler通过read从连接上读取请求数据将请求数据分发给Worker线程池进行业务处理。
6. worker线程池会分配独立的线程来完成真正的业务处理并将处理结果返回给handler。Handler通过send向客户端发送响应数据。
7. 一个MainReactor可以对应多个subReactor即一个MainReactor线程可以对应多个subReactor线程。
### 主从Reactor优势
这种模式的优势如下:
1. MainReactor 线程与SubReactor 线程的数据交互简单职责明确MainReactor 线程只需要接受新连接SubReactor 线程完成后续的业务处理。
2. MainReactor 线程与SubReactor 线程的数据交互简单MainReactor 线程只需要把新连接传给SubReactor 线程SubReactor 线程无需返回数据。
3. 多个SubReactor 线程能够应对更高的并发请求。
这种模式的缺点是编程复杂度高。但是由于优点明显在许多项目中被广泛使用包括Nginx、Memcachend、Netty等。
这种模式也被叫做服务器的 1+M+N线程模式即使用改模式开发的服务器包含一个或多个1只是表示相对较少连接建立线程 + M 个IO 线程 + N 个业务处理线程。这是业界成熟的服务器程序设计模式。
### Netty中的Reactor实现
1. Netty抽象出两组线程池Boss EventLoopGroup和Worker EventLoopGroup每个线程池中都有 EventLoopGroup 线程可以是OIONIOAIO。Boss EventLoopGroup中的线程专门负责处理和客户端建立连接Worker EventLoopGroup 中的线程专门负责处理连接上的读写EventLoopGroup 相当于一个事件循环组,这个组中含有锁多个事件循环。
2. EventLoop 表示一个不断循环的执行事件处理的线程每个EventLoop 都含有一个selector用于监听注册在其上的socket 网络连接channel
3. 每一个Boss EventLoopGroup 中循环执行以下三个步骤a、select轮询注册在其上的serverSocketChannel 的 accept 事件OP_ACCEPT 事件b、processSeleckendKeys处理 accept 事件与客户端建立连接生产一个socketChannel并将其注册到Worker EventLoop 上的某个selector 上c、runAllTasks再去以此循环处理任务队列中的其他任务。
3. 每个Worker EventLoop 中循环执行这三个步骤a、select轮询注册在其上的socketChannel 的read / write 事件OP_READ / OP_WRITE 事件b、processSeleckendKeys在对应的socketChannel 上处理read / write 事件c、runAllTasks再去以此循环处理任务队列中的其他任务。
4. 在以上两个processSeleckendKeys步骤中会使用Pipeline管道Pipeline 中引用了channel即通过Pipeline 可以获取对应的ChannelPipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。
![](image/2022-11-27-13-57-48.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB