Netty是什么?
Netty 是一个广泛使用的 Java 网络编程框架,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。Netty 的内部实现时很复杂的,但是 Netty 提供了简单易用的 api 从网络处理代码中解耦业务逻辑。 Netty 是完全基于 NIO 实现的,实际上相当于NIO+多线程,所以整个 Netty 都是异步的。 简单点说就是Netty提供了一个简单,间接的方法来操作网络之间的通讯。
Netty的优势是什么?
-
并发高
-
传输快
-
封装好
为什么并发高?
Netty是基于NIO实现的 ,相较于BIO(单个线程控制单个连接),它的并发性得到了很大的提高(原因在于selector的出现,使得单个线程可以控制多个连接),从下图可以明显看出NIO与BIO的区别:
BIO处理流程
NIO处理流程
在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。
而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是阻塞的,这样就能让一个Thread处理更多的请求了。
为什么传输快?
Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。
Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。
关于零拷贝的内容会在下篇博文介绍。
为什么封装好?
Netty基于NIO,同时也改进了NIO中的一些问题,将改进方法进行了封装,使用户不用在意底层操作,操作更加简便。详细会在Netty的源码分析进行介绍。
基本组件是什么?
-
Channel ----Socket
-
EventLoop ----控制流,多线程处理,并发;
-
ChannelHandler和ChannelPipeline
-
Bootstrap 和 ServerBootstrap
Channel 接口
基本的I/O操作,在基于java 的网络编程中,其基本的构造是 Socket,在jdk中channel是通讯载体,在netty中channel被赋予了更多的功能。
EventLoop 接口
EventLoop 是用来处理连接的生命周期中所发生的事情,EventLoop, channel, Thread 以及 EventLoopGroup 之间的关系如下图:
这几个组件之间的关系总结下就是:
-
一个 EventLoopGroup 包含多个 EventLoop
-
一个 EventLoop 在他的生命周期中只和一个 Thread 绑定
-
所有的 EventLoop 处理的 I/O 事件都将在专有的 Thread 上处理
-
一个 Channel 在他的生命周期中只会注册一个 EventLoop
-
一个 EventLoop 会被分配给多个 Channel;
ChannelHandler 接口
ChannelHandler 充当了所有处理入站和出站数据的应用程序逻辑的容器,ChannelHandler 的生命周期主要指的是 handler 添加到 ChannelPipeline 中,handler 从pipeline 中移除 。ChannelHandler 主要是用来用来用户入站出站的工具,netty 中提供了一些开箱即用的处理器,本文只是介绍常见组件,以及他们之间的关系。
ChannelPipeline 接口
ChannelPipeline 为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline ,这个关联是永久性的。
ChannelHandler 添加到 ChannlePipeline 的过程:
-
一个 ChannelInitializer 的实现被注册到 ServerBootstrop 当中
-
当 ChannelInitializer.initChannel() 方法被调用时,ChannelInitializer 将在 ChannelPipeline 中安装一组自定义的 ChannelHandler
-
ChannelInitializer 将他自己从 ChannelPipeline 中移除;
当 ChannelHandler 被添加到 ChannelPipeline 时,会被分配一个 ChannelHandlerContext ,代表的是 ChannelHandler 和 ChannelPipeline 之间的绑定。
注:
Netty 中发送消息有两种方式,可以直接写入到 Channel 中,也可以写入到和 ChannelHandler 绑定的 ChannelHandlerContext 中。前者会使消息从 ChannelPipeline 当中尾部开始移动,后者会导致消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动。
ChannelHandlerContext 接口
ChannelHandlerContext 主要是用来管理它所关联的 ChannelHandler 和在同一个 ChannlePipeline 中其他的 ChannelHandler 之间的交互。
Bootstrap 和 ServerBootstrap(引导类)
Bootstrap 和 ServerBootstrap 这两个引导类分别是用来处理客户端和服务端的信息,服务器端的引导一个父 Channel 用来接收客户端的连接,一个子 Channel 用来处理客户端和服务器端之间的通信,客户端则只需要一个单独的、没有父 Channel 的 Channel 来去处理所有的网络交互(或者是无连接的传输协议,如 UDP)
这里可以大概先总结一下,之间的关系:
-
一个EventLoopGroup当中包含多个EventLoop
-
一个EventLoop在它的整个生命周期中只于一个thread进行绑定
-
所有由EventLoop所处理的各种I/O操作都将在它关联的Thread上进行处理
-
一个Channel在它的生命周期中只会注册在一个EventLoop上
-
一个EvenLoop在运行当中,会被分配给多个Channel
Netty中的责任链模式
责任链模式是一种对象的行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。
- 从击鼓传花谈起
击鼓传花是一种热闹而又紧张的饮酒游戏。在酒宴上宾客依次坐定位置,由一人击鼓,击鼓的地方与传花的地方是分开的,以示公正。开始击鼓时,花束就开始依次传递,鼓声一落,如果花束在某人手中,则该人就得饮酒。
比如说,贾母、贾赦、贾政、贾宝玉和贾环是五个参加击鼓传花游戏的传花者,他们组成一个环链。击鼓者将花传给贾母,开始传花游戏。花由贾母传给贾赦,由贾赦传给贾政,由贾政传给贾宝玉,又贾宝玉传给贾环,由贾环传回给贾母,如此往复,如下图所示。当鼓声停止时,手中有花的人就得执行酒令。
击鼓传花便是责任链模式的应用。责任链可能是一条直线、一个环链或者一个树结构的一部分。
-
责任链模式的结构
下面使用了一个责任链模式的最简单的实现。
责任链模式涉及到的角色如下所示:
● 抽象处理者(Handler)角色:定义出一个处理请求的接口。如果需要,接口可以定义 出一个方法以设定和返回对下家的引用。这个角色通常由一个Java抽象类或者Java接口实现。上图中Handler类的聚合关系给出了具体子类对下家的引用,抽象方法handleRequest()规范了子类处理请求的操作。
● 具体处理者(ConcreteHandler)角色:具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家。
-
Netty中的责任链
数据传输流,与channel相关的概念有以下四个,上一张图让你了解netty里面的Channel。
-
Channel,表示一个连接,可以理解为每一个请求,就是一个Channel。
-
ChannelHandler,核心处理业务就在这里,用于处理业务请求。
-
ChannelHandlerContext,用于传输业务数据。
-
ChannelPipeline,用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext。
-
Netty将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipline中流动和传递。ChannelPipeline持有IO事件拦截器ChannelHandler的链表,由ChannelHandler对IO事件进行拦截和处理,可以方便的新增和删除ChannelHandler来实现不同的业务逻辑定制,不必对已有的ChannelHandler进行修改,这个开放闭合原则的很好体现。
ChannlePipeline的事件事件处理流程,如下图
- 底层Socket读取bytebuf触发ChannelRead事件(Inbound 事件),由NioEventLoop调用ChannelPipeline的fireChannelRead方法;
- 消息被ChannelPipeline中的ChannelHandlerContext传递,依次被各个ChannelHandler处理;
- 当有写出的需求(Outbound 事件),调用ChannelHandlerContext write方法,消息再通过ChannelHandlerContext反向传递通过各个ChannelHandler处理。当然各个ChannelHadler可以通过定制只对自己感兴趣的消息进行处理,其余跳过。
Netty线程模型
Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理。
单线程模型:所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。
多线程模型:有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。
主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,重新注册到Sub 线程池的线程上,用于处理I/O 的读写等操作,从而保证mainReactor只负责接入认证、握手等操作;
利用主从 NIO 线程模型,可以解决 1 个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在 Netty 的官方 demo 中,推荐使用该线程模型。
事实上,Netty 的线程模型并非固定不变,通过在启动辅助类中创建不同的 EventLoopGroup 实例并通过适当的参数配置,就可以支持上述三种 Reactor 线程模型。正是因为 Netty 对 Reactor 线程模型的支持提供了灵活的定制能力,所以可以满足不同业务场景的性能诉求。
Netty编程实现服务端和客户端
注意:在使用Netty前先要添加Netty的jar包
NettyServer
public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);//主事件组作用是接收新用户连接,并交给工作事件组
NioEventLoopGroup worker = new NioEventLoopGroup();//工作事件组的作用是接收主事件组提交的用户连接并做相应处理
try {
//服务端启动相关辅助类,作用是将接收连接和用户逻辑处理相关的配置进行初始化
ServerBootstrap serverBootstrap = new ServerBootstrap()
//将主/工作事件组进行配置
.group(boss, worker)
//设置主事件组处理的通道类型
.channel(NioServerSocketChannel.class)
//配置工作事件组相关的Handler
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
ChannelPipeline pipeline = nioSocketChannel.pipeline();
pipeline.addLast(new NettyServerHandler());
pipeline.addLast(new StringDecoder());//定义接收类型为将字符串转为ByteBuf
pipeline.addLast(new StringEncoder());//定义发送类型为将ByteBuf转为字符串
}
});
//同步阻塞绑定端口,真正启动服务器
ChannelFuture sync = serverBootstrap.bind(1234).sync();
System.out.println("服务端绑定端口成功且启动:");
//同步阻塞关闭服务端
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//关闭主事件组和工作事件组
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
NettyServerHandler
//自定义实现Handler,用于对客户端发送数据进行读取
public class NettyServerHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String s = new String(bytes);
System.out.println("server收到消息:"+s);
//给客户端回复消息
ByteBuf res = Unpooled.copiedBuffer(("server确认收到消息:"+s).getBytes());
ctx.channel().writeAndFlush(res);
}
//用户新连接消息:accept事件连接上触发该操作
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SocketAddress address = ctx.channel().remoteAddress();
System.out.println(address+"上线了...");
}
//用户下线
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
SocketAddress address = ctx.channel().remoteAddress();
System.out.println(address+"下线了...");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
NettyClient
public class NettyClient {
public static void main(String[] args) {
NioEventLoopGroup loopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(loopGroup)
.channel(NioSocketChannel.class)
.remoteAddress("127.0.0.1",1234)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
ChannelPipeline pipeline = nioSocketChannel.pipeline();
pipeline.addLast(new NettyClientHandler());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
}
});
//同步阻塞连接服务端
Channel channel = bootstrap.connect().sync().channel();
//发消息
String msg = new String("hello");
channel.writeAndFlush(msg);
//关闭通道
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//关闭事件组
loopGroup.shutdownGracefully();
}
}
}
NettyClientHandler
public class NettyClientHandler extends SimpleChannelInboundHandler{
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String s = new String(bytes);
System.out.println("client接收消息:"+"\n"+s);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SocketAddress address = ctx.channel().remoteAddress();
System.out.println(address+"正在连接...");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
运行结果:
server
client
注:若要实现客户端循环发送消息,服务端循环接收消息,只需要将NettyClient类中发消息的部分稍加改动即可。