Netty进阶

Netty核心组件及关系


1. Channel

IO操作的一种连接,实体与实体之间的连接,实体可以是硬件,文件,网络套接字,或者是程序组件Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channe l的 EventLoop。(这个 Channel 我们可以理解为 Socket 连接,它负责基本的 IO 操作,例如:bind(),connect(),read(),write() 等等。简单的说,Channel 就是代表连接,实体之间的连接,程序之间的连接,文件之间的连接,设备之间的连接。同时它也是数据入站和出站的载体)

生命周期

  • channelUnregistered:channel已创建但未注册到一个EventLoop
  • channelRegistered:channel注册到一个EventLoop
  • channelActive:channel变为活跃状态,可以接收和发送数据了
  • channelInactive:处于非活跃状态,没有连接到远程主机

与流的区别

  • Channel可以同时支持读和写,而Stream只能单向的读或写
  • Channel支持异步读写,Stream通常只支持同步
  • Channel总是读向Buffer,或者写自Buffer

2. ChannelHandler

在这里插入图片描述

  • ChannelInboundHandler:处理进站数据和所有状态更改事件(进站指的是读操作等由通道引发的事件)
  • handlerAdded:新建立的连接会按照初始化的策略,把handler添加到该channel的pipeline里面,也就是channel.pipeline.addLast(new XxxxHandler) 执行完后的回调
  • channelRegistered:当channel已经注册到他的EventLoop并且能够处理I/O时被调用(当该连接分配到具体的worker线程后,该回调会被调用)
  • channelUnregistered:对应channelRegistered,当连接关闭后,释放绑定的workder线 程
  • channelActive:channel的准备工作已经完成,所有的pipeline添加完成,并分配到具体的线上上,说明该channel准备就绪,可以使用了
  • channelInactive:当连接断开时,该回调会被调用,说明这时候底层的TCP连接已经被断00开了
  • channelReadComplete:当channel上的一个读操作完成时调用
    channelRead:当从channel读取数据时调用
  • ChannelWritability-Changed:当channel的可写状态发生改变时被调用
  • handlerRemoved: 对应handlerAdded,将handler从该channel的pipeline移除后的回调方法。

在这里插入图片描述

  • ChannelOutboundHandler:处理出站数据(写操作由用户触发的事件,发送到服务器的事件)

bind(ChannelHandlerContext var1, SocketAddress var2, ChannelPromise var3)
connect(ChannelHandlerContext var1, SocketAddress var2, SocketAddress var3,ChannelPromise var4)
disconnect(ChannelHandlerContext var1, ChannelPromise var2)
close(ChannelHandlerContext var1, ChannelPromise var2)
deregister(ChannelHandlerContext var1, ChannelPromise var2)
read(ChannelHandlerContext var1)
write(ChannelHandlerContext var1, Object var2, ChannelPromise var3)
flush(ChannelHandlerContext var1)

3. ChannelPipline

ChannelPipeline其实就是一个ChannelHandler容器,里面包括一系列的ChannelHandler 实例,用于拦截流经一个Channel 的入站和出站事件,每个Channel都有一个ChannelPipeline

  • addFirst/addBefore/addAfter/addLast:添加ChannelHandler到ChannelPipeline
  • remove:从ChannelPipeline移除ChannelHandler
  • replace:在ChannelPipeline替换另外一个ChannelHandler

4. ChannelHandlerContext

  • ChannelHandlerContext表示ChannelHandler 和ChannelPipeline 之间的关联,在ChannelHandler 添加到 ChannelPipeline 时创建ChannelHandlerContext表示两者之间的关系
  • 每个ChannelHandler都对应一个ChannelHandlerContext,ChannelHandler之间其实没有联系,都是由ChannelHandlerContext关联起来的

在这里插入图片描述

5. ChannelFuture

Netty 为异步非阻塞,即所有的 I/O 操作都为异步的,因此,我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口,通过该接口的 addListener() 方法注册一个ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

6. ChannelOption的含义以及使用的场景

  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会自动发送一个活动探测数据报文。(是否启用心跳保活机制。在双方TCP套接字建立连接后(即都
    进入ESTABLISHED状态)并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活。)
  4. ChannelOption.SO_SNDBUFChannelOption.SO_RCVBUF:ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小 ,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
  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. ByteBuf

