前言
Netty是现在比较流行的NIO框架。它的健壮性、可扩展性、性能方面都得到了很多项目的验证。
要想了解Netty,首先得了解IO模型。Java支持三种IO模型分别是BIO,NIO,AIO。
- BIO:同步阻塞模型,连接一个请求就会有一个线程,假设有个客户端连接进来了,一直不做读写操作,这个连接就会一直占用,会大量的浪费资源,,如果连接的客户端巨多,就会导致线程数暴增,可能服务器都撑不住。使用场景,连接数比较小且不会有连接数暴增
- NIO:同步非阻塞模型,主要实现的是一个线程可以处理多个连接,它的实现逻辑是把所有打过来的请求都会丢到一个selector(多路复用器)上面去,多路复用器在去轮询,根据请求类型进行读、写、连接处理。多路复用器底层就是调用的操作系统的select,poll,epoll方法实现。使用场景:连接数多但连接较短。
- AIO:异步非阻塞模型,发起请求后会回调服务端程序去执行对应的请求,相当于一个钩子程序,NIO的升级版。使用场景:连接数多,且连接耗时较长
认识Netty
Netty说白了就是对NIO的一层封装,改造了原来的IO模型提升性能,为了让你代码写起来更方便,增强扩展性。
Netty在游戏领域,开源中间件(Dubbo,RocketMQ),大数据领域(Hadoop),通信行业等方向都有不少的应用。
Netty优点
- 使用简单
- 功能强大,内置了很多编解码功能,对主流协议的支持
- 定制能力强,可以通过ChannelHandler进行定制化开发
- 性能高
- 经历了多种行业的锤炼,证明了它的稳定性
Netty线程模型
Netty线程模型跟Reactor模式相一致。而Reacror模式又分为三种。主要核心逻辑可以参照Doug Lea大佬的Scalable IO in Java
单线程模型

从图可以看出Reactor内部通过selector进行监控,如果收到的是一个连接请求就通过dispatch分发任务给acceptor去处理并生成一个handler去处理之后的读写请求。因为处理连接和读写请求都在一个线程里面去执行,所以如果handler被阻塞了,会导致其他的线程同样不能执行,性能受到影响。
多线程模型

从图看出,Reactor收到一个连接请求就分发给acceptor,然后acceptor创建一个Handler处理后续事件Handler不处理业务操作,只负责响应度和写,业务操作扔到线程池里面去操作。这里的性能瓶颈是单Reactor,当有大量的客户端进行连接,可能会有处理不过来的情况。
主从多线程模型

这里使用了多个Reactor,mainReactor处理进来的连接请求,交给acceptor处理,然后acceptor将新的连接分配给一个子线程,子线程subReactor将分配过来的连接加入连接队列并通过自己的selector进行监听,并创建一个Handler处理后续事件。
对于Netty而言上诉几种模型都可以实现。主要看NioEventLoopGroup线程个数的分配。
单线程模型
// 只申请一个工作线程
NioEventLoopGroup group = new NioEventLoopGroup(1);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ServerHandlerInitializer());
多线程模型
NioEventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(eventGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ServerHandlerInitializer());
主从多线程模型(最常用)
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ServerHandlerInitializer());
Netty的线程模型是对于上诉的Reactor模式进行了一定的升级,但核心思想没有变。

