一、Netty简介
Netty是一个高性能、异步事件驱动的网络应用框架,提供了对TCP、UDP和文件传输的支持,用于快速开发可维护的高性能服务器和客户端。作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
Netty常见使用场景
互联网行业
在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC框架使用。
典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
游戏行业
无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。
非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信
大数据领域
经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service基于Netty框架二次封装实现
二、REACTOR 模型
Reactor模式(反应器模式)是一种处理一个或多个客户端并发交付服务请求的事件设计模式。当请求抵达后,服务处理程序使用I/O多路复用策略,然后同步地派发这些请求至相关的请求处理程序。
Netty中的Reactor模型主要由多路复用器(Acceptor)、事件分发器(Dispatcher)、事件处理器(Handler)组成。
单线程Reactor模式
所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用却不合适,主要原因如下:
-
不能充分利用多核cpu的性能,一个线程同时处理成百上千的链路,性能上无法支撑,即便CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送;
-
当负载过重后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
-
一旦单线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障,可靠性不高。

多线程Reactor模式
多线程模型的特点:
-
有专门一个Acceptor线程用于监听服务端,接收客户端的TCP连接请求;
-
网络IO的读写操作由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;
-
一个NIO线程可以同时处理多条链路,但是一个链路只能对应一个NIO线程,防止发生并发操作问题。
在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是,在极特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型。

主从Reactor模型
采用两个reactor,每个reactor都在自己单独的线程里执行。如果是多核,则可以同时响应多个客户端的请求,一旦链路建立成功就将链路注册到负责I/O读写的SubReactor线程池上。

mainReactor —> NioEventLoopGroup
subReactor —> NioEventLoopGroup
acceptor —> ServerBootstrapAcceptor
ThreadPool —> 用户自定义线程池
事实上,Netty的线程模型并非固定不变,在启动辅助类中创建不同的EventLoopGroup实例并通过适当的参数配置,就可以支持上述三种Reactor线程模型。正是因为Netty对Reactor线程模型的支持提供了灵活的定制能力,所以可以满足不同业务场景的性能需求。
简易客户端、服务器端demo
//服务器端
public class EchoServer {
private final int port;
public EchoServer(int port)
{
this.port=port;
}
public void start() throws Exception
{
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup work =new NioEventLoopGroup();
try
{
ServerBootstrap b=new ServerBootstrap();
b.group(boss,work).channel(NioServerSocketChannel.class).localAddress(port) // 这里告诉Channel如何接收新的连接
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception
{
// 自定义处理类
ch.pipeline().addLast(new EchoServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口,开始接收进来的连接
ChannelFuture f = b.bind(port).sync();
System.out.println(EchoServer.class.getName()+" started and listen on "+f.channel().localAddress());
// 等待服务器socket关闭
f.channel().closeFuture().sync();
}catch (Exception e){
System.out.println(e);
}
finally {
boss.shutdownGracefully().sync();
work.shutdownGracefully().sync();
}
}
public static void main(String[] agrs)throws Exception
{
System.out.println("Server_star");
new EchoServer(8001).start();
System.out.println("Server_end");
}
}
//handler
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception
{
System.out.println("客户端连上...");
}
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception
{
try {
ByteBuf in = (ByteBuf) msg;
System.out.println( new Date().toString() );
System.out.println("收到的:"+in.toString(CharsetUtil.UTF_8));
ctx.write(Unpooled.copiedBuffer("你好, I am server!", CharsetUtil.UTF_8));
ctx.flush();
}catch (Exception e){
System.out.println("异常"+e);
}finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception
{
try {
ctx.flush();
}catch (Exception e){
System.out.println("异常"+e);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception
{
System.out.println("exceptionCaught:"+ctx);
cause.printStackTrace();
ctx.close();
}
}
//客户端
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host,int port)
{
this.host=host;
this.port=port;
}
public void start() throws Exception
{
EventLoopGroup group =new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class).remoteAddress(new InetSocketAddress(host,port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception
{
ch.pipeline().addLast(new EchoClientHandler());
}
});
Channel channel = b.connect().sync().channel();
channel.closeFuture().sync();
}finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] agrs) throws Exception
{
System.out.println("client_star");
new EchoClient("localhost",8001).start();
}
}
//handle
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception
{
System.out.println("服务端连上了");
ctx.write(Unpooled.copiedBuffer("Hello, I am client!", Charset.forName("UTF-8")));
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception
{
cause.printStackTrace();
ctx.close();
}
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf msg) throws Exception {
try {
System.out.println( new Date().toString() );
System.out.println("Client Received!: "+ msg.toString(CharsetUtil.UTF_8));//
}catch (Exception e){
System.out.println("异常"+e);
}
}
}
三、组件
Bootstrap、ServerBootstrap
Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
他两有两点区别:
-
ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而 Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。
-
引导一个客户端只需要一个 EventLoopGroup,但是一个 ServerBootstrap 则需要两个(也可以是同一个实例)。
-
第一组只包含一个 ServerChannel,代表服务 器自身的已绑定到某个本地端口的正在监听的套接字。
-
第二组将包含所有已创建的用来处理传入客户端连接的 Channel。
-
Channel、EventLoop
Channel
Channel 是 Netty 网络通信的组件,能够用于执行网络 I/O 操作,内部封装了 Java NIO。Channel 为用户提供:
-
当前网络连接的通道的状态(例如是否打开?是否已连接?)
-
网络连接的配置参数 (例如接收缓冲区大小)
-
提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
-
支持关联 I/O 操作与对应的处理程序。
不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。下面是一些常用的 Channel 类型:
-
NioSocketChannel,异步的客户端 TCP Socket 连接。
-
NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
-
NioDatagramChannel,异步的 UDP 连接。
-
NioSctpChannel,异步的客户端 Sctp 连接。
-
NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
EventLoop
EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中发生的事件。下图说明了 Channel、EventLoop 和 EventLoopGroup 的关系:
-
一个 EventLoopGroup 包含一个或者多个 EventLoop;
-
一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
-
所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
-
一个 Channel 在它的生命周期内只注册于一个 EventLoop;
-
一个 EventLoop 可能会被分配给一个或多个 Channel。

