从前面几篇博文中我们已经学习了从BIO到NIO再到Reactor线程模型+NIO实现了客户端与服务端的一个通信,但这样写起代码来还是比较繁琐的,而Netty则极大的简化我们这种网络通信的开发,它是基于NIO之上的也同时实现了Reactor线程模型,而且使用JDK自带的NIO需要了解很多复杂的概念,一不小心就bug飞起,既然这样那我们就一起来学习下这个神秘的网络框架吧。
Netty概念介绍
Netty是由JBOSS提供的一个java开源框架,是一个高性能、高可扩展性的异步事件驱动的网络应用程序框架,它极大地简化了TCP和UDP客户端和服务器开发等网络编程。
- 设计特点:统一的API,支持多种传输类型(阻塞和非阻塞),简单而强大的线程模型,真正的无连接数据报套接字支持,基于灵活且可扩展的事件模型,可以清晰地分离关注点。
- 易于使用:翔实的Javadoc和大量的实例集 没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了。(一些可选的特性可 能需要Java1.7+或额外的依赖)
- 性能:拥有比Java的核心API更高的吞吐量以及更低的延迟 得益于池化和复用(Unpooled.buffer()非池化和Unpooled.directBuffer()池化),拥有更低的资源消耗 最小化不必要的内存复制
- 健壮性:不会因为慢速、快速或超载的连接而导致OutOfMemoryError 消除在高速网络中NIO应用程序常见的不公平读/写比率
- 安全性: 完整的SSL / TLS和StartTLS支持 ,并且拥有丰富活跃的社区
多Reactor线程模型
为了让netty更好的处理任务,也实现了Reactor线程模型,四个核心概念:1.Resources 资源(请求/任务) 2. Request Handler 请求处理器 3.Dispatcher 分配 4.Synchronous Event Demultiplexer 同步事件复用器,它的线程模型跟我之前一篇博文提到的多Reactor线程模型很类似(https://blog.csdn.net/qq_40826106/article/details/93404421),mainReactor线程去处理客户端的连接请求,并分配subReactor线程组去处理,基于一个subReactor线程里就有一个selector所以可以监听多个客户端,一旦客户端发来了请求,subReactor线程又去分配线程池或者其它业务线程中的业务线程去处理业务。唯一不同的就是subRector自己有一个工作队列,这个是响应给客户端的,就是说当我们IO耗时比较长时,subReactor不会一直在阻塞,而是响应式的事件驱动,会等线程池处理完业务之后,把该返回结果或者说业务放到subReactor队列中,然后等到它空闲时,它再去处理队列中的任务。可以说是尽善尽美了。
职责链设计模式
除了上述的多Reactor线程模型还涉及到了职责链模式(Chain of Responsibility Pattern),这也是一种设计模式,就是为请求创建了一个处理对象的链,发送请求和处理请求进行一个解耦,职责链上的处理者负责处理请求,客户只需要请求发送到职责链上即可,无需关心请求的处理细节和请求的传递,这样即使你需要增加新的工序,也只要加一个AbstractHandler的实现类即可,就跟我们工厂流水线一样,先经过工序一,再把工序一完成的半成品交给工序二,然后再将工序二的半成品交给工序三,类似。有点类似于我们的链表结构,那么现在就用代码来实现以下netty中的职责链模式。这里推荐:https://www.processon.com/ 作为类图或者流程的分析,非常强大好用,主要是免费,推荐给大家(不过文件超过9个就要收费了)。
package com.dongnaoedu.network.humm.netty;
/**
* @author Heian
* @time 19/07/20 21:18
* @description:链表形式调用,netty就是类似的这种形式
*/
public class PipelineDemo {
//因为只有HandlerChainContext里有工序实现这个属性,所以只有通过这个HandlerChainContext来看它是否有
HandlerChainContext headContext = new HandlerChainContext (new AbstractHandler () {
@Override
void doHandler(HandlerChainContext chainContext, Object args) {
chainContext.runNext (args);
}
});
/**
* 从第一个节点是headContext,它这个类保存的工序为handler1,所以先会执行工序一的具体实现,如果该节点的还有下个节点,
* 则继续执行下个节点的方法,类似于递归执行,所以每个工序的方法都能得到执行
*/
void doProcessHandler(Object args){
this.headContext.runCurrent (args);
}
//增加工序
public void addCircle(AbstractHandler abstractHandler){
//每次添加一个对象,都会从头到尾添加一遍,保证添加的元素是处于最尾部
HandlerChainContext context = headContext;
while (context.chainContextNext != null){
context = context.chainContextNext;
}
//把传入的工序赋值给下一个节点
context.chainContextNext = new HandlerChainContext (abstractHandler);
}
public static void main(String[] args) {
PipelineDemo pipeline = new PipelineDemo ();
//此时头部节点 已经加载出来(类的初始化:局部变量通过关键字new加载)
pipeline.addCircle (new Handler1 ());// header1
pipeline.addCircle (new Handler2 ());// header1
pipeline.addCircle (new Handler2 ());// header1
//分别执行每一个节点所包含的工序实现方法的信息
pipeline.doProcessHandler ("火车头");
}
}
/**
* 下面就写一个字符串的叠加 1 + 2 +3...
*/
//第一步:定义处理器抽象类(就是工序的抽象) 和 负责维护链和链的执行
//工序的抽象:参数1:chainContext 参数2:传入的参数
abstract class AbstractHandler{
abstract void doHandler(HandlerChainContext chainContext,Object args);
}
//节点,承上启下负责A流程--》B流程的执行
class HandlerChainContext{
//既然承上启下,就必须要知道下一个工序和处理工序的实现
HandlerChainContext chainContextNext;
AbstractHandler abstractHandler;
//构造方法:切换上下文只需要关心抽象工序即可
public HandlerChainContext(AbstractHandler abstractHandler){
this.abstractHandler = abstractHandler;
}
//运行当前节点信息
void runCurrent(Object args){
this.abstractHandler.doHandler (this,args);
}
//节点运行下一个具体实现的方法,并且保存节点信息
void runNext(Object args){
if (this.chainContextNext != null){
this.chainContextNext.runCurrent (args);
}
}
}
//第二步:工序的实现类
class Handler1 extends AbstractHandler{
@Override
void doHandler(HandlerChainContext chainContext, Object args) {
args = args.toString () + "我是工序一 ";
System.out.println ("One:" + args);
chainContext.runNext (args);
}
}
class Handler2 extends AbstractHandler{
@Override
void doHandler(HandlerChainContext chainContext, Object args) {
args = args.toString () + "我是工序二 ";
System.out.println ("Two:" + args);
chainContext.runNext (args);
}
}
//备注:以节点为中心展开思维,节点保存节点和对应节点的工序。
//One:火车头我是工序一
//Two:火车头我是工序一 我是工序二
//Two:火车头我是工序一 我是工序二 我是工序二
Pipeline管道保存了通道所有处理器信息,当客户端连接进来,每一个channel时自动创建一个专有的pipeline。 入站事件和出站操作会调用pipeline上的处理器。上面的伪代码Context是单向的,只有指向下一个Context,而在Netty中是双向的,其中保存了headContex头部和tail尾部Context,如果你要添加一个MyContext,则默认是按照先进先出原则加到中间。
inbound入站事件 | 描述 | outbound出站事件 | 描述 |
fireChannelRegistered | channel注册事件 | bind | 端口绑定时间 |
fireChannelUnregistered | channel解除注册事件 | connect | 连接时间 |
fireChannelActive | channel活跃事件 | disconnect | 断开连接事件 |
fireChannelInactive | channel非活跃事件 | close | 关闭事件 |
fireExceptionCaught | 异常事件 | deregister | 解除注册事件 |
fireUserEventTriggered | 用户自定义事件 | flush | 刷新数据到网络事件 |
fireChannelRead | channel读事件 | read | 读事件,用于op_read到selector |
fireChannelReadComplete | channel读取完成事件 | write | 写事件 |
fireChannelWritabilityChanged | channel写状态变化事件 | writeAndFlush | 写出数据事件 |
- 入站事件:通常指I/O线程生成了入站数据。 (通俗理解: 从socket底层自己往上冒上来的事件都是入站) 比如EventLoop收到selector的OP_READ/Accept事件,入站处理器调用socketChannel.read(ByteBuffer) 接收到数据后,这将导致通道的ChannelPipeline中包含的下一个中的channelRead方法被调用。
- 出站事件:经常是指I/O线程执行实际的输出操作。 (通俗理解:想主动往socket底层操作的事件的都是出站) 比如bind方法用意是请求server socket绑定到给定的SocketAddress,这将导致通道的 ChannelPipeline中包含的下一个出站处理器中的bind方法被调用。应用层(Tcp)往传输层(Http等)写数据:写数据、连接服务器、邦迪端口、连接关闭等等。
- ChannelHandler:用于处理I/O事件或拦截I/O操作,并转发到ChannelPipeline中的下一个处理器。 这个顶级接口定义功能很弱,实际使用时会去实现下面两大子接口: 处理入站I/O事件的ChannelInboundHandler、处理出站I/O操作的ChannelOutboundHandler
- ChannelHandlerContext:实际存储在Pipeline中的对象并非ChannelHandler,而是上下文对象。 将handler,包裹在上下文对象中,通过上下文对象与它所属的ChannelPipeline交互,向上或向下传递事件 或者修改pipeline都是通过上下文对象。
- 适配器类:为了开发方便,避免所有handler去实现一遍接口方法,Netty提供了简单的实现类: ChannelInboundHandlerAdapter处理入站I/O事件 ChannelOutboundHandlerAdapter来处理出站I/O操作 ChannelDuplexHandler来支持同时处理入站和出站事件。
ChannelPipeline是线程安全的,ChannelHandler(你要添加的流水线作业)可以在任何时候添加或删除。 例如,你可以在即将交换敏感信息时插入加密处理程序,并在交换后删除它。 一般操作,初始化的时候增加进去,较少删除。下面是Pipeline中管理handler的API。
Netty 的核心组件
- Channel:Netty中自己定义的Channel,增强版的通道概念
- EventLoop:由线程驱动,处理Channel的所有I/O事件
- ChannelPipeline:事件处理机制
- ChannelHandler:事件处理器
- ByteBuf:增强的ByteBuf缓冲区
- Bootstrap:启动器,引导Netty应用程序启动
Netty实现通信Demo
服务端代码:
package com.dongnaoedu.network.humm.netty.echo;
import com.dongnaoedu.network.netty.echo.EchoServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* @author Heian
* @time 19/07/21 23:40
* @description:服务端
*/
public class NettyServer {
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// 1、创建EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup (1);//类比mainReactor
EventLoopGroup workerGroup = new NioEventLoopGroup();//类比subReactor线程组 默认大小是cpu核心数X2
final ServerHandler serverHandler = new ServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();// 2、创建启动器
b.group(bossGroup, workerGroup)// 3、配置启动器
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler (LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel> () {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(serverHandler);//3、向流水线添加作业 ChannelHandler
}
});
// 等待客户端连接
ChannelFuture f = b.bind(PORT).sync();// 4、启动器启动
f.channel().closeFuture().sync();// 5、等待服务端channel关闭,不关闭则一直阻塞
} finally {
// 6.释放资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务端业务代码:
package com.dongnaoedu.network.humm.netty.echo;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @author Heian
* @time 19/07/21 23:40
* @description:服务端添加需要入栈的
*/
//ChannelHandler接口的子类 ChannelInboundHandlerAdapter-->ChannelInboundHandler-->ChannelHandler
public class ServerHandler extends ChannelInboundHandlerAdapter {
//读取通道内消息,并返回给客户端
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("收到客户端数据,还给客户端:" + msg);
ctx.write(msg);
}
//读取完成刷新
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
//异常则关闭ChannelHandlerContext连接
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
客户端代码:
package com.dongnaoedu.network.humm.netty.echo;
import com.dongnaoedu.network.netty.echo.EchoClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* @author Heian
* @time 19/07/21 23:41
* @description:客户端
*/
public class NettyClient {
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// 1、创建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup ();//类比subReactor线程组 默认大小是cpu核心数X2
try {
Bootstrap b = new Bootstrap();//// 2、创建启动器
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel> () {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new ClientHandler ());//3、向流水线添加作业 ChannelHandler
}
});
// 连接服务端
ChannelFuture f = b.connect(HOST, PORT).sync();// 4、启动器启动
f.channel().closeFuture().sync();// 5、等待服务端channel关闭,不关闭则一直阻塞
} finally {
// 6.释放资源
group.shutdownGracefully();
}
}
}
客户端业务代码:
package com.dongnaoedu.network.humm.netty.echo;
import com.dongnaoedu.network.netty.echo.EchoClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* @author Heian
* @time 19/07/21 23:41
* @description:客户端
*/
public class NettyClient {
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// 1、创建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup ();//类比subReactor线程组 默认大小是cpu核心数X2
try {
Bootstrap b = new Bootstrap();//// 2、创建启动器
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel> () {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new ClientHandler ());//3、向流水线添加作业 ChannelHandler
}
});
// 连接服务端
ChannelFuture f = b.connect(HOST, PORT).sync();// 4、启动器启动
f.channel().closeFuture().sync();// 5、等待服务端channel关闭,不关闭则一直阻塞
} finally {
// 6.释放资源
group.shutdownGracefully();
}
}
}
上述代码完成一个回复的功能:
截取部分客户端控制台打印信息:
- 给服务器发送数据:UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 256, cap: 256)
- 收到服务端数还给服务器:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 1024)
- 收到服务端数还给服务器:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 1024)
- 收到服务端数还给服务器:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 512)
- 收到服务端数还给服务器:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 512)
截取部分服务端控制台打印信息:
- 收到客户端数据,还给客户端:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 1024)
- 收到客户端数据,还给客户端:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 1024)
- 收到客户端数据,还给客户端:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 512)
- 收到客户端数据,还给客户端:PooledUnsafeDirectByteBuf(ridx: 0, widx: 256, cap: 512)
jar包成本地源码,方便调试和阅读
小提示:如果你想要更好的阅读源码,建议利用IDE的工具特性去更换源码地址,这样可以在源码写注释,方便自己理解记忆,还可以利用自带的show Diagraims来查看类图的层级关系。(netty源码注释版本:https://github.com/nbpeak/netty/tree/netty-4.1.34)