什么是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线程模型
当然,这里只是列出了主从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的拆包和粘包问题。 下面可以看一张图,是客户端向服务端发送包:
- 第一种情况,Data1和Data2都分开发送到了Server端,没有产生粘包和拆包的情况。
- 第二种情况,Data1和Data2数据粘在了一起,打成了一个大的包发送到Server端,这个情况就是粘包。
- 第三种情况,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;
}
}