ChannelHandler 、ChannelHandlerContext和 ChannelPipeline

ChannelHandler
ChannelHandler 是和应用开发人员接触最多的,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 可专 门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,或者处理转换过程 中所抛出的异常。
ChannelHandler 分为处理入站事件和数据的 ChannelInboundHandler,和处理出站事件和数据的 ChannelOutboundHandler。在 Netty 里提供了很多有用的实现,比如支持 protobuf 序列化的 ProtobufEncoder 和 ProtobufDecoder。
Netty 还提供了一些适配器类:ChannelHandlerAdapter、ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter,提供了一些默认实现,让开发变得更简单了。
ChannelHandlerContext
每个ChannelHandler通过add方法加入到ChannelPipeline中去的时候,会创建一个对应的ChannelHandlerContext,并且绑定,ChannelPipeline实际维护的是ChannelHandlerContext 的关系
ChannelPipeline
ChannelPipeline 提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站和出站事件流的 API。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。
下图说明了一个 Netty 应用程序中入站和出站数据流之间的区别。从一个客户端应用程序 的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。
入站和出站 ChannelHandler 可以被安装到同一个 ChannelPipeline 中。如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部 开始流动,并被传递给第一个 ChannelInboundHandler。这个 ChannelHandler 不一定 会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个 ChannelInboundHandler。最终,数据将会到达 ChannelPipeline 的尾端,届时,所有 处理就都结束了。
数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从 ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为 Socket。通常情况下,这将触发一个写操作。
下图描述了 Channel 与 ChannelHandler、ChannelPipeline 之间的关系,AbstractChannel 里包含了 DefaultChannelPipeline 对象,DefaultChannelPipeline 里包含了一个 DefaultChannelHandlerContext 链,DefaultChannelHandlerContext 里包含了一个 ChannelHandler 对象。

(虚线箭头表示依赖关系 如Channel只有依赖于ChannelPipeline时Channel才能发挥作用
实线箭头表示关联关系 ----如图中每一个AbstractChannel对象都必须有一个 DefaultChannelPipeline对象
虚线三角形箭头表示接口实现关系
空心的菱形表示聚合关系 ------如DefaultChannelPipeline包含AbstrantChannelHandlerContext对象,但是AbstrantChannelHandlerContext脱离了DefaultChannelPipeline仍然有存在(也就是部分脱离整体仍然有意义)
实心的菱形表示组合关系 -----如AbstractChannel包含DefaultChannelPipeline对象,但是DefaultChannelPipeline脱离了AbstractChannel不能单独存在(“部分”脱离了“整体”便不复存在)
)
Nio的ByteBuffer 对比 netty的Bytebuf
参考:
TCP 粘包/拆包问题
TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取的字节数是不定的,故可能存在以下四种情况。
(1) 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2) 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3) 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4) 服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第5种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
原因:
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
等等。
解法:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
NETTY 中解决办法(各种类型的解码器)
1,LineBasedFrameDecoder 以换行符标志结束
2,DelimiterBasedFrameDecoder 以分隔符做标志符
3,FixedLengthFrameDecoder 固定长度
参考:
https://www.jianshu.com/nb/7269354
https://cloud.tencent.com/developer/article/1360553
9006

被折叠的 条评论
为什么被折叠?



