Netty学习笔记

934 阅读10分钟

什么是Netty

Netty是一个基于JAVA NIO类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。

Netty应用场景

  • 1.分布式开源框架中dubbo、Zookeeper,RocketMQ底层rpc通讯使用就是netty。
  • 2.游戏开发中,底层使用netty通讯。

为什么选择netty

  • NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;
  • 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;
  • 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大;
  • JDK NIO的BUG,例如臭名昭著的epollbug,它会导致Selector空轮询,最终导致CPU100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决。

为什么netty仅支持NIO了

  • 为什么不建议阻塞I/O(BIO/OIO)

连接高效的情况下:阻塞->耗资源,效率低

  • 为什么删掉已经做好的AIO支持
    • Windows实现成熟,但是很少用来做服务器;
    • Linux常用来做服务器,但是AIO实现不够成熟;
    • Linux下AIO相比较NIO的性能提升不明显。

Netty线程模型

MainReactor负责客户端的连接请求,并将请求转交给SubReactor,SubReactor负责相应通道的IO读写请求,非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理。

当然,这里只是列出了主从Reactor模型,还有单线程Reactor模型和非主从Reactor模型没有列出,这里只简单先罗列下不同模型的编码使用方式吧。

Reactor单线程模型使用方式

NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventLoopGroup);

非主从Reactor多线程模型使用方式

NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventLoopGroup);

主从Reactor多线程模型使用方式

NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventLoopGroup, workerLoopGroup);

如何使用netty快速完成开发

创建服务器端

这里使用主从Reactor多线程模型