Netty的数据容器

要想使用ByteBuf,首先肯定是要创建一个ByteBuf,更确切的说法就是要申请一块内存,后续可以在这块内存中执行写入数据读取数据等等一系列的操作。
那么如何创建一个ByteBuf呢?Netty中设计了一个专门负责分配ByteBuf的接口:ByteBufAllocator。该接口有一个抽象子类和两个实现类,分别对应了用来分配池化的ByteBuf和非池化的ByteBuf。(池化:被许多用户重复使用,非池化:被单一用户使用,使用完后释放掉)

在这里插入图片描述

ByteBuf和ByteBufAllocator之间是一种相辅相成的关系,ByteBufAllocator用来创建一个ByteBuf,而ByteBuf亦可以返回创建他的Allocator。ByteBuf和ByteBufAllocator之间是一种 抽象工厂模式 ,具体可以用一张图描述如下:
在这里插入图片描述

  • 池化堆内存 ctx.alloc().heapBuffer()
  • 池化直接内存 ctx.alloc().directBuffer()
  • 非池化堆内存 Unpooled.buffer()
  • 非池化直接内存 Unpooled.directBuffer()

1. ByteBuf如何工作的

在这里插入图片描述

  • ByteBuf维护了readerIndex和writerIndex索引
  • 当readerIndex > writerIndex时,则抛出IndexOutOfBoundsException
  • ByteBuf容量 = writerIndex。
  • ByteBuf可读容量 = writerIndex - readerIndex readXXX()和writeXXX()方法将会推进其对应的索引。自动推进
  • getXXX()和setXXX()方法将对writerIndex和readerIndex无影响

2、ByteBuf的使用模式

我们以Unpooled类为例,查看Unpooled的源码可以发现,他为我们提供了许多创建ByteBuf的方法,但是最终都是以下几种,只是参数不一样而已:在这里插入图片描述

  1. 堆缓冲区模式(Heap Buffer)

堆缓冲区模式又称为:支撑数组(backing array)。将数据存放在JVM的堆空间,通过将数据存储在数组中实现堆缓冲的优点: 由于数据存储在Jvm堆中可以快速创建和快速释放,并且提供了数组直接快速访问的方法
堆缓冲的缺点: 每次数据与I/O进行传输时,都需要将数据拷贝到直接缓冲区

public static void heapBuffer() { 
// 创建Java堆缓冲区 
	ByteBuf heapBuf = Unpooled.buffer(); 
//判断是否有一个支撑数组 
	if (heapBuf.hasArray()) { 
	//如果有,则获取该数组的引用 
		byte[] array = heapBuf.array(); 
		//计算第一个字节的偏移量 
		int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); 
		//获得可读字节数 
		int length = heapBuf.readableBytes(); 
		System.out.println(Arrays.toString(array)); 
		System.out.println(offset); 
		System.out.println(length); 
    }
 }
  1. 直接缓冲区模式(Direct Buffer)

Direct Buffer属于堆外分配的直接内存,不会占用堆的容量。适用于套接字传输过程,避免了数据从内部缓冲区拷贝到直接缓冲区的过程,性能较好

  • Direct Buffer的优点: 使用Socket传递数据时性能很好,避免了数据从Jvm堆内存拷贝到直接缓冲区的过程。提高了性能
  • Direct Buffer的缺点: 相对于堆缓冲区而言,Direct Buffer分配内存空间和释放更为昂贵
  • 对于涉及大量I/O的数据读写,建议使用Direct Buffer。而对于用于后端的业务消息编解码模块建议使用Heap Buffer
public static void main(String args[]) {
	 ByteBuf directBuf = Unpooled.directBuffer(100);
	 directBuf.writeBytes("direct buffer".getBytes()); 
	 //检查 ByteBuf 是否由数组支撑。如果不是,则这是一个直接缓冲区 
	 if (!directBuf.hasArray()) { 
	 	//获取可读字节数 
	 	int length = directBuf.readableBytes(); 
	 	//分配一个新的数组来保存具有该长度的字节数据 
	 	byte[] array = new byte[length]; 
	 	//将字节复制到该数组 
	 	directBuf.getBytes(directBuf.readerIndex(), array); 
	 	System.out.println(Arrays.toString(array)); 
	 	System.out.println(length);
   } 
}