Netty相关组件
Netty的一个服务端通用代码
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast(new MyServerHandler());
}
});
ChannelFuture channelFuture = bootstrap.bind(9000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
- Selector:通过Selector监听多个连接的channel事件,通过selector轮训所有注册的channel,
- NioEventLoopGroup:相当于一个线程池,内部维护类一组线程(NioEventLoop),每个线程处理多个Channel上的事件。
- NioEventLoop:里面有线程和任务队列,有run方法处理不同的事件
- ServerBootstrap:串联Netty所有组件,是Netty的启动类。
- ChannelHandler:ChannelHandler处理I/O事件,并将其转发到pipeline中的下一个处理程序
- ChannelInboundHandler:处理入站IO事件
- ChannelOutboundHandler:处理出站IO事件
- ChannelHandlerContext:关联ChannelHandler
- ChannelPipline:一个由ChannelHandlerContext构成的过滤器链,它是一个双向链表,他会维护一个head和tail,入站则是head->tail,出站是tail到head,注意:入站和出站在这里是相对的,read是入站事件,write是出站事件,服务端写数据到客户端,服务端是出站事件,客户端是入站事件
Netty常见问题
编解码问题
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new StringDecoder());// 解码器
pipeline.addLast("encoder", new StringEncoder());// 编码器
pipeline.addLast(new MyServerHandler());
首先服务器之间的通讯只能是字节,然后我们要把消息发送到客户端,首先要对发送内容进行编码,客户端要对收到内容进行解码.上诉代码就是一个编解码的pipeline.通过分析StringEncoder编码器应该继承的是ChannelOutboundHandlerAdapter或者实现了ChannelOutboundHandler满足出站事件。StringDecoder解码器应该继承的是ChannelInboundHandlerAdapter或者实现了ChannelInboundHandler满足入站事件。看一下继承关系图,发现分析正确


Netty还有其他的编解码器,比如ObjectEncoder和ObjectDecoder
粘包拆包问题
由于TCP是面向流的,所以是无消息保护边界的。发送端有时为了更有效地将数据包发送给对方,有时会把多个数据包合并为一个发送就是粘包问题,把一个数据包拆分成多个就是拆包问题。这样带来的问题就是虽然你是提高了效率,但是接收端这边就无法分清哪个数据包是哪个了。
解决方案:
- 对发送内容进行边界控制,就是在你所发送的内容开始与结束加上标志符。但这样不好用,因为每次开发你都的加上边界符,不易于其他人对代码的理解。
- 在你所发送的内容上加上发送内容的长度,通过长度判断数据的开始与结束。
Netty零拷贝
在零拷贝之前必须了解直接内存与堆内存的区别;堆内存顾名思义就是放在JVM堆里面的内存,直接内存是排除掉堆内存的其他内存相当于物理内存,gc不参与到其中,这部分内存JVM管不着;
申请直接内存:java申请直接内存会调用本地native方法,在物理内存上申请一块空间,然后自己JVM堆内存上也分配一块叫DirectByteBuffer的空间,DirectByteBuffer里面会存申请的物理内存的内存地址的开始位置,和数据长度,通过定位和长度就能确定数据了。
内存回收:JVM gc 管不了他们那么怎么将这部分内存进行回收呢?在申请直接内存的时候会给引用对象绑定一个Cleaner机制,就是只要引用对象一被GC,那么就会触发Cleaner机制把内存回收。
直接内存与堆内存对比:读写上直接内存更快;申请空间上堆内存更快。
下面可以介绍零拷贝了
我们客户端发一条数据到服务端,服务端需要在IO读写层面需要做哪些操作呢?
假设没有零拷贝,发生的情况就是客户端发一条数据到服务端,服务端将这条数据放到socket缓冲区然后拷贝到直接内存(数据不能直接给堆内存),然后直接内存把数据拷贝到JVM堆内存,然后JVM对数据进行修改后在将数据拷贝到直接内存(数据不能直接给socket缓冲区),然后直接内存把数据拷贝到socket缓冲区。大家数一下中间发生了几次拷贝。共4次拷贝,
以刚刚的例子来说零拷贝技术就是客户端发一条数据到服务端,服务端将这条数据socket缓冲区然后拷贝到直接内存,然后堆内存会有一个DirectByteBuffer映射到这一块直接内存,然后JVM对数据进行修改将数据直接改到直接内存,然后直接内存拷贝给socket缓冲区总共发生2次拷贝,零拷贝就是减少数据之间的拷贝。
直接内存优缺点
优点:
* 不占用堆内存
* 内存读写快
缺点
- 申请空间慢
- 有物理内存被撑爆的可能性,这就跟JVM元空间内存机制是一样的,元空间也是直接内存,然后元空间必须设置最大值一个道理。在这里我们可以和元空间的处理类似,加上一个配置参数-XX:MaxDirectMemorySize控制直接内存的大小