netty网络编程 <一>
讲解步骤
Linux网络编程
网络事件模型
事件模型的实现并不简单,它依赖于操作系统提供的多路复用功能。“多路复用”这一概念来自电信领域,通俗地讲,是指用一条电缆传递多路信号。在服务端领域,“多路复用”是指使程序能够同时处理多个客户端连接的技术,其中又以Epoll最为流行;
有两种事件模型
1.轮询
2.Epoll和Select 一种高度可扩展的事件通知特性
轮询的效率很低,Epoll和Select具体做法是让线程阻塞,当有任何一个客户端发来消息时,再唤醒线程,并告知线程是哪些客户端发来的消息。
连接事件优化—多路复用epoll
epoll 底层实现:
epoll 对象在操作系统底层监听socket 文件产生的变动,如果有数据就通知进程处理, 没有数据就等待。
详细来说:epoll_wait是Epoll多路复用最重要的API。如果epoll对象所监听的元素暂无事件,那么线程会阻塞,直到某一事件发生;如果epoll对象所监听的文件有事件发生,那么epoll_wait将立即返回。如图所示,假设epoll对象监听着两个套接字的读事件,且它们的读缓冲区都有数据(图中的“a”和“go”),这时调用epoll_wait,它会立即返回,走标记为“有”的分支,返回值为2(该返回值表示事件的数量)。如果两个套接字的数据全部读取完毕,那么操作系统将会调用epoll_wait阻塞线程,如图6-28所示。当没有事件发生时,epoll_wait就会进入休眠状态。
详细说明可以搜 《如果这篇文章说不清Epoll的本质,那就过来掐死我吧!》
零拷贝
零拷贝是在NIO和Epoll上的一种特性。可以快速的从文件系统移动到网络接口。而不需要将数据从内核空间拷贝到用户空间。
netty 的channel 就是零拷贝的实现。
➢零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
➢零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销
DMA 拷贝是cpu拷贝的十倍性能,同时少了2次cpu拷贝。
在早期计算机中,用户进程需要读取磁盘数据,需要CPU中断和CPU参与,因此效率比较低,发起IO请求,每次的IO中断,都带来CPU的上下文切换。因此出现了——DMA。
DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU 的大量中断负载。DMA控制器,接管了数据读写请求,减少CPU的负担。这样一来,CPU能高效工作了。现代硬盘基本都支持DMA。
实际因此IO读取,涉及两个过程:
1、DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区;
2、用户进程,将内核缓冲区的数据copy到用户空间。
这两个过程,都是阻塞的。
I/O类别
BIO/OIO(同步阻塞)
食堂排队打饭模式:排队在窗口,打好才走;
NIO(异步通知、非阻塞)
点单、等待被叫模式:等待被叫,好了自己去端;
NIO和BIO的主要区别
面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
AIO(异步通知、非阻塞)
包厢模式:点单后菜直接被端上桌。windows 实现非开源。
NIO 核心组件
Selector选择器
Selector的英文含义是“选择器”,也可以称为为“轮询代理器”、“事件订阅器”、“channel容器管理机”都行。
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器(Selectors),然后使用一个单独的线程来操作这个选择器,进而“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
应用程序将向Selector对象注册需要它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器。
Channel管道
通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据,而且可以同时进行读写。
所有被Selector(选择器)注册的通道,只能是继承了SelectableChannel类的子类。
ServerSocketChannel:应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用IO”的端口监听。同时支持UDP协议和TCP协议。
ScoketChannel:TCP Socket套接字的监听通道,一个Socket套接字对应了一个客户端IP:端口到服务器IP:端口的通信连接。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
buffer缓冲区
SelectionKey
SelectionKey是一个抽象类,表示selectableChannel在Selector中注册的标识.每个Channel向Selector注册时,都将会创建一个SelectionKey。SelectionKey将Channel与Selector建立了关系,并维护了channel事件。
可以通过cancel方法取消键,取消的键不会立即从selector中移除,而是添加到cancelledKeys中,在下一次select操作时移除它.所以在调用某个key时,需要使用isValid进行校验.
要想获得一个Buffer对象首先要进行分配。每一个Buffer类都有allocate方法(可以在堆上分配,也可以在直接内存上分配)。
SelectionKey类型和就绪条件
在向Selector对象注册感兴趣的事件时,JAVA NIO共定义了四种:OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT(定义在SelectionKey中),分别对应读、写、请求连接、接受连接等网络Socket操作。
服务端和客户端分别感兴趣的类型
ServerSocketChannel和SocketChannel可以注册自己感兴趣的操作类型,当对应操作类型的就绪条件满足时OS会通知channel,下表描述各种Channel允许注册的操作类型,Y表示允许注册,N表示不允许注册,其中服务器SocketChannel指由服务器ServerSocketChannel.accept()返回的对象。
服务器启动ServerSocketChannel,关注OP_ACCEPT事件,
客户端启动SocketChannel,连接服务器,关注OP_CONNECT事件
服务器接受连接,启动一个服务器的SocketChannel,这个SocketChannel可以关注OP_READ、OP_WRITE事件,一般连接建立后会直接关注OP_READ事件
客户端这边的客户端SocketChannel发现连接建立后,可以关注OP_READ、OP_WRITE事件,一般是需要客户端需要发送数据了才关注OP_READ事件
连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注OP_READ、OP_WRITE事件。
Buffer
Buffer用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。以写为例,应用程序都是将数据写入缓冲,再通过通道把缓冲的数据发送出去,读也是一样,数据总是先从通道读到缓冲,应用程序再读缓冲的数据。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存(其实就是数组)。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
当你写数据到Buffer中时,position表示当前能写的位置。初始的position值为0。当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
Buffer的分配
要想获得一个Buffe
r对象首先要进行分配。每一个Buffer类都有allocate方法(可以在堆上分配,也可以在直接内存上分配)。
DirectByteBuffer和HeapByteBuffer
DirectByteBuffer和HeapByteBuffer是Java NIO中的两种ByteBuffer实现。它们在内存分配和访问上有一些区别:
内存分配:
1. DirectByteBuffer:DirectByteBuffer使用直接内存(direct memory)进行分配,这是一种在JVM堆之外分配内存的方式。直接内存通过调用操作系统的本机I/O函数来进行管理。在创建DirectByteBuffer时,内存会从操作系统申请,并在Java堆内存中建立一个对该内存的引用。
2. HeapByteBuffer:HeapByteBuffer使用堆内存进行分配,即在JVM的堆中分配内存。
内存访问:
1. DirectByteBuffer:DirectByteBuffer通过调用JNI(Java Native Interface)函数直接操作直接内存。这种方式可以避免数据在Java堆和直接内存之间的复制,因此在进行I/O操作时,性能较好。
2. HeapByteBuffer:HeapByteBuffer是通过数组来实现的,在Java堆中存储字节数据。在进行I/O操作时,需要将数据从Java堆复制到直接内存或从直接内存复制回Java堆,这涉及额外的内存复制操作,可能会降低性能。
总结:
1. DirectByteBuffer使用直接内存而不是Java堆内存,可以提高I/O操作的性能。
2. HeapByteBuffer使用Java堆内存,需要进行数据的复制操作。
3. DirectByteBuffer适用于需要频繁进行I/O操作的场景,而HeapByteBuffer适用于一般的数据处理任务。
netty demo点击查看代码
byte[] bytes ={1,23,3};
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
ByteBuf directBuffer = Unpooled.directBuffer(12,12);
核心原理
reactor 模型
概念:
Reactor 是一种开发模式,模式的核心流程: 注册感兴趣的事件 -> 扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理
单线程模型
①服务器端的Reactor是一个线程对象,该线程会启动事件循环,并使用Selector(选择器)来实现IO的多路复用。注册一个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
②客户端向服务器端发起一个连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,这样一来Reactor就会监听该连接的READ事件了。
③当Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。比如,读处理器会通过SocketChannel的read()方法读取数据,此时read()操作可以直接读取到数据,而不会堵塞与等待可读的数据到来。
④每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理。
注意,Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所有的I/O的accept()、read()、write()以及connect()操作都在一个线程上完成的。
但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
多线程模型
与单线程Reactor模式不同的是,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作者线程池来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。
使用线程池的优势:
①通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程产生的巨大开销。
②另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。
③通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态。同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。
改进的版本中,所以的I/O操作依旧由一个Reactor来完成,包括I/O的accept()、read()、write()以及connect()操作。
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发或大数据量的应用场景却不合适,主要原因如下:
①一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送;
②当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
netty 要点
概要
入门程序解读
点击查看代码
NettyServer.java
package com.atguigu.netty.simple;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
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;
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建BossGroup 和 WorkerGroup
//说明
//1. 创建两个线程组 bossGroup 和 workerGroup
//2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
//3. 两个都是无限循环
//4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
// 默认实际 cpu核数 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
// .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
//给pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
ch.pipeline().addLast(new NettyServerHandler());
}
}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
NettyServerHandler.java
package com.atguigu.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
/**
* 说明
* 1. 我们自定义一个Handler 需要继续netty 规定好的某个HandlerAdapter(规范)
* 2. 这时我们自定义一个Handler , 才能称为一个handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据实际(这里我们可以读取客户端发送的消息)
/**
* 1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
* 2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
System.out.println("server ctx =" + ctx);
System.out.println("看看channel 和 pipeline的关系");
Channel channel = ctx.channel();
ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接, 出站入站
//将 msg 转成一个 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:" + channel.remoteAddress());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush 是 write + flush
//将数据写入到缓存,并刷新
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
}
//处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
NettyClient.java
package com.atguigu.netty.simple;
import io.netty.bootstrap.Bootstrap;
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;
public class NettyClient {
public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
NettyClientHandler.java
package com.atguigu.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client " + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
}
//当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址: " + ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Bootstrap、ServerBootstrap
1. Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
2. 常见的方法有
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),该方法用于服务器端,用来设置两个 EventLoop
public B group(EventLoopGroup group),该方法用于客户端,用来设置一个 EventLoop
public B channel(Class<? extends C> channelClass),该方法用来设置一个服务器端的通道实现
public
public
public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务处理类(自定义的handler)
public ChannelFuture bind(int inetPort),该方法用于服务器端,用来设置占用的端口号
public ChannelFuture connect(String inetHost, int inetPort),该方法用于客户端,用来连接服务器端
Future、ChannelFuture
Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件
Channel
1. Netty 网络通信的组件,能够用于执行网络 I/O 操作。
2. 通过 Channel 可获得当前网络连接的通道的状态
3. 通过 Channel 可获得网络连接的配置参数(例如接收缓冲区大小)
4. Channel 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成
5. 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方
6. 支持关联 I/O 操作与对应的处理程序
7. 不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常用的 Channel 类型:
- NioSocketChannel,异步的客户端 TCP Socket 连接。
- NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
- NioDatagramChannel,异步的 UDP 连接。
- NioSctpChannel,异步的客户端 Sctp 连接。
- NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
常见的方法有
Channel channel(),返回当前正在进行 IO 操作的通道
ChannelFuture sync(),等待异步操作执行完毕
Selector
1.Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
2.当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select)这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel
ChannelHandler 及其实现类
1.ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
2.ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类
3.ChannelHandler 及其实现类一览图(后)
4.我们经常需要自定义一个 Handler 类去继承 ChannelInboundHandlerAdapter,然后通过重写相应方法实现业务逻辑,我们接下来看看一般都需要重写哪些方法
Pipeline 和 ChannelPipeline
ChannelHandlerContext
ChannelOption
EventLoopGroup 和其实现类 NioEventLoopGroup
Unpooled 类
Netty的编解码器
解码器: 将字节解码为消息
编码器: 将消息编码为字节
一次解码器
一次解码器: 解决半包粘包问题的常用三种解码器
io.netty.buffer.ByteBuf (原始数据流)-> io.netty.buffer.ByteBuf (用户数据)
二次解码器
二次解码器: 需要和项目中所使用的对象做转化的编解码器
- Java 序列化
- Marshaling
- XML
- JSON
- MessagePack
- Protobuf
- 其他
编解码速度对比:
2017-05-06 10_30_22-Serialization Performance comparison(XML,Binary,JSON,P…) | HowToAutomate.in.th
MessagePack
MessagePack: It's like JSON. but fast and small. (msgpack.org)
优点:压缩包比高,语言支持丰富,缺点:难懂
编解码器源码分析
NETTY的String编解码器
比较简单,转成字符串
NETTY的Java序列化编解码器
核心类,你可以看出继承了 LengthFieldBasedFrameDecoder
再到编码器中: ObjectEncoder
通过看源码可知,NETTY针对JDK的序列化也做了优化(虽然JDK的序列化本身效率低)
protobuf编解码器
Release Protocol Buffers v21.2 · protocolbuffers/protobuf · GitHub
源码分析
ProtobufVarint32FrameDecoder
这里可以看出来,protobuf采用的是不定长,报文头不是定长的,站在性能角度来说,是可以不浪费空间的。
这样看也大致大知道protobuf非常高效
Netty源码之请求处理流程
Netty源码之启动服务
我们了解到Netty本身是基于JDK的NIO进行优化和改进一个框架,其实Netty本身还是基于JDK的NIO实现,所以我们再次把JDK的NIO的流程拉出来看一看。
从上图可以看到,要启动NIO,那么就需要创建Selector,同样Netty要启动,同样也需要创建Selector。
那么我们可以采用调试跟踪的方式来分析源码。
主线:
主线程
- 创建Selector
- 创建ServerSocketChannel
- 初始化ServerSocketChannel
- 给ServerSocketChannel 从 boss group 中选择一个 NioEventLoop
BossThread
- 将ServerSocketChannel 注册到选择的 NioEventLoop的Selector
- 绑定地址启动
- 注册接受连接事件(OP_ACCEPT)到Selector 上
主线程
创建Selector
调试案例中的启动Netty服务端的代码
最后可以得出:NioEventLoopGroup的构造方法中调用JDK的SelectorProvider中provider()创建 selector
创建ServerSocketChannel
那么我们需要在这里加上断点:
初始化ServerSocketChannel
事件的传播
底层事件的传播使用的就是针对链表的操作
Netty源码之构建连接
从前面我们知道一个技术点,就是绑定IP也好,处理连接也好都是NioEventLoop来做,于是我们来看对应的核心run方法
断点在这里调试一下,同时我们看下ops的标志代表着什么?
在这里我们需要debug一下看下连接事件的发生
然后启动一个客户端连接
再回到服务端的断点,可以看到ops=16,这个是accpct
跟完之后整体流程图就出来了
Netty源码之接收数据
接收数据,也就是读数据(对于服务端来说),也是在NioEventLoop中操作的,不过具体的实现却不同。
具体再回到构建连接之后代码
这里很明显使用到了责任链模式
然后我们回到NioEventLoop中在以下位置打上断点,再次调试运行server,同时发起client调用
第一次进入断点,发现这里是NioServerSocketChannel说明走的是创建连接
第二次进入断点,发现这里是NioSocketChannel,这里走的应该是读事件了
另外也可以观察到两次进入的方法除了对象不同,线程也不同,因为这里采用了主从的方式。
然后我们接着进入来看读数据如何处理的
内存管理机制( 内存使用不当会内存泄露)
1.Bytebuf 的内存结构 (release readIndex writeIndex 三者转换)
-
Unpooled 类 Bytebuf 的一个操作工具类
-
堆内存 非堆内存 (创建方方式)
-
池化和非池化
UnpoolHeapBytebuf JVM 垃圾回收器
UnpoolDerictBytebuf 特殊方式回收
PoolBytebuf 一般不回收,最终具体的回收方式有自己的机制 -
内存释放不及时: 所以需要自己手动释放 referenceCountedUtil.release() ;
-
使用的时机: 因为是handler 链条, 默认是头尾释放,可以追踪DefaulPipeline 查看源码
-
原则
1.最后使用时候释放链接,比如链条中间的时候, 向下传递的已经不在是bytebuf的时候,最后一次使用的时候就应该释放
2.业务里面自己创建的时候也应该释放
附录: 资料推荐
基础知识网站
https://dongzl.github.io/netty-handbook
进阶知识
《netty进阶之路》
《TCP/IP 网络编程》