Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。拥有比Java核心API更高的吞吐量以及更低的延迟。
Netty高效的原因:零拷贝(引用)、责任链(高效扩展)、内存池
阻塞IO缺点:
(1)大量线程处于休眠状态
(2)需要为每个线程的调用栈分配内存
(3)上下文切换所带来的开销会非常麻烦
非阻塞IO (NIO)
class java.nio.channels.Selector 是Java的非阻塞I/O实现的关键,它使用了事件通知API以确定在一组非阻塞套接字中有哪些已经就绪能够进行I/O相关的操作。
单一的线程便可以处理多个并发的连接
特点:
(1)使用较少的线程可以处理许多连接,因此也减少了内存管理和上下文切换所带来的开销
(2)当没有I/O操作需要处理的时候,线程也可以被用于其他任务
编写Echo服务器
- 至少一个ChannelHandler
- 引导
每个Channel都拥有一个与之相关联的ChannelPipeline,其持有一个ChannelHandler的实例链,在默认情况下,ChannelHandler会把对它的方法的调用转发给链中的下一个ChannelHandler
服务器实现步骤:
- 创建一个ServerBootstrap的实例以引导和绑定服务器
- 创建并分配一个NioEventLoopGroup实例以进行事件的处理,如接收新连接以及读写数据
- 指定服务器绑定的本地的InetSocketAddress
- 使用一个EchoServerHandler的实例初始化每一个新的Channel
- 调用ServerBootstrap.bind()方法以绑定服务器
编写Echo客户端
实现步骤:
- 为初始化客户端,创建了一个Bootstrap实例
- 为进行事件处理分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据
- 为服务器连接创建一个InetSocketAddress
- 当连接被创建时,一个EchoClientHnadler实例会被安装到ChannelPipeline中
- 在一切都设置完成后,调用Bootstrap.connect()方法连接到远程节点
运行:
mvn exec:java -Dexec.mainClass=“EchoServer” -Dexec.args=“8055”
mvn exec:java -Dexec.mainClass=“EchoClient” -Dexec.args=“localhost 8055”
Netty核心组件
组件 | 功能 |
---|---|
Channel | Socket |
EventLoop | 控制流,多线程处理,并发 |
ChannelFuture | 异步通知 |
Channel :数据载体
代表一个实体的开放链接,如读操作写操作。可以看做传入传出数据的载体。(可以被打开或者关闭,连接或者断开)。Netty的Channel接口所提供的API,大大降低了Socket类的复杂性。
Channel拥有许多预定义的,专门实现的广泛类层次结构的根:
- EmbeddedChannel
- LocalServerChannel
- NioDatagramChannel
- NioSctpChannel
- NioSocketChannel
- Channel是线程安全的
Channel的状态:
ChannelRegistered (Channel被注册到EventLoop)
–> ChannelActive (Channel处于活动状态)
–> ChannelInactive (Channel没有连接到远程节点)
–> ChannelUnregistered (Channel 已经被创建,未被注册到EventLoop)
ChannelFuture:在addListener方法中注册了一个ChannelFutureListener,以便在某个操作完成时得到通知
EventLoop:处理事件,执行任务
EventLoop定义了Netty的核心抽象,用户处理生命周期中所发生的事件。
采用了两个基本的API:并发、网络编程。
一个EventLoopGroup包含一个或多个EventLoop,一个EventLoop在它的生命周期内只和一个Thread绑定
所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理
一个Channel在它的生命周期内只注册一个EventLoop,一个EventLoop可能会被分配给一个或多个Channel
使用EventLoop,创建任务稍后执行
ScheduledFuture<?> future = ch.eventLoop().schedule(
new Runnale(){
@Override
public void run(){
...
}
},60,TimeUnit.SECONDS
);
使用EventLoop,周期性的任务
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
new Runnale(){
@Override
public void run(){
...
}
},60,60,TimeUnit.SECONDS
);
EventLoopGroup
ChannelHandler:数据的逻辑容器
入站和出站数据的应用程序逻辑的容器(应用程序的业务逻辑)
Netty以适配类的形式提供了大量默认ChannelHandler实现,简化逻辑开发过程
- ChannelHandlerAdapter
- ChannelInboundHandlerAdapter
- ChannelOutboundHandlerAdapter
- ChannelDuplexHandler
ChannelHnadler的典型应用包括:
- 将数据从一种格式转换为另一种格式
- 提供异常的通知
- 提供Channel变为活动或非活动的通知
- 提供Channel注册到EventLoop或者从EventLoop注销时的通知
- 提供有关用户自定义事件的通知
- ChannelInboundHandler 接口
- ChannelOutboundHandler 接口
参考:https://blog.csdn.net/MOVIE14/article/details/75077817
ChannelHandlerContext:
ChannelHandlerContext和ChannelHandler之间是关联的
ChannelHandlerContext使得ChannelHandler能够和它的ChannelPipeline以及其他的ChannelHandler配合工作。
每当有ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandlerContext。
ChannelHandler可以用@Sharable标注,可以从属多个ChannelPipline
(在多个ChannelPipline中安装多个ChannelHandler用于收集多个Channel的统计性信息)
异常:ChannelHandler.exceptionCaught() 默认将异常转发给ChannelPipline中的下一个ChannelHnadler,如果达到ChannelPipline的尾端,将会被记录为未被处理。
ChannelPipeline
ChannelPipeline提供了ChannelHandler链的容器,并定义了用于在该链上传播入站和出站时间流的API。ChannelHandler的执行顺序与被添加的顺序一致。
当ChannelHnadler被添加到ChannelPipeline时,它将会被分配一个ChannelHandlerContext,代表了ChannelHandler和ChannelPipeline之间的绑定。
ChannelPipeline实现了常用的设计模式:拦截器模式。
ChannelPipeline会查看下一个ChannelHandler是否匹配,如果不匹配会跳过进入下一个
ChannelPipline可以通过remove,replace修改handler布局
netty中我们会添加一些处理器,如果对其添加流程及内部结构不清楚经常会出现各种问题,我先给出一幅处理器整体排版图
addFist添加Handler是将Handler放在Head后面
addLast添加Handler是将Handler放在Tail的前面
消息接收时是从Head开始向Tail流动直到某个handler没有将事件传递下去,或者tail结束(事件未传递是对应handler未调用ctx.fireChannelRead())
消息发送一般是从Tail开启到head结束。当用户调用ctx.pipeline().writeAndFlush(xxx)输出流是从tail开启。如果我们在某个handler执行ctx.writeAndFlush(xx)那么事件是从当前Handler向Head流出。如果存在某个Handler未调用ctx.write()则事件会到此结束无法向head流动
参考:https://blog.csdn.net/yinbucheng/article/details/90710293
引导
引导涉及到:
(1)将一个进程绑定到某一个指定的端口
(2)或将一个进程连接到另一个运行在某个指定主机的指定端口上的进程
服务端:ServerBootstrap将绑定到一个端口,因为服务器必须要监听连接
客户端:Bootstrap则是由想要连接到远程节点的客户端应用程序所使用的
在引导中只能添加一个Handler,如果需要将多个Hnadler注册到一个ChannelPipline中,需要提供ChannelInitializer实现(initChannel方法)
Channel属性的批量配置:ChannelOption
bootstrap.option(ChannelOption.SO_KEEPALIVE,true)
option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000);
基于UDP:DatagramChannel
回调:已经被提供给另一个方法的方法的引用(处理触发的事件)
Future:提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;在未来某个时刻完成,并提供给其结果的访问。
Netty提供了ChannelFuture,用户在执行异步操作的时候使用(回调的更精细版本)
Channel channel = ...;
ChannelFuture future = channel.connect(new InetSocketAddress("..."));
future.addListener(new ChannelFutureListener(){
@Override
public void operationComplete(Channel future){
if(future.isSuccess()){
ByteBuf buffer = Unpooled.copiedBuffer("Hello",Charset.defaultCharset());
ChannelFuture wf = future.channel().writeAndFlush(buffer);
... ...
}else {
Throwable cause = future.cause();
cause.printStackTrace();
}
}
}
});
Netty编程基本逻辑(以Server为例)
关系梳理:
引导—>EventLoopGroup—>EventLoop—>各个Channel(事件的载体)
—>Channel内部有Pipeline—>Pipeline中有各个Handler—>使用ByteBuf
public void start() throws Exception {
final EchoServerHandler serverHandler = new EchoServerHandler();//Hnadler处理逻辑
EventLoopGroup group = new NioEventLoopGroup();//EventLoop组
try {
ServerBootstrap b = new ServerBootstrap();//引导
//组装引导、EventLoopGroup、channel类型、地址、ChannelHandler调用链
b.group(group).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(serverHandler);//pipeline: ChannelHandler链
}
});
//引导执行
ChannelFuture f = b.bind().sync();
//结束
f.channel().closeFuture().sync();
} finally {
//关闭EventLoop组
group.shutdownGracefully().sync();
}
}
Netty的其他版本:
- 不通过Netty的OIO
- 不通过Netty的NIO
- 基于选择器(注册表),可以请求在Channel的状态发生变化时得到通知。
- 通过Netty的OIO
- 通过Netty的NIO
https://github.com/vicotorz/NettyDemo/tree/master/src/main/java/Demo
Nio | io.netty.channel.socket.nioio.netty.channel.epoll 非阻塞代码库或者常规起点 |
Oio | io.netty.channel.socket.oio 阻塞代码库 |
Local | io.netty.channel.local 在同一个JVM内部通信 |
Embedded | io.netty.channel.embedded 测试ChannelHandler实现单元测试 |
ByteBuf (Netty的数据容器)
Java Nio提供了ByteBuffer作为字节容器,但使用起来过于复杂。
Netty提供了ByteBuf,解决了ByteBuffer的局限性。
- abstract class ByteBuf
- interface ByteBufHolder
ByteBuf优点:
- 它可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长
- 在读和写两种模式之间切换不需要调用ByteBuffer的flip()方法
- 读和写使用了不同的索引(读索引,写索引)(JDK的ByteBuffer只有一个索引,因此需要调用flip())
- 支持方法的链式调用
- 支持引用计数
- 支持池化
1.堆缓存区
支撑数组模式,在没有使用池化的情况快速的分配和释放,JVM堆中
ByteBuf heapBuf = ...;
if(heapBuf.hasArray()){
int length = heapBuf.readableBytes();
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
byte[] array = heapBuf.array();
handleArray(array,offset,length);
}
2.直接缓冲区
使用直接缓冲区,分配和释放代价比较高,适用于网络传输
ByteBuf directBuf = ...;
if(!directBuf.hasArray()){
int length = directBuf.readableBytes();
byte[] array = new byte[length];
directBuf.getBytes(directBuf.readerIndex(),array);
handleArray(array,0,length);
}
3.复合缓冲区
CompositeByteBuf 将多个缓冲区 表示为单个合并缓冲区的虚拟表示(为多个ByteBuf提供一个聚合视图)
例如:HTTP协议传输(头和主体),可以使用CompositeByteBuf的方式进行封装。
ByteBuffer[] message = new ByteBuffer[]{header,body}
ByteBuffer message2 = ByteBuffer.allocate(header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip();
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = ...
ByteBuf bodyBuf = ...
messageBuf.addComponents(headerBuf,bodyBuf);
messageBuf.removeComponent(0);//删除位于索引位置为0的ByteBuf
//读取数据
int length = compBuf.readableBytes();
byte[] array = new byte[length];
compBuf.getBytes(compBuf.readerIndex(),array);//将字节读入到数组中
handleArray(array,0,array.length);
可丢弃字节(discardReadBytes()),可读字节,可写字节
图(p59)
markReaderIndex()
markWriterIndex()
resetReaderIndex()
resetWriterIndex()
readerIndex(int)
writerIndex(int)
clear() 比 discardReadBytes()轻量得多,只是重置索引而不会复制任何内存
process(byte value)
forEachByte(ByteBufProcessor.FIND_NUL)
派生缓冲区:
- duplicate()
- slice()
- slice(int,int)
- Unpooled.unmodifiableBuffer(…)
- order(ByteOrder)
- readSlice(int)
ByteBufHolder ?
ByteBufAllocator
实现了ByteBuf池化,可以分配任意类型的ByteBuf实例,通过引用计数的方式降低内存分配开销
- PooledByteBufAllocator:最大限度减少内存碎片
- UnpooledByteBufAllocator:每次调用都会返回一个新的实例
ByteBufUtil
用于操作ByteBuf的静态的辅助方法
例如:hexdump(),以十六进制的形式打印ByteBuf的内容
ByteBuf 复制:
如果需要一个现有缓冲区的真实副本,使用copy() 或者 copy(int,int)
Netty提供了class ResourceLeakDetector对应用程序缓冲区做大约1%的采样检测内存泄漏
编码、解码器
将字节解码为消息:
ByteToMessageDecoder、ReplayingDecoder、MessageToByteEncoder、MessageToMessageEncoder
将一种消息类型解码为另一种:
MessageToMessageDecoder
CombinedChannelDuplexHandler
SSLHandler
管理连接:
- idleStateHandler 连接空闲时间时长
- ReadTimeoutHandler 如果在指定时间没有收到入站数据,则抛出一个Read-TimeoutException异常
- WriteTimeoutHandler 如果在指定时间没有收到出站数据,则抛出一个WriteTimeoutException异常
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel>{
protected void initChannel(Channel ch) throws Exception{
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(0,0,60,TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatHandler());
}
public static final class HeartbeatHandler extends ChannelInboundHandlerAdapter{
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleaseableBuffer(
Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("HEARTBEAT",CharsetUtil.IOS_8859_1));
)
}
public void userEventTriggered(ChannelHandlerContext ctx,Object evt) throws Exception{
if(evt instanceof IdleStateEvent){
ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}else{
super.userEventTriggered(ctx,evt);
}
}}
- 基于分隔符协议:LineBasedFrameDecoder
- 基于长度的协议:FixedLengthFrameDecoder
- 写大型数据(基于FileRegion)
netty粘包与拆包
UDP不存在粘包与拆包,UDP是基于报文发送的,UDP首部采用了16bit来指示UDP数据报文的长度。可以将不同的数据报文区分开,避免粘包和拆包的问题
TCP是基于字节流的,应用层和TCP传输层之间的数据交互是大小不等的数据块,TCP把这些数据块看成一连串无结构的字节流,没有边界,且首部没有表示数据长度的字段,可能发生粘包。
产生原因
要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
来自 https://blog.battcn.com/2017/09/01/netty/netty-4/
解决:添加边界信息、或将没个数据包封装固定长度、在数据包之间添加特殊符号
netty提供的decoder:
DelimiterBasedFrameDecoder 基于消息边界方式进行粘包拆包处理的。 FixedLengthFrameDecoder 基于固定长度消息进行粘包拆包处理的。 LengthFieldBasedFrameDecoder 基于消息头指定消息长度进行粘包拆包处理的。 LineBasedFrameDecoder 基于行来进行消息粘包拆包处理的。