3. 复合缓冲区模式(Composite Buffer)

Composite Buffer是Netty特有的缓冲区。本质上类似于提供一个或多个ByteBuf的组合视图,可以根据需要添加和删除不同类型的ByteBuf。

  • 想要理解Composite Buffer,请记住:它是一个组合视图。它提供一种访问方式让使用者自由的组合多个ByteBuf,避免了拷贝和分配新的缓冲区。
  • Composite Buffer不支持访问其支撑数组。因此如果要访问,需要先将内容拷贝到堆内存中,再进行访问

8. 组件之间的关系

在这里插入图片描述


七、Netty心跳检测


在Netty4中,使用IdleStateHandler实现心跳检测及空闲状态检测。我们知道使用netty的时候,大多数的东西都与Handler有关,我们的业务逻辑基本都是在Handler中实现的。Netty中自带了一个IdleStateHandler 可以用来实现心跳检测。
心跳检测的逻辑
本文中我们将要实现的心跳检测逻辑是这样的:服务端启动后,等待客户端连接,客户端连接之后,向服务端发送消息。如果客户端在“干活”那么服务端必定会收到数据,如果客户端“闲下来了”那么服务端就接收不到这个客户端的消息,既然客户端闲下来了,不干事,那么何必浪费连接资源呢?所以服务端检测到一定时间内客户端不活跃的时候,将客户端连接关闭。


八、Netty粘包和拆包处理


1、什么是粘包和半包

我们知道, Netty 发送和读取数据的单位,可以形象的使用 ByteBuf 来充当。每一次发送,就是向Channel 写入一个 ByteBuf ;每一次读取,就是从 Channel 读到一个 ByteBuf 。我们的理想是:发送端每发送一个buffer,接收端就能接收到一个一模一样的buffer。然而,理想很丰满,现实很骨感。在实际的通讯过程中,并没有大家预料的那么完美。一种意料之外的情况,如期而至。这就是粘包和半
包。
1、粘包和半包:指的都不是一次是正常的 ByteBuf 缓存区接收。
2、粘包:就是接收端读取的时候,多个发送过来的 ByteBuf “粘”在了一起。换句话说,接收端读取一次的 ByteBuf ,读到了多个发送端的 ByteBuf ,是为粘包。
3、半包:就是接收端将一个发送端的ByteBuf “拆”开了,形成一个破碎的包,我们定义这种 ByteBuf为半包。换句话说,接收端读取一次的 ByteBuf ,读到了发送端的一个 ByteBuf的一部分,是为半包处理粘包拆包,其实思路都是一致的,就是“分分合合”,粘包由于数据过多,那就按照固定策略分割下交给程序来处理;拆包由于一次传输数据较少,那就等待数据传输长度够了再交给程序来处理。

2. 常用解码器

在这里插入图片描述

在Netty中的编码器其实就是一个handler,Netty中的编解码器太多了,下面就常用ByteToMessageDecoder介绍它的体系:

解码器的模板基类:ByteToMessageDecoder

ByteToMessageDecoder继承了ChannelInboundHandlerAdapter,说明它是处理入栈方向数据的编码器,因此它也是一个不折不扣的Handler,再回想,其实In开头的handler都是基于事件驱动的,被动的处理器,当客户端发生某种事件时,它对应有不同的动作回调,而且它的特色就是 fireXXX往下传递事件, 待会我们就能看到,netty用它把处理好的数据往下传递

  • FixedLengthFrameDecoder(固定长度的拆包器)
  1. 消息长度固定,累积读取到长度总和为定长 LEN 的报文后,就认为读取到了一个完整的消息,再将计数器置位,重新读取下一个数据报。例如可以让每个报文的大小为固定长度 1024 字节,如果消息长度不够,则使用空位填补空缺,这样读取到了之后,只需要 trim 去掉空格即可。
  2. FixedLengthFrameDecoder 固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑 TCP 的粘包与拆包问题,非常实用。无论一次接收到多少数据报,它都会按照构造器中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder 会缓存半包消息并等待下个包到达之后进行拼包合并,直到读取一个完整的消息包
  • LineBasedFrameDecoder(行拆包器)

