目录
# 介绍
dk1.4提供了NIO,使用select/polld模型,jdk1.5使用epoll替换了前者。jdk1.7提供了NIO2.0,AsynchronousSeverSocketChannel和CompletionHandler参数实现异步。它是真正的异步非阻塞IO,Netty用的是epoll模型,
https://blog.csdn.net/zhuguanghalo/article/details/84799409最大连接数不修改配置的话13000个左右。修改配置的话30w可以达到,100也可以达到,https://www.cnblogs.com/crazymakercircle/archive/2018/11/05/9911981.html,但是考虑到还有业务逻辑,100万级别下8核16G的情况相下可以测试下。
为什么Netty使用NIO而不是AIO?
1.Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化
2.Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱,把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来.
3.AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多
# java内存模型
JVM规范定义了Java内存模型(java memory model)来屏蔽掉各种操作系统,虚拟机实现厂商和硬件的内存访问差异,以确保java程序在所有操作系统和平台上能够实现一次编写,到处运行的效果。
1.工作内存和主内存
2. java内存交互协议
java内存模型定义了8种操作来完成主内存和工作内存的变量访问
java的线程
线程是比进程更轻量级的的调度执行单元:它可以把进程的资源分配和调度执行分开,各个线程可以共享内存、I/O等操作系统资源,但是又能够被操作系统发起的内核线程或者进程执行。线程的实现方式主要有三种:
1.内核线程(KTL)实现,这种线程有内核来完成线程切换,内核通过线程调度器对线程进行调度,并负责将线程任务映射到不同处理器上。
# 线程模型
Rector多线程模型:单线程模型只是少了下面的ReactorPool,所有操作都由ReactorThread处理。
主从Reactor多线程模型
特点:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的线程池。Accepter接收到客户端连接TCP连接请求并处理完成后将创建新的SocketChannel注册到I/O线程池(subreactor)的某个I/O上,由它负责SocketChannel的读写和编码操作,Acceptor线程池仅仅用于客户单登录、握手、安全认证,一旦建立成功,就将链路注册到后端subreactor线程池的I/O线程上。由I/O线程负责后续的I/O操作。
Reactor线程的保护
尽管Reactor线程主要处理IO操作,发生的异常通常是IO异常,但是实际上在一些特殊的场景下回发生非IO异常,如果仅仅捕获IO异常可能会导致Reactor线程跑飞,为了防止这种意外,在循环体内一定要捕获Throwable,而不是IO异常或者Exception。
Netty可以设置单、多、主从线程模型,推荐使用主从
服务端启动的时候创建了两个NioEventLoopGroup,一个用于接收客户端TCP请求,一个用于处理I/O。在I/O线程内部进行串行化操作,避免了多线程竞争导致的性能下降,只要用户不主动切换线程,一直都由NioEventLoop调用用户的Handler,期间不线程切换。就是管道中的有序操作,通过调整NIO线程池的串行化线程数量,可以同时启动多个串行化的线程并运行,这种局部无锁串行化比一个队列多个工作线程模型性能更优。
对于高性能的 RPC(远程过程调用 Remote Procedure Call) 框架,Netty 作为异步通信框架,几乎成为必备品。例如,Dubbo 框架中通信组件,还有 RocketMQ 中生产者和消费者的通信,都使用了 Netty。今天,我们来看看 Netty 的基本架构和原理。
Netty的逻辑架构:它采用了典型的三层网络架构进行设计和开发。
Rector通信调度层
ChannelPopLine职责链
Service ChannelHandler 业务逻辑遍拍层
# Netty 的架构设计是如何实现高性能的
1.采用异步非阻塞的I/O类库,基于Reactor模式实现,解决了传统同步阻塞I/O模式下一个服务端无法平滑的处理线性增长的客户端问题。
2.TCP 接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制,提升了I/O的读取和写入性能。
3.支持通过内存池的方式循环利用ByteBuf,避免了频繁的创建和销毁Bytebuf带来的性能消耗。
4.可配置的I/O线程数,TCP参数,为不同的用户场景提供制定化的调优参数。
5.采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器或者锁。
6.合理的使用线程安全容器,原子类等,提升系统的并发能力
7.关键资源的处理用单线程串行化的方式,避免了多线程并发访问带来的锁竞争和额外CPU资源消耗。
8.通过引用计数器及时的申请释放不再被引用的对象,细粒度的内存管理降低了GC 的频率,减少了频繁GC 带来的时延增大和CPU损耗。
可靠性
由于长连接不需要每次发送消息都创建链路,也不需要在消息交互完成时关闭链路,因此相对于短连接性能更高。
内存保护机制
ByteBuf的解码保护,防止非法码流导致内存溢出。
一旦使用内存池机制,对象的声明周期有内存池管理,通常是个全局引用,如果不显示释放JVM是不会回收这部分内存的。为了防止因为用户遗漏导致内存泄漏,Netty在Pipeline的尾Handler中自动对内存进行释放。
流量整形
1.防止由于上下游网元性能不均衡导致下游网元被压垮,业务流程中断
2.防止由于通信模块接收消息过快,后端业务线程处理不及时导致的‘撑死’问题。
全局流量整形:进程级
无论你创建多少个Channel,它的作用域针对所有的Channel。流量整形的阈值limit越大,流量整形的精度就越高,他是一种可靠性屏障,但无法做到100%的精确。
优雅停机
Netty5.0版本的退出做的更加完善,当系统退出时,JVM通过注册的Shutdown Hook拦截到退出信号量,然后执行退出操作。释放相关模块的资源占用,将缓冲区的消息处理完成或清空,将待刷新的数据持久化到磁盘或者数据库中,等到资源回收和缓冲区消息处理完成后,再退出。优雅停机往往要设置最大超时时间。
从一个简单的例子开始:
NettyServer 启动以后会监听某个端口的请求,当接受到了请求就需要处理了。在 Netty 中客户端请求服务端,被称为“入站”操作,服务发出的消息称作“出站”消息。
sever端:
package netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.util.CharsetUtil;
import java.net.InetSocketAddress;
public class EchoSever {
private final int port;
public EchoSever(int port)
{
this.port = port;
}
public static void main(String[] args) throws Exception {
EchoSever sever = new EchoSever(1106);
sever.start();
}
public void start() throws Exception
{
final EchoSeverHandle severHandle = new EchoSeverHandle();
//(1)create EventGroup
EventLoopGroup group = new NioEventLoopGroup();
// create SeverBootstrap
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
//指定用来传输的NIO Channel
.channel(NioServerSocketChannel.class)
//指定套接字
.localAddress(new InetSocketAddress(port))
//添加一个EchoSeverHandle到Channel的ChannelPipLine
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(severHandle);
}
});
//异步地绑定服务器:调用sync方法阻塞等待直到绑定完成
ChannelFuture f = b.bind().sync();
System.out.println(EchoSever.class.getName() +
" started and listening for connections on " + f.channel().localAddress());
//获取Channel的CloseFuture 并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
}
finally {
//关闭EventLoopGroup释放所有资源
group.shutdownGracefully().sync();
}
}
class EchoSeverHandle extends ChannelInboundHandlerAdapter
{
//当接收到消息时的操作
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf)msg;
//将消息输出到控制台
System.out.println("Sever reveive " + in.toString(CharsetUtil.UTF_8));
//将接收到的消息写给发送者,而不冲刷出站消息
ctx.write(in);
}
//消息读取完成时的方法
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//将消息重刷到远程节点,并且关闭channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
//出现异常时的方法
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//打印异常栈跟踪
cause.printStackTrace();
//关闭channel
ctx.close();
}
}
}
client端
package netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.CharsetUtil;
import java.net.InetSocketAddress;
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port)
{
this.host = host;
this.port = port;
}
public static void main(String [] args) throws Exception {
EchoClient client = new EchoClient("127.0.0.1", 1106);
client.start();
}
public void start() throws Exception
{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
//指定EventLoopGroup以处理客户端事件
b.group(group)
//指定传输的Channel
.channel(NioSocketChannel.class)
//设置套接字
.remoteAddress((new InetSocketAddress(host, port)))
//创建Channel时,向ChannelPopeLine中
//添加一个EchoClientHandle实例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandle());
}
});
//连接到远程节点,阻塞等待直到完成
ChannelFuture f = b.connect().sync();
//阻塞 直到channel关闭
f.channel().closeFuture().sync();
}finally {
//关闭线程池并且释放所有资源
group.shutdownGracefully().sync();
}
}
class EchoClientHandle extends SimpleChannelInboundHandler<ByteBuf>{
@Override
public void channelActive(ChannelHandlerContext ctx)
{
//当被通知Channel是活跃的时候,发送一条消息
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
//输出已接收的消息
System.out.println("Client reveive " + byteBuf.toString(CharsetUtil.UTF_8));
}
//发生错误关闭channel
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{
cause.printStackTrace();
ctx.close();
}
}
}
# Netty 核心组件
通过上面的简单例子,发现有些 Netty 组件在服务初始化以及通讯时被用到,下面就来介绍一下这些组件的用途和关系。
①Channel
通过上面例子可以看出,当客户端和服务端连接的时候会建立一个 Channel。
这个 Channel 就是底层NIO的channel通道,它负责基本的 IO 操作,例如:bind(),connect(),read(),write() 等等。
简单的说,Channel 就是代表连接,实体之间的连接,程序之间的连接,文件之间的连接,设备之间的连接。同时它也是数据入站和出站的载体。
②EventLoop 和 EventLoopGroup
既然有了 Channel 连接服务,让信息之间可以流动。如果服务发出的消息称作“出站”消息,服务接受的消息称作“入站”消息。那么消息的“出站”/“入站”就会产生事件(Event)。
例如:连接已激活;数据读取;用户事件;异常事件;打开链接;关闭链接等等。
顺着这个思路往下想,有了数据,数据的流动产生事件,那么就有一个机制去监控和协调事件。
这个机制(组件)就是 EventLoop。在 Netty 中每个 Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。
每个 EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件(Netty 4.0)。
理解了 EventLoop,再来说 EventLoopGroup 就容易了,EventLoopGroup 是用来生成 EventLoop 的,还记得例子代码中第一行就 new 了 EventLoopGroup 对象。一个 EventLoopGroup 中包含了多个 EventLoop 对象。EventLoopGroup 要做的就是创建一个新的 Channel,并且给它分配一个 EventLoop。
EventLoopGroup,EventLoop 和 Channel 的关系
在异步传输的情况下,一个 EventLoop 是可以处理多个 Channel 中产生的事件的,它主要的工作就是事件的发现以及通知。
相对于以前一个 Channel 就占用一个 Thread 的情况。Netty 的方式就要合理多了。
客户端发送消息到服务端,EventLoop 发现以后会告诉服务端:“你去获取消息”,同时客户端进行其他的工作。
当 EventLoop 检测到服务端返回的消息,也会通知客户端:“消息返回了,你去取吧“。客户端再去获取消息。整个过程 EventLoop 就是监视器+传声筒。
③ChannelHandler,ChannelPipeline 和 ChannelHandlerContext
如果说 EventLoop 是事件的通知者,那么 ChannelHandler 就是事件的处理者。
在 ChannelHandler 中可以添加一些业务代码,例如数据转换,逻辑运算等等。
正如上面例子中展示的,Server 和 Client 分别都有一个 ChannelHandler 来处理,读取信息,网络可用,网络异常之类的信息。
并且,针对出站和入站的事件,有不同的 ChannelHandler,分别是:
ChannelInBoundHandler(入站事件处理器,netty接收客户端消息)ChannelOutBoundHandler(出站事件处理器,netty发往客户端消息)不同的handler继承自入站或出战事件处理器,这样就区分了入站操作还是出战操作,比如:MessageToMessageEncoder和MessageToMessageDecoder,即编码(pojo->二进制)和解码(二进制->pojo)。
假设每次请求都会触发事件,而由 ChannelHandler 来处理这些事件,这个事件的处理顺序是由 ChannelPipeline 来决定的。
ChannelHanlder 处理,出站/入站的事件。
ChannelPipeline 为 ChannelHandler 链提供了容器。到 Channel 被创建的时候,会被 Netty 框架自动分配到 ChannelPipeline 上。
ChannelPipeline 保证 ChannelHandler 按照一定顺序处理事件,当事件触发以后,会将数据通过 ChannelPipeline 按照一定的顺序通过 ChannelHandler。
说白了,ChannelPipeline 是负责“排队”的。这里的“排队”是处理事件的顺序。
同时,ChannelPipeline 也可以添加或者删除 ChannelHandler,管理整个队列。
如上图,ChannelPipeline 使 ChannelHandler 按照先后顺序排列,信息按照箭头所示方向流动并且被 ChannelHandler 处理。
说完了 ChannelPipeline 和 ChannelHandler,前者管理后者的排列顺序。那么它们之间的关联就由 ChannelHandlerContext 来表示了。
每当有 ChannelHandler 添加到 ChannelPipeline 时,同时会创建 ChannelHandlerContext 。
ChannelHandlerContext 的主要功能是管理 ChannelHandler 和 ChannelPipeline 的交互, 负责传递消息。
不知道大家注意到没有,开始的例子中 ChannelHandler 中处理事件函数,传入的参数就是 ChannelHandlerContext。
ChannelHandlerContext 参数贯穿 ChannelPipeline,将信息传递给每个 ChannelHandler,是个合格的“通讯员”。
Netty 的 Bootstrap
Bootstrap 的作用就是将 Netty 核心组件配置到程序中,并且让他们运行起来。底层通过门面模式对各种能力进行抽象和封装,尽量不需要用户过多的底层API打交道。
从 Bootstrap 的继承结构来看,分为两类分别是 Bootstrap 和 ServerBootstrap,一个对应客户端的引导,另一个对应服务端的引导。
客户端引导 Bootstrap,主要有两个方法 bind() 和 connect()。Bootstrap 通过 bind() 方法创建一个 Channel。
在 bind() 之后,通过调用 connect() 方法来创建 Channel 连接。
服务端引导 ServerBootstrap,与客户端不同的是在 Bind() 方法之后会创建一个 ServerChannel,它不仅会创建新的 Channel 还会管理已经存在的 Channel。
通过上面的描述,服务端和客户端的引导存在两个区别
ServerBootstrap(服务端引导)绑定一个端口,用来监听客户端的连接请求。而 Bootstrap(客户端引导)只要知道服务端 IP 和 Port 建立连接就可以了。
Bootstrap(客户端引导)需要一个 EventLoopGroup,但是 ServerBootstrap(服务端引导)则需要两个 EventLoopGroup。
因为服务器需要两组不同的 Channel。第一组 ServerChannel 自身监听本地端口的套接字。第二组用来监听客户端请求的套接字。
# Netty 的数据容器
前面介绍了 Netty 的几个核心组件,服务器在数据传输的时候,产生事件,并且对事件进行监控和处理。
接下来看看数据是如何存放以及是如何读写的。Netty 将 ByteBuf 作为数据容器,来存放数据。
ByteBuf 工作原理
从结构上来说,ByteBuf 由一串字节数组构成。数组中每个字节用来存放信息。
ByteBuf 提供了两个索引,一个用于读取数据readerIndex,一个用于写入数据writeIndex。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。与NIO底层的ByteBuffer一致。
当从 ByteBuf 读取时,它的 readerIndex(读索引)将会根据读取的字节数递增。
同样,当写 ByteBuf 时,它的 writerIndex 也会根据写入的字节数进行递增。
需要注意的是极限的情况是 readerIndex 刚好读到了 writerIndex 写入的地方。
如果 readerIndex 超过了 writerIndex 的时候,Netty 会抛出 IndexOutOf-BoundsException 异常。
Netty为了防止溢出,对write操作进行了封装,都对剩余可用空间进行校验,如果可用缓冲区不足,ByteBuf会自动j进行动态扩展,但不能超过最大容量,降低了Bytebuf的学习和使用成本。
ByteBuf 使用模式
谈了 ByteBuf 的工作原理以后,再来看看它的使用模式。
根据存放缓冲区的不同分为三类:
1.堆缓冲区,ByteBuf 将数据存储在 JVM 的堆中,通过数组实现,可以做到快速分配。
由于在堆上被 JVM 管理,在不被使用时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。
2.直接缓冲区,在 JVM 的堆之外直接分配内存,用来存储数据。其不占用堆空间,使用时需要考虑内存容量。
它在使用 Socket 传递时性能较好,因为间接从缓冲区发送数据,在发送之前 JVM 会先将数据复制到直接缓冲区再进行发送。
由于,直接缓冲区的数据分配在堆之外,通过 JVM 进行垃圾回收,并且分配时也需要做复制的操作,因此使用成本较高。
3.复合缓冲区,顾名思义就是将上述两类缓冲区聚合在一起。Netty 提供了一个 CompsiteByteBuf,可以将堆缓冲区和直接缓冲区的数据放在一起,让使用更加方便。
ByteBuf 的分配
聊完了结构和使用模式,再来看看 ByteBuf 是如何分配缓冲区的数据的。
1.Netty 提供了两种 ByteBufAllocator 的实现,他们分别是:
2.PooledByteBufAllocator,实现了 ByteBuf 的对象的池化,提高性能减少内存碎片。Unpooled-ByteBufAllocator,没有实现对象的池化,每次会生成新的对象实例。
对象池化的技术和线程池,比较相似,主要目的是提高内存的使用率。池化的简单实现思路,是在 JVM 堆内存上构建一层内存池,通过 allocate 方法获取内存池中的空间,通过 release 方法将空间归还给内存池。
对象的生成和销毁,会大量地调用 allocate 和 release 方法,因此内存池面临碎片空间回收的问题,在频繁申请和释放空间后,内存池需要保证连续的内存空间,用于对象的分配。
基于这个需求,有两种算法用于优化这一块的内存分配:伙伴系统和 slab 系统。
伙伴系统,用完全二叉树管理内存区域,左右节点互为伙伴,每个节点代表一个内存块。内存分配将大块内存不断二分,直到找到满足所需的最小内存分片。
内存释放会判断释放内存分片的伙伴(左右节点)是否空闲,如果空闲则将左右节点合成更大块内存。
slab 系统,主要解决内存碎片问题,将大块内存按照一定内存大小进行等分,形成相等大小的内存片构成的内存集。
按照内存申请空间的大小,申请尽量小块内存或者其整数倍的内存,释放内存时,也是将内存分片归还给内存集。
Netty 内存池管理以 Allocate 对象的形式出现。一个 Allocate 对象由多个 Arena 组成,每个 Arena 能执行内存块的分配和回收。
Arena 内有三类内存块管理单元:
TinySubPage
SmallSubPage
ChunkList
Tiny 和 Small 符合 Slab 系统的管理策略,ChunkList 符合伙伴系统的管理策略。
当用户申请内存介于 tinySize 和 smallSize 之间时,从 tinySubPage 中获取内存块。
申请内存介于 smallSize 和 pageSize 之间时,从 smallSubPage 中获取内存块;介于 pageSize 和 chunkSize 之间时,从 ChunkList 中获取内存;大于 ChunkSize(不知道分配内存的大小)的内存块不通过池化分配。
# Tcp粘包/拆包
TCP粘包和拆包发生的原因
1.应用程序write写入的字节大小大于套接口发送的缓冲区大小。
2.进行MSS大小的TCP 分段
3.以太网的payload大于MTU进行ip分片
解决:
利用LineBasedFrameDecoder解决TCP粘包的问题:
只要支持半包解码的handler: LineBasedFrameDecoder和StringDecoder添加到服务端和客户端的ChannelPipeline中即可
package nio;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.util.Date;
public class NettyTimeServer {
public static void main(String []args) throws InterruptedException {
int port = 8080;
new NettyTimeServer().bind(port);
}
private void bind(int port) throws InterruptedException {
//创建Reactor线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();//用于接受客户端的连接
EventLoopGroup workGroup = new NioEventLoopGroup();//用于进行SocketChannel的读写。
try {
ServerBootstrap b = new ServerBootstrap();//启动Netty服务端的辅助启动类,降低开发复杂度
b.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannnelHandler());
//绑定端口,同步阻塞等待成功
ChannelFuture f = b.bind(port).sync();
//阻塞等待服务端链路关闭后退出main
f.channel().closeFuture().sync();
}
finally {
bossGroup.shutdownGracefully();//释放资源
workGroup.shutdownGracefully();
}
}
private class ChildChannnelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
System.out.println("服务器收到连接请求");
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new TimerSeverHandler());
}
}
private class TimerSeverHandler extends ChannelInboundHandlerAdapter {
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{
String body = (String) msg;
System.out.println("The time server receive order : " + body + " ; the counter is : " + ++counter);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString()
: "BAD ORDER";
currentTime = currentTime + System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);//为了防止频繁的唤醒Selector进行消息发送,write方法不直接将消息写入SocketChannel中,
//只是把待发消息放到缓存数组中。调用flush方法将缓冲区的内容写到SocketChannel中
}
//出现异常时的方法
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
package nio;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.nio.ByteBuffer;
public class NettyTimeClient {
public static void main(String []args) throws InterruptedException {
new NettyTimeClient().connect(8080, "127.0.0.1");
}
public void connect(int port, String host) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new TimerClientHandler());
}
});
//发起异步连接请求 然后调用同步方法等待连接成功
ChannelFuture f = b.connect(host, port).sync();
//等待客户端链关闭
f.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
private class TimerClientHandler extends SimpleChannelInboundHandler<ByteBuffer>
{
private byte[] req;
private int counter;
public TimerClientHandler()
{
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuffer byteBuffer) throws Exception {
System.out.println("Client reveive counter is " + ++counter + " " + byteBuffer.toString());
}
@Override
public void channelActive(ChannelHandlerContext ctx)
{
//当被通知Channel是活跃的时候,发送一条消息
ByteBuf message = null;
for (int i = 0; i < 100; i++)
{
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
System.out.println("已连接");
}
//发生错误关闭channel
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{
ctx.close();
}
}
}
DelimiterBasedFrameDecoder:自动完成以分隔符(自定义:比如$_)做结束标志的消息的解码。
FixedLengthFrameDecoder :自动完成对定长消息的解码。无论一次接收到多少服务器,它都会按照构造函数中设置的固定长度解码,。如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达h后进行拼包。。直到读取一个完成的包。
三个解码器都配合其他的解码器如字符串解码器StringDecoder完成解码,只需要将解码器添加到管道起始位即可, 字符串编码器紧随其后,然后是处理handler。
# MessagePack编解码
一个高效的二进制序列化框架,支持不同语言交换,性能快,序列化后的码流也很小,
Netty提供的LengthFieldBasedFrameDecoder(解码器)和LengthFieldPrepender(编码器)可以解决半包黏包的问题
package messagepack;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.CharsetUtil;
import java.net.InetSocketAddress;
import java.util.List;
public class EchoSever {
private final int port;
public EchoSever(int port)
{
this.port = port;
}
public static void main(String[] args) throws Exception {
EchoSever sever = new EchoSever(1106);
sever.start();
}
public void start() throws Exception
{
final EchoSeverHandle severHandle = new EchoSeverHandle();
//(1)create EventGroup
EventLoopGroup acceptGroup = new NioEventLoopGroup();
EventLoopGroup ioGroup = new NioEventLoopGroup();
// create SeverBootstrap
try {
ServerBootstrap b = new ServerBootstrap();
b.group(acceptGroup, ioGroup)
//指定用来传输的NIO Channel
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
//添加一个EchoSeverHandle到Channel的ChannelPipLine
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast("frameDecoder",
new LengthFieldBasedFrameDecoder(65535,0, 2, 0, 2));
socketChannel.pipeline().addLast("msgpack decoder", new MsgPackDecoder());
socketChannel.pipeline().addLast("frameEncoder",
new LengthFieldPrepender(2));
socketChannel.pipeline().addLast("msgpack encoder", new MsgpckEncoder());
socketChannel.pipeline().addLast(severHandle);
}
});
//异步地绑定服务器:调用sync方法阻塞等待直到绑定完成
ChannelFuture f = b.bind(port).sync();
System.out.println(EchoSever.class.getName() +
" started and listening for connections on " + f.channel().localAddress());
//获取Channel的CloseFuture 并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
}
finally {
//关闭EventLoopGroup释放所有资源
acceptGroup.shutdownGracefully();
ioGroup.shutdownGracefully();
}
}
class EchoSeverHandle extends ChannelHandlerAdapter
{
int count;
//当接收到消息时的操作
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//将消息输出到控制台
List<Object> ls = (List<Object>) msg;
for(Object item : ls)
{
System.out.println(item);
//ctx.writeAndFlush(item);
}
ctx.writeAndFlush(msg);
System.out.println("count" + ++count);
}
//出现异常时的方法
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//打印异常栈跟踪
cause.printStackTrace();
//关闭channel
ctx.close();
}
}
}
package messagepack;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.util.CharsetUtil;
import java.net.InetSocketAddress;
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public static void main(String [] args) throws Exception {
EchoClient client = new EchoClient("127.0.0.1", 1106);
client.start();
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
//指定EventLoopGroup以处理客户端事件
b.group(group)
//指定传输的Channel
.channel(NioSocketChannel.class)
//设置套接字
.remoteAddress((new InetSocketAddress(host, port)))
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
//创建Channel时,向ChannelPopeLine中
//添加一个EchoClientHandle实例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast("frameDecoder",
new LengthFieldBasedFrameDecoder(65535,0, 2, 0, 2));
socketChannel.pipeline().addLast("msgpack decoder", new MsgPackDecoder());
socketChannel.pipeline().addLast("frameEncoder",
new LengthFieldPrepender(2));
socketChannel.pipeline().addLast("msgpack encoder", new MsgpckEncoder());
socketChannel.pipeline().addLast(new EchoClientHandle(100));
}
});
//连接到远程节点,阻塞等待直到完成
ChannelFuture f = b.connect(host, port).sync();
//阻塞 直到channel关闭
f.channel().closeFuture().sync();
}finally {
//关闭线程池并且释放所有资源
group.shutdownGracefully().sync();
}
}
class EchoClientHandle extends ChannelHandlerAdapter {
int sendNumber;
public EchoClientHandle(int sendNumber) {
this.sendNumber = sendNumber;
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
//当被通知Channel是活跃的时候,发送一条消息
MessagePackTest [] infos = getInfo();
for(MessagePackTest item : infos)
{
ctx.write(item);
}
ctx.flush();
}
private MessagePackTest[] getInfo() {
MessagePackTest[] info = new MessagePackTest [sendNumber];
MessagePackTest msg = null;
for (int i = 0; i < sendNumber; i++)
{
msg = new MessagePackTest(i, "ABCD-》》》" + i);
info[i] = msg;
}
return info;
}
@Override
public void channelRead(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
//输出已接收的消息
System.out.println("Client reveive " + msg);
//channelHandlerContext.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext channelHandlerContext) throws Exception {
channelHandlerContext.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{
cause.printStackTrace();
ctx.close();
}
}
}
package messagepack;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.msgpack.MessagePack;
public class MsgpckEncoder extends MessageToByteEncoder<Object> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
MessagePack msg = new MessagePack();
byte[] raw = msg.write(o);
byteBuf.writeBytes(raw);
}
}
package messagepack;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.MessageToMessageDecoder;
import org.msgpack.MessagePack;
import java.util.List;
public class MsgPackDecoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
final byte[] arry;
final int length = byteBuf.readableBytes();
arry = new byte[length];
byteBuf.getBytes(byteBuf.readerIndex(), arry, 0, length);
MessagePack msg = new MessagePack();
list.add(msg.read(arry));
}
}
package messagepack;
import org.msgpack.annotation.Message;
@Message
public class MessagePackTest {
public MessagePackTest()
{}
public MessagePackTest(int age, String name){
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
int age;
String name;
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "MessagePackTest[name = " + name + ",age = " + age + "]";
}
}
这里编解码都有list参数是因为 类似下面解码的时候用out接收参数然后finally里遍历获取list
里的每个参数然后传给下一个管道的handler,所以我们业务里channelRead的时候调用
channelRead0的时候只有一个Object参数,最后handler通过泛型定义了方法参数。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
CodecOutputList out = CodecOutputList.newInstance();
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I cast = (I) msg;
try {
decode(ctx, cast, out);
} finally {
ReferenceCountUtil.release(cast);
}
} else {
out.add(msg);
}
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
int size = out.size();
for (int i = 0; i < size; i ++) {
ctx.fireChannelRead(out.getUnsafe(i));
}
out.recycle();
}
}
#Google Protobuf编解码
1.支持 C++、java和Python
2.编码后的消息更小,更加利于存储和传输
3.编解码的性能非常高
4.向前兼容
5.支持可选和必选字段
# Netty解决epoll死循环bug
epoll死循环bug发生的原因:
1.根据该bug的特征,首先侦测该bug是否发生
2.将问题selector上注册的Channel转移到新建的selector上
3.老的问题selector关闭,使用新的selector替换
# Netty的最佳实战
1.创建两个NioEventLoopGroup 用于逻辑隔离NIO Acceptor和NIO I/O线程。
2.尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务除外)。
3.解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息解码。
4.如果业务逻辑操作简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作,数据库操作,网络操作等,可以直接在NIO线程上完成业务逻辑编排,不需要切换用户线程。
5.如果业务逻辑复杂,不要在NIO线程上完成,建议将解码后的Pojo消息封装成task,派发到其他业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的I/O操作。
6.不要在ChannelHandler中调用ChannelFuture的await方法。这会导致死锁,原因是发起I/O操作之后,由于I/O线程负责异步通知发起I/O操作的用户线程,如果I/O线程和用户线程在同一个县城,就会导致I/O线程等待自己通知操作完成,这就导致了死锁,自己把自己死锁。
7.消息发送的队列ChannelOutBoudBuffer并没有容量限制,它会随着消息的积压自动扩展,直到达到0X7fffffff.如果网络对方处理速度比较慢,导致TCP滑窗长时间为0,或者消息发送速度过快,或者一次批量发送消息量过大,可能导致ChannelOutBoudBuffer的内存膨胀,导致内存溢出,建议优化方式如下,在启动客户端或者服务端的时候通过启动项的ChannelOption设置发送队列长度。
公式一:线程数量 = (线程总时间/瓶颈资源时间)* 瓶颈资源的线程并行数。
公式二:QPS = 1000/ 线程总时间 * 线程数
8.I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf,性能达到最优。
9.当某个ChannelInboundHandler的实现重写channelRead()方法时,它将负责显式地释放与池化的ByteBuf实例相关的内存。Netty为此提供了一个实用方法ReferenceCountUtil.release() 但是以这种方式管理资源可能很繁琐。
一个更加简单的方式是使用SimpleChannelInboundHandler。 由于SimpleChannelInboundHandler会自动释放资源,所以你不应该存储指向任何消息的引用供将来使用,因为这些引用都将会失效。
10.@Sharable注解:ChannelHandlerAdapter还提供了实用方法isSharable()。如果其对应的实现被标注为Sharable,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline中。否则,试图将它添加到多个ChannelPipeline时将会触发异常。显而易见,为了安全地被用于多个并发的Channel(即连接),这样的ChannelHandler必须是线程安全的。
正常情况下同一个ChannelHandler,的不同的实例会被添加到不同的Channel管理的管线里面的,但是如果你需要全局统计一些信息,比如所有连接报错次数(exceptionCaught)等,这时候你可能需要使用单例的ChannelHandler,需要注意的是这时候ChannelHandler上需要添加@Sharable注解。
# 业务线程池
主从模型中服务端bind时通过反射生成NioServerSocketChannel实例,该实例绑定的NioEventLoop,轮询接听客户端连接,连接成功后创建NioSocketChannel实例,创建实例的同时会绑定reactor的子线程池中的一个NioEventLoop,并且会为channel创建一个新的DefaultChannelPipeline和AbstractNioUnsafe辅助类,多个客户端公用一个NioEventLoop线程。
如果监听到有可执行的操作,就会通过SelectionKey绑定的channel的辅助类NioByteUnsafe来read和flush,然后调用 DefaultChannelPipeline的fireChannelRead方法,从头结点head开始执行fireChannelRead方法(每个handler被封装成DefaultChannelHandlerContext),开始执行管道中的handler,如果未指定业务线程池,会用pipeline所绑定的nioeventloop线程执行业务逻辑,如果在addLast的时候从执行的线程池中指定一个指定一个EventExecutor保存在DefaultChannelHandlerContext(封装handler)中,这样执行逻辑的时候就会是固定的线程,最后做动态添加handler的时候处理。
增加业务线程池主要有两种方式:1.在pipeline最后addlast一个线程池来处理业务逻辑:
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
//指定EventLoopGroup以处理客户端事件
EventExecutorGroup businessGroup = new DefaultEventExecutorGroup(2);//业务线程池
b.group(group)
//指定传输的Channel
.channel(NioSocketChannel.class)
//设置套接字
.remoteAddress((new InetSocketAddress(host, port)))
//创建Channel时,向ChannelPopeLine中
//添加一个EchoClientHandle实例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//socketChannel.pipeline().addLast(new EchoClientHandle());
socketChannel.pipeline().addLast(businessGroup, new EchoClientHandle());
}
});
//连接到远程节点,阻塞等待直到完成
ChannelFuture f = b.connect().sync();
//阻塞 直到channel关闭
f.channel().closeFuture().sync();
}finally {
//关闭线程池并且释放所有资源
group.shutdownGracefully().sync();
}
2.在handle处理的时候调用自定义的线程池来处理业务逻辑,这种方式更可控一些,可以在每个客户端一个线程池的基础上增加一个模块共用几个线程。
netty主动给客户端发送消息
Session:Session是很常用的技术。不管是WEB,还是游戏服务,还是联网的桌面程序,都有session的身影。有了Session,我们可以向里面保存各种个人参数,还可以利用session来向客户端发送消息。极大方便了程序对客户端的管理。
Mina IO框架默认有IoSession这个对象,Netty可就没有了。所以我们可以自己创建一个Session抽象。
关于Session,我们希望它有这样的作用。
1. 客户端链路第一次激活时,服务端为之创建一个Session;
2. 可以使用Session向用户发送消息;
3. 可以保存一些重要的且不需要持久化的用户信息;
4. 只能由服务端控制它的生命周期消亡;
public class IoSession {
private static final Logger logger = LoggerFactory.getLogger(IoSession.class);
/** 网络连接channel */
private Channel channel;
private User user;
/** ip地址 */
private String ipAddr;
private boolean reconnected;
/** 拓展用,保存一些个人数据 */
private Map<String, Object> attrs = new HashMap<>();
public IoSession() {
}
public IoSession(Channel channel) {
this.channel = channel;
this.ipAddr = ChannelUtils.getIp(channel);
}
public void setUser(User user) {
this.user = user;
}
/**
* 向客户端发送消息
* @param packet
*/
public void sendPacket(Packet packet) {
if (packet == null) {
return;
}
if (channel != null) {
channel.writeAndFlush(packet);
}
}
public String getIpAddr() {
return ipAddr;
}
public void setIpAddr(String ipAddr) {
this.ipAddr = ipAddr;
}
public boolean isReconnected() {
return reconnected;
}
public void setReconnected(boolean reconnected) {
this.reconnected = reconnected;
}
public User getUser() {
return user;
}
public boolean isClose() {
if (channel == null) {
return true;
}
return !channel.isActive() ||
!channel.isOpen();
}
/**
* 关闭session
* @param reason {@link SessionCloseReason}
*/
public void close(SessionCloseReason reason) {
try{
if (this.channel == null) {
return;
}
if (channel.isOpen()) {
channel.close();
logger.info("close session[{}], reason is {}", getUser().getUserId(), reason);
}else{
logger.info("session[{}] already close, reason is {}", getUser().getUserId(), reason);
}
}catch(Exception e){
}
}
}
//Session被关闭可以有一系列原因,所以我们最后有一个枚举保存各种原因,像这样
public enum SessionCloseReason {
/** 正常退出 */
NORMAL,
/** 链接超时 */
OVER_TIME,
}
//管理用户通讯的工具类
public enum ServerManager {
INSTANCE;
private Logger logger = LoggerFactory.getLogger(ServerManager.class);
/** 缓存通信上下文环境对应的登录用户(主要用于服务) */
private Map<IoSession, Long> session2UserIds = new ConcurrentHashMap<>();
/** 缓存用户id与对应的会话 */
private ConcurrentMap<Long, IoSession> userId2Sessions = new ConcurrentHashMap<>();
public void sendPacketTo(Packet pact,Long userId){
if(pact == null || userId <= 0) return;
IoSession session = userId2Sessions.get(userId);
if (session != null) {
session.sendPacket(pact);
}
}
/**
* 向所有在线用户发送数据包
*/
public void sendPacketToAllUsers(Packet pact){
if(pact == null ) return;
userId2Sessions.values().forEach( (session) -> session.sendPacket(pact));
}
/**
* 向单一在线用户发送数据包
*/
public void sendPacketTo(Packet pact,ChannelHandlerContext targetContext ){
if(pact == null || targetContext == null) return;
targetContext.writeAndFlush(pact);
}
public IoSession getSessionBy(long userId) {
return this.userId2Sessions.get(userId);
}
public boolean registerSession(User user, IoSession session) {
session.setUser(user);
userId2Sessions.put(user.getUserId(), session);
logger.info("[{}] registered...", user.getUserId());
return true;
}
/**
* 注销用户通信渠道
*/
public void ungisterUserContext(Channel context ){
if(context == null){
return;
}
IoSession session = ChannelUtils.getSessionBy(context);
Long userId = session2UserIds.remove(session);
userId2Sessions.remove(userId);
if (session != null) {
session.close(SessionCloseReason.OVER_TIME);
}
}
}
最后在自定义的handler中添加缓存session逻辑
ChannelOption
netty通过ChannelOption配置一些属性。
netty 中在创建ServerBootstrap 时,里面会维护一个生成好的LinkedHashMap, 来保存所有的ChannelOption及对应的值。
private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {
if (childOption == null) {
throw new NullPointerException("childOption");
}
if (value == null) {
synchronized (childOptions) {
childOptions.remove(childOption);
}
} else {
synchronized (childOptions) {
childOptions.put(childOption, value);
}
}
return this;
}
@Override
void init(Channel channel) throws Exception {
final Map<ChannelOption<?>, Object> options = options();
synchronized (options) {
channel.config().setOptions(options);
}
。。。
}
public abstract class ConstantPool<T extends Constant<T>> {
private final Map<String, T> constants = new HashMap<String, T>();
private int nextId = 1;
public T valueOf(String name) {
if (name == null) {
throw new NullPointerException("name");
}
if (name.isEmpty()) {
throw new IllegalArgumentException("empty name");
}
synchronized (constants) {
T c = constants.get(name);
if (c == null) {
c = newConstant(nextId, name);
constants.put(name, c);
nextId ++;
}
return c;
}
}
。。。
}
在ServerBootstrap 中放option时,会将这个option对象,及value存放到这个LinkedHashMap当中。在serverBootstrap 绑定到具体的端口时,init()方法当中,会去将之前的options的信息,绑定到具体channel的config中。ChannelOption 类的具体实现,ChannelOption当中维护了一个 ConstantPool 对象,在ConstantPool当中,维护了一个Map对象,每创建一个ChannelOption对象,都会把这个对象,作为value放入到Map 中的value中。
1、ChannelOption.SO_BACKLOG
ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。
2、ChannelOption.SO_REUSEADDR
ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。
3、ChannelOption.SO_KEEPALIVE
Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
4、ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF
ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。需要根据推送消息的大小,合理设置,对于海量长连接,通常32K是个不错的选择。
5、ChannelOption.SO_LINGER
ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发送剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送。
6、ChannelOption.TCP_NODELAY
ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
7、IP_TOS
IP参数,设置IP头部的Type-of-Service字段,用于描述IP包的优先级和QoS选项。
8、ALLOW_HALF_CLOSURE
Netty参数,一个连接的远端关闭时本地端是否关闭(关闭连接时,允许半关,默认不允许)。默认值为False时,连接自动关闭;为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。
9.CONNECT_TIMEOUT_MILLIS
Netty参数,连接超时毫秒数,默认值30000毫秒即30秒。
10.MAX_MESSAGES_PER_READ
Netty参数,一次Loop读取的最大消息数,对于ServerChannel或者NioByteChannel,默认值为16,其他Channel默认值为1。默认值这样设置,是因为:ServerChannel需要接受足够多的连接,保证大吞吐量,NioByteChannel可以减少不必要的系统调用select。
11.WRITE_BUFFER_WATER_MARK
设置buffer的大小超过高水位线和低水位线,通过netty的WriteBufferWaterMark设置。如下:boot.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(8 * 1024, 32 * 1024));
下面这个坑其实也不算坑,只是因为懒,该做的事情没做。一般来讲我们的业务如果比较小的时候我们用同步处理,等业务到一定规模的时候,一个优化手段就是异步化。 异步化是提高吞吐量的一个很好的手段。但是,与异步相比,同步有天然的负反馈机制,也就是如果后端慢了,前面也会跟着慢起来,可以自动的调节。 但是异步就不同了,异步就像决堤的大坝一样,洪水是畅通无阻。如果这个时候没有进行有效的限流措施就很容易把后端冲垮。 如果一下子把后端冲垮倒也不是最坏的情况,就怕把后端冲的要死不活。 这个时候,后端就会变得特别缓慢,如果这个时候前面的应用使用了一些无界的资源等,就有可能把自己弄死。 那么现在要介绍的这个坑就是关于Netty里的ChannelOutboundBuffer这个东西的。 这个buffer是用在netty向channel write数据的时候,有个buffer缓冲,这样可以提高网络的吞吐量(每个channel有一个这样的buffer)。 初始大小是32(32个元素,不是指字节),但是如果超过32就会翻倍,一直增长。 大部分时候是没有什么问题的,但是在碰到对端非常慢(对端慢指的是对端处理TCP包的速度变慢,比如对端负载特别高的时候就有可能是这个情况)的时候就有问题了, 这个时候如果还是不断地写数据,这个buffer就会不断地增长,最后就有可能出问题了(我们的情况是开始吃swap,最后进程被linux killer干掉了)。 为什么说这个地方是坑呢,因为大部分时候我们往一个channel写数据会判断channel是否active,但是往往忽略了这种慢的情况。 那这个问题怎么解决呢?其实ChannelOutboundBuffer虽然无界,但是可以给它配置一个高水位线和低水位线, 当buffer的大小超过高水位线的时候对应channel的isWritable就会变成false, 当buffer的大小低于低水位线的时候,isWritable重新变成true。所以应用应该判断isWritable,如果是false就不要再写数据了。 高水位线和低水位线是字节数,默认高水位是64K,低水位是32K,我们可以根据我们的应用需要支持多少连接数和系统资源进行合理规划。
# 其他
心跳机制
IdleStateHandler心跳检测实例,构造函数有读监听时间,写监听时间,和读写监听时间。
public IdleStateHandler( int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) { this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds, TimeUnit.SECONDS); }
IdleStateEvent : 超时的事件
IdleStateHandler : 超时状态处理
ReadTimeoutHandler : 读超时状态处理
WriteTimeoutHandler : 写超时状态处理
public class HeartbeatHandlerInitializer extends ChannelInitializer<Channel> {
private static final int READ_IDEL_TIME_OUT = 4; // 读超时
private static final int WRITE_IDEL_TIME_OUT = 5;// 写超时
private static final int ALL_IDEL_TIME_OUT = 7; // 所有超时
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(READ_IDEL_TIME_OUT,
WRITE_IDEL_TIME_OUT, ALL_IDEL_TIME_OUT, TimeUnit.SECONDS)); // 1
pipeline.addLast(new HeartbeatServerHandler()); // 2
}
}
使用了 IdleStateHandler ,分别设置了读、写超时的时间
定义了一个 HeartbeatServerHandler 处理器,用来处理超时时,发送心跳
public class HeartbeatServerHandler extends ChannelInboundHandlerAdapter {
// Return a unreleasable view on the given ByteBuf
// which will just ignore release and retain calls.
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled
.unreleasableBuffer(Unpooled.copiedBuffer("Heartbeat",
CharsetUtil.UTF_8)); // 1
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
throws Exception {
if (evt instanceof IdleStateEvent) { // 2
IdleStateEvent event = (IdleStateEvent) evt;
String type = "";
if (event.state() == IdleState.READER_IDLE) {
type = "read idle";
} else if (event.state() == IdleState.WRITER_IDLE) {
type = "write idle";
} else if (event.state() == IdleState.ALL_IDLE) {
type = "all idle";
}
ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()).addListener(
ChannelFutureListener.CLOSE_ON_FAILURE); // 3
System.out.println( ctx.channel().remoteAddress()+"超时类型:" + type);
} else {
super.userEventTriggered(ctx, evt);
}
}
}
定义了心跳时,要发送的内容
判断是否是 IdleStateEvent 事件,是则处理
将心跳内容发送给客户端
自定义编码器和解码器
MessageToMessageEncoder 和MessageToMessageDecoder
SimpleChannelInboundHandler和ChannelInboundHandlerAdapter的区别
//SimpleChannelInboundHandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
//ChannelInboundHandlerAdapter
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
//继承关系
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {}
从源码上上面,我们可以看出,当方法返回时,SimpleChannelInboundHandler会负责释放指向保存该消息的ByteBuf的内存引用。而ChannelInboundHandlerAdapter在其时间节点上不会释放消息,而是将消息传递给下一个ChannelHandler处理。
从类的定义中,我们可以看出
- SimpleChannelInboundHandler<T>是抽象类,而ChannelInboundHandlerAdapter是普通类,
- SimpleChannelInboundHandler支持泛型的消息处理,而ChannelInboundHandlerAdapter不支持泛型
Netty中Channel的生命周期(SimpleChannelInboundHandler)
1.channelRegistered
channel注册事件
channel取消注册事件
channel活跃
channel不活跃
channel读取完毕事件
channel 用户事件触发
channel 可写更改
channel 捕获到异常
channel 助手类的添加
channel 助手类移除
channel读取数据
执行顺序
9-1-3-11-5-4-2-10
AttributeKey
以上类关系图,可以看到和ChannelOption是很相似的,两个类都实现了AbstractConstrant, 同时类内部也都维护了ConstantPool ,主要维护业务数据。每个channel有独自的Map收集AttributeKey。
AttributeMap这是是绑定在Channel或者ChannelHandlerContext上的一个附件,相当于依附在这两个对象上的寄生虫一样,相当于附件一样,如图所示:
我们知道每一个ChannelHandlerContext都是ChannelHandler和ChannelPipeline之间连接的桥梁,每一个ChannelHandlerContext都有属于自己的上下文,也就说每一个ChannelHandlerContext上如果有AttributeMap都是绑定上下文的,也就说如果A的ChannelHandlerContext中的AttributeMap,B的ChannelHandlerContext是无法读取到的,但是Channel上的AttributeMap就是大家共享的,每一个ChannelHandler都能获取到。如下 channel和ChannelHandlerContext继承了AttributeMap
public interface Channel extends AttributeMap, Comparable<Channel> {
}
public interface ChannelHandlerContext extends AttributeMap {
}
* Implementations must be Thread-safe.
*/
public interface AttributeMap {
/**
* Get the {@link Attribute} for the given {@link AttributeKey}. This method will never return null, but may return
* an {@link Attribute} which does not have a value set yet.
*/
<T> Attribute<T> attr(AttributeKey<T> key);
}
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
}
public class DefaultAttributeMap implements AttributeMap {
private volatile Map<AttributeKey<?>, Attribute<?>> map;
@Override
public <T> Attribute<T> attr(AttributeKey<T> key) {
Map<AttributeKey<?>, Attribute<?>> map = this.map;
if (map == null) {
// Not using ConcurrentHashMap due to high memory consumption.
map = new IdentityHashMap<AttributeKey<?>, Attribute<?>>(2);
if (!updater.compareAndSet(this, null, map)) {
map = this.map;
}
}
synchronized (map) {
@SuppressWarnings("unchecked")
Attribute<T> attr = (Attribute<T>) map.get(key);
if (attr == null) {
attr = new DefaultAttribute<T>(map, key);
map.put(key, attr);
}
return attr;
}
}
}
可以看出这个是线程安全的,所以我们可以放心使用,再看看AttributeMap的结构,其实和Map的格式很像,key是AttributeKey,value是Attribute,我们可以根据AttributeKey找到对应的Attribute,并且我们可以指定Attribute的类型T:Attribute的实现是内部类DefaultAttribute,而AtomicReference里有对应的set和get错。