class ServerHandler extends ChannelHandlerAdapter {
	/**
	 * 当通道被调用,执行该方法
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 接收数据
		String value = (String) msg;
		System.out.println("Server msg:" + value);
		// 回复给客户端 “您好!”
		String res = "好的...";
		ctx.writeAndFlush(Unpooled.copiedBuffer(res.getBytes()));
	}
}

public class NettyServer {

	public static void main(String[] args) throws InterruptedException {
		System.out.println("服务器端已经启动....");
		// 1.创建2个线程,一个负责接收客户端连接, 一个负责进行 传输数据
		NioEventLoopGroup pGroup = new NioEventLoopGroup();
		NioEventLoopGroup cGroup = new NioEventLoopGroup();
		// 2. 创建服务器辅助类
		ServerBootstrap b = new ServerBootstrap();
		b.group(pGroup, cGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
				// 3.设置缓冲区与发送区大小
				.option(ChannelOption.SO_SNDBUF, 32 * 1024).option(ChannelOption.SO_RCVBUF, 32 * 1024)
				.childHandler(new ChannelInitializer<SocketChannel>() {
					@Override
					protected void initChannel(SocketChannel sc) throws Exception {
						sc.pipeline().addLast(new StringDecoder());
						sc.pipeline().addLast(new ServerHandler());
					}
				});
		ChannelFuture cf = b.bind(8080).sync();
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
		cGroup.shutdownGracefully();
	}
}

创建客户端

class ClientHandler extends ChannelHandlerAdapter {

	/**
	 * 当通道被调用,执行该方法
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 接收数据
		String value = (String) msg;
		System.out.println("client msg:" + value);
	}
}

public class NettyClient {

	public static void main(String[] args) throws InterruptedException {
		System.out.println("客户端已经启动....");
		// 创建负责接收客户端连接
		NioEventLoopGroup pGroup = new NioEventLoopGroup();
		Bootstrap b = new Bootstrap();
		b.group(pGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel sc) throws Exception {
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ClientHandler());
			}
		});
		ChannelFuture cf = b.connect("127.0.0.1", 8080).sync();
		 cf.channel().writeAndFlush(Unpooled.wrappedBuffer("itmayiedu".getBytes()));
		 cf.channel().writeAndFlush(Unpooled.wrappedBuffer("itmayiedu".getBytes()));
		// 等待客户端端口号关闭
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
	}
}

Maven坐标

<dependencies>
	<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
	<dependency>
		<groupId>io.netty</groupId>
		<artifactId>netty-all</artifactId>
		<version>5.0.0.Alpha2</version>
	</dependency>

	<!-- https://mvnrepository.com/artifact/org.jboss.marshalling/jboss-marshalling -->
	<dependency>
		<groupId>org.jboss.marshalling</groupId>
		<artifactId>jboss-marshalling</artifactId>
		<version>1.3.19.GA</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.jboss.marshalling/jboss-marshalling-serial -->
	<dependency>
		<groupId>org.jboss.marshalling</groupId>
		<artifactId>jboss-marshalling-serial</artifactId>
		<version>1.3.18.GA</version>
		<scope>test</scope>
	</dependency>
</dependencies>

以上是使用5.0版本实现的demon,但5.0版本官方已经下掉了,4.x版本在使用起来基本是一样的,具体的可参见官网

TCP粘包、拆包问题解决方案

一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和粘包问题。 下面可以看一张图,是客户端向服务端发送包:

  1. 第一种情况,Data1和Data2都分开发送到了Server端,没有产生粘包和拆包的情况。
  2. 第二种情况,Data1和Data2数据粘在了一起,打成了一个大的包发送到Server端,这个情况就是粘包。
  3. 第三种情况,Data2被分离成Data2_1和Data2_2,并且Data2_1在Data1之前到达了服务端,这种情况就产生了拆包。

由于网络的复杂性,可能数据会被分离成N多个复杂的拆包/粘包的情况,所以在做TCP服务器的时候就需要首先解决拆包。

解决办法

1、消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。

sc.pipeline().addLast(new FixedLengthFrameDecoder(10));

2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。

ByteBuf buf = Unpooled.copiedBuffer("_study".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));

3、除了上述两种解码器,还有一种解码器是LengthFieldBasedFrameDecoder,这种解码器支持固定长度字段存个内容的长度信息

sc.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,5,2,10,0));

LengthFieldBasedFrameDecoder的五个参数如下:

  • maxFrameLength:单个包最大的长度,这个值根据实际场景而定。
  • lengthFieldOffset:表示数据长度字段开始的偏移量。
  • lengthFieldLength:数据长度字段的所占的字节数。
  • lengthAdjustment:这里取值为10=7(系统时间) + 1(校验码)+ 2 (包尾),如果这个值取值为0,试想一下,解码器跟数据长度字段的取值(这里数据长度内容肯定是1),只向后取一个字节,肯定不对。(lengthAdjustment + 数据长度取值 = 数据长度字段之后剩下包的字节数)
  • initialBytesToStrip:表示从整个包第一个字节开始,向后忽略的字节数,设置为0。

为什么需要“二次”编解码

因为第一次解码的结果是字节,需要和项目中使用的对象做转化,方便使用,这层解码器可以称为“二次解码器”,相应的,对应的编码器是为了将java对象转化为字节流方便存储或传输。

  • 一次解码器:ByteToMessageDecoder
    • io.netty.buffer.ByteBuf(原始数据流)-> io.netty.buffer.ByteBuf(用户数据)
  • 二次编码器:MessageToMessageDecoder
    • io.netty.buffer.ByteBuf(用户数据)-> java Object

常用的“二次”编解码

XML

(1)定义

XML(Extensible Markup Language)是一种常用的序列化和反序列化协议,它历史悠久,从1998年的1.0版本被广泛使用至今。

(2)优点

  • 人机可读性好
  • 可指定元素或特性的名称

(3)缺点

序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息。类必须有一个将由XmlSerializer序列化的默认构造函数。只能序列化公共属性和字段不能序列化方法,文件庞大,文件格式复杂,传输占带宽。

(4)使用场景

  • 当做配置文件存储数据
  • 实时数据转换

JSON

(1)定义

JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于ECMAScript (w3c制定的js规范)的一个子集, JSON采用与编程语言无关的文本格式,但是也使用了类C语言(包括C, C++, C#, Java, JavaScript, Perl, Python等)的习惯,简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。

(2)优点

  • 前后兼容性高
  • 数据格式比较简单,易于读写
  • 序列化后数据较小,可扩展性好,兼容性好
  • 与XML相比,其协议比较简单,解析速度比较快

(3)缺点

  • 数据的描述性比XML差
  • 不适合性能要求为ms级别的情况
  • 额外空间开销比较大

(4)适用场景(可替代XML)

  • 跨防火墙访问
  • 可调式性要求高的情况
  • 基于Web browser的Ajax请求
  • 传输数据量相对小,实时性要求相对低(例如秒级别)的服务

Thrift

(1)定义

Thrift并不仅仅是序列化协议,而是一个RPC框架。它可以让你选择客户端与服务端之间传输通信协议的类别,即文本(text)和二进制(binary)传输协议, 为节约带宽,提供传输效率,一般情况下使用二进制类型的传输协议。

(2)优点

  • 序列化后的体积小, 速度快
  • 支持多种语言和丰富的数据类型
  • 对于数据字段的增删具有较强的兼容性
  • 支持二进制压缩编码

(3)缺点

  • 使用者较少
  • 跨防火墙访问时,不安全
  • 不具有可读性,调试代码时相对困难
  • 不能与其他传输层协议共同使用(例如HTTP)
  • 无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议

(4)适用场景

  • 分布式系统的RPC解决方案

Avro

(1)定义

Avro属于Apache Hadoop的一个子项目。Avro提供两种序列化格式:JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美,Avro的产生解决了JSON的冗长和没有IDL的问题

(2)优点

  • 支持丰富的数据类型
  • 简单的动态语言结合功能
  • 具有自我描述属性
  • 提高了数据解析速度
  • 快速可压缩的二进制数据形式
  • 可以实现远程过程调用RPC
  • 支持跨编程语言实现

(3)缺点

  • 对于习惯于静态类型语言的用户不直观

(4)适用场景

  • 在Hadoop中做Hive、Pig和MapReduce的持久化数据格式

Protobuf

(1)定义 protocol buffers 由谷歌开源而来,在谷歌内部久经考验。它将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。

(2)优点

  • 序列化后码流小,性能高
  • 结构化数据存储格式(XML JSON等)
  • 通过标识字段的顺序,可以实现协议的前向兼容
  • 结构化的文档更容易管理和维护

(3)缺点

  • 需要依赖于工具生成代码
  • 支持的语言相对较少,官方只支持Java 、C++ 、Python
  • 可读性太差

(4)适用场景

  • 对性能要求高的RPC调用
  • 具有良好的跨防火墙的访问属性
  • 适合应用层对象的持久化

Marshalling编码器

public final class MarshallingCodeCFactory {

	/**
	 * 创建Jboss Marshalling解码器MarshallingDecoder
	 */
	public static MarshallingDecoder buildMarshallingDecoder() {
		final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
		final MarshallingConfiguration configuration = new MarshallingConfiguration();
		configuration.setVersion(5);
		UnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
		MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024);
		return decoder;
	}

	/**
	 * 创建Jboss Marshalling编码器MarshallingEncoder
	 */
	public static MarshallingEncoder buildMarshallingEncoder() {
		final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
		final MarshallingConfiguration configuration = new MarshallingConfiguration();
		configuration.setVersion(5);
		MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, configuration);
		MarshallingEncoder encoder = new MarshallingEncoder(provider);
		return encoder;
	}
}