LineBasedFrameDecoder的工作原理是它一次遍历ByteBuf中的可读字节,判断看是否有“\n”或 者“\r\n”, 如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有出现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。

  • DelimiterBasedFrameDecoder(分隔符拆包器)

DelimterBaseFrameDecoder是分隔符解码器,用户可以指定消息结束的分隔符,它可以自动完成以分隔符作为码流结束标识的消息解码。回车换行解码器实际上是一种特殊的DelimiterBaseFrameDecoder解码器。

  • LengthFieldBasedFrameDecoder(基于数据包长度的拆包器)

大多数的协议(私有或者公有),协议头中会携带长度字段,用于标识消息体或者整包消息的长度,例如SMPP、HTTP协议等。由于基于长度解码需求的通用性,以及为了降低用户的协议开发难度,Netty提供了[LengthFieldBasedFrameDecoder,自动屏蔽TCP底层的拆包和粘包问题,只需要传入正确的参数,即可轻松解决“读半包“问题。

基于长度域,指的是在传输的协议中有一个 length字段,这个十六进制的字段记录的可能是整个协议的长度,也可能是消息体的长度, 我们根据具体情况使用不同的构造函数

在这里插入图片描述


九. Netty零拷贝


传统拷贝

在这里插入图片描述

JVM发出read()调用:
第一次拷贝:hardware ------> kernel buffer(硬盘数据读取到内核空间缓冲区)
第二次拷贝:kernel -------> user buffer(从内核缓冲区复制到用户缓冲区)JVM处理代码逻辑并发送write()系统调用
第三次拷贝:user buffer -----> kernel buffer(从用户缓冲区复制数据到内核缓冲区)
第四次拷贝:kernel buffer ----> socket Buffer(内核空间缓冲区中的数据写到hardware)
总的来说,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。显然在这个用例中,从内核空间到用户空间内存的复制是完全不必要的,因为除了将数据转储到不同的buffer之外,我们没有做任何其他的事情。所以,我们能不能直接从hardware读取数据到kernel buffer后,再从kernel buffer写到目标地点不就好了。为了解决这种不必要的数据复制,操作系统出现了零拷贝的概念。注意,不同的操作系统对零拷贝的实现各不相同.

零拷贝

在这里插入图片描述

第一种方式需要四次数据拷贝和两次上下文切换:
\1. 数据从磁盘读取到内核的read buffer
\2. 数据从内核缓冲区拷贝到用户缓冲区
\3. 数据从用户缓冲区拷贝到内核的socket buffer
\4. 数据从内核的socket buffer拷贝到网卡接口的缓冲区

明显上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两
次多余的拷贝(当然这需要底层操作系统支持)

\1. 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
\2. 接着DMA从内核read buffer将数据拷贝到网卡接口buffer
上面的两次操作都不需要CPU参与,所以就达到了零拷贝。

Netty的零拷贝体现在三个方面:

  1. Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直
    接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
  2. Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
  3. Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

案例 Netty构建Http服务器


package com.yuandengta.http;

import org.apache.http.impl.bootstrap.ServerBootstrap;

import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/*** @Author:Hardy * @QQ:2937270766 * @官网:http://www.yuandengta.com */
public class HttpServer {
	public static void main(String[] args) throws InterruptedException {
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap serverBootstrap = new ServerBootstrap();
			serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
					.childHandler(new ServerInitializer());
			ChannelFuture ch = serverBootstrap.bind(8888).sync();
			ch.channel().closeFuture().sync();
		} finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}
}
package com.yuandengta.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;

public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> { 
	@Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
	 //判断msg是不是httpRequest请求 
	 if(msg instanceof HttpRequest){ 
	 	System.out.println("msg 类型 = "+msg.getClass()); 
	 	System.out.println("客户端地址 = "+ctx.channel().remoteAddress()); 
	 	//回复信息给浏览器[http协议] 
	 	ByteBuf content = Unpooled.copiedBuffer("hello,我是服务器", CharsetUtil.UTF_16); 
	 	//构造一个http的响应,即httpresponse 
	 	DefaultFullHttpResponse response = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK,content); 
	 	response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
	 	response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes()); 
	 	//将构建好response返回
	 	ctx.writeAndFlush(response);
	  } 
   } 
}
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值