Netty,网络通信的权威专家
Posted 毛奇志
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty,网络通信的权威专家相关的知识,希望对你有一定的参考价值。
一、前言
二、认识Netty
2.1 Netty是什么
问题1:Netty是什么?
回答1:Netty一个好使的处理Socket的工具。Netty的本质是JBoss开发的一个Jar包,其目的是快速开发高性能、高可靠性的网络服务器和客户端程序,最大优点是相对于传统的网络通信框架,提供了一个异步的、事件驱动的网络应用程序框架和工具。Netty是最好的网络通信框架之一,在Java应用层面,Netty涉及Java NIO和Java 网络编程(Socket通信)两个方面的内容;在IO处理方面,它封装了基于NIO的Reactor;在网络通信方面,它基于Java Socket网络编程封装了TCP UDP 通信方式。
问题2:如果没有Netty如何进行网络通信?
回答2:远古:java.net + java.io;近代:用java.nio自己写;现在:其他网络通信替代框架,如Mina,Grizzly。
问题3:Netty与Mina两个网络通信框架的异同?
回答3:Netty与Mina的架构差别不大,Mina靠apache生存,而Netty则与jboss的结合度非常高,都是Trustin Lee的作品,Netty更晚;所以,Netty有对google protocal buf的支持,有更完整的ioc容器支持(spring,guice,jbossmc和osgi)。
2.2 Netty的应用
理论上来说,NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用来做网络通信 :
1、Netty用来作为 RPC 框架的网络通信工具 :我们在分布式系统中,不同服务节点之间经常需要相互调用,这个时候就需要 RPC 框架了。不同服务节点之间的通信是如何做的呢?可以使用 Netty 来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数吧!
2、Netty用来实现一个自己的 HTTP 服务器 :通过 Netty 我们可以自己实现一个简单的 HTTP 服务器,这个大家应该不陌生。说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。
3、Netty用来实现一个即时通讯系统 :使用 Netty 我们可以实现一个可以聊天类似微信的即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。
4、Netty用来实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。
特别注意,在Netty上述四个用途中,强调一下Netty作为RPC框架网络通信应用,拉出来单独看一下。
软件升级过程中,随着网站规模的不断扩大,系统并发访问量也越来越高,传统基于 Tomcat 等 Web 容器的垂直架构已经无法满足需求,需要拆分应用进行服务化,以提高开发和维护效率。从组网情况看,垂直的架构拆分之后,系统采用分布式部署,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。
Netty作为RPC框架网络通信应用,最经典的就是阿里Dubbo+淘宝RocketMQ。
阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。在Dubbo的RPC架构中,服务提供者和服务消费者之间,服务提供者、服务消费者和性能统计节点之间使用 Netty 进行异步/同步通信。
除了 Dubbo 之外,淘宝的消息中间件 RocketMQ 的消息生产者和消息消费者之间,也采用 Netty 进行高性能、异步通信。
除了阿里系和淘宝系之外,很多其它的大型互联网公司或者电商内部也已经大量使用 Netty 构建高性能、分布式的网络服务器,很多中间件也使用了Netty作为网络通信框架,比如索引库Elasticsearch、RPC框架 gRPC 、 大数据Hadoop,都默认使用Netty作为网络通信框架。
附加:序列化和RPC的关系:RPC存在网络传输,所以需要一种对Java Bean的序列化技术。
三、Netty核心组件和IO响应
3.1 Netty核心组件
Netty核心组件:Channel、EventLoop、ChannelFuture、ChannelHandler 和 ChannelPipeline
对于Netty实际工作机制,我们直接使用Reactor三种线程模式进行类比学习比较好,Reactor服务端NIO三个核心组件:Dispatcher/Reactor负责事件分发、Acceptor负责处理客户端连接、Handler处理非连接事件(例如:读写事件)。
3.1.1 Channel(NioserverSocketChannel服务端+NioSocketChannel客户端)
Channel 接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。
比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 NIO 编程模型中的ServerSocketChannel 以及 SocketChannel 两个概念对应上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。
3.1.2 EventLoop事件循环,表示事件实体(Netty最核心的概念)
EventLoop 中文译为事件循环,定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
说白了,EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
问题:Netty的两个组件Channel 和 EventLoop 是什么关系?
回答:Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。
3.1.3 ChannelFuture(事件监听者listener)
Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。
因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
并且,你还可以通过ChannelFuture 的 channel() 方法获取关联的Channel
public interface ChannelFuture extends Future<Void> {
Channel channel();
ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1);
......
ChannelFuture sync() throws InterruptedException;
}
另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。
3.1.4 ChannelHandler 和 ChannelPipeline
下面这段代码使用过 Netty 的小伙伴应该不会陌生,我们指定了序列化编解码器(序列化解码器用于处理Tcp 粘包/拆包)以及自定义的 ChannelHandler 处理消息。
b.group(eventLoopGroup)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));
ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));
ch.pipeline().addLast(new KryoClientHandler());
}
});
ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。
ChannelPipeline 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。
我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。
3.2 Netty中的 EventloopGroup类 和 EventLoop类
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),上面我们已经说了 EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。
上图是一个服务端对 EventLoopGroup 使用的大致模块图,其中 Boss EventloopGroup 用于接收连接,Worker EventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。
小结:当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求,即accept。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作,即read/write 。
3.3 NioEventLoopGroup 无参构造函数,启动cpu核心数*2的线程
回顾我们在上面写的服务器端的代码:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
为了搞清楚NioEventLoopGroup 默认的构造函数 到底创建了多少个线程,我们来看一下它的源码。
/**
* 无参构造函数。
* nThreads:0
*/
public NioEventLoopGroup() {
//调用下一个构造方法
this(0);
}
/**
* Executor:null
*/
public NioEventLoopGroup(int nThreads) {
//继续调用下一个构造方法
this(nThreads, (Executor) null);
}
//中间省略部分构造函数
/**
* RejectedExecutionHandler():RejectedExecutionHandlers.reject()
*/
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) {
//开始调用父类的构造函数
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
一直向下走下去的话,你会发现在 MultithreadEventLoopGroup 类中有相关的指定线程数的代码,如下:
// 从1,系统属性,CPU核心数*2 这三个值中取出一个最大的
//可以得出 DEFAULT_EVENT_LOOP_THREADS 的值为CPU核心数*2
private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
// 被调用的父类构造函数,NioEventLoopGroup 默认的构造函数会起多少线程的秘密所在
// 当指定的线程数nThreads为0时,使用默认的线程数DEFAULT_EVENT_LOOP_THREADS
protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
}
综上,我们发现 NioEventLoopGroup 默认的构造函数实际会起的线程数为 CPU核心数*2。
另外,如果你继续深入下去看构造函数的话,你会发现每个NioEventLoopGroup对象内部都会分配一组NioEventLoop,其大小是 nThreads, 这样就构成了一个线程池, 每一个NIOEventLoop 和一个线程相对应,这和我们上面说的 EventloopGroup 和 EventLoop关系这部分内容相对应。
问题:netty底层是如何实现cpu核心数*2的线程的?
回答:每个NioEventLoopGroup对象内部都会分配一组NioEventLoop,其大小是 nThreads, 这样就构成了一个线程池, 每一个NIOEventLoop 和一个线程相对应,这和我们上面说的 EventloopGroup 和 EventLoop关系这部分内容相对应。NioEventLoopGroup类和NioEventLoop类直接没有继承关系,但是EventLoopGroup和EventLoop是父子继承关系,EventLoopGroup类是父类,EventLoop类是子类,如下图:
3.4 Netty中 Bootstrap类和 ServerBootstrap类
ServerBootstrap 客户端的启动引导类/辅助类,具体使用方法如下:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup).
......
// 6.绑定端口
ChannelFuture f = b.bind(port).sync();
// 等待连接关闭
f.channel().closeFuture().sync();
} finally {
//7.优雅关闭相关线程组资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
Bootstrap 是客户端的启动引导类/辅助类,具体使用方法如下:
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动引导/辅助类:Bootstrap
Bootstrap b = new Bootstrap();
//指定线程模型
b.group(group).
......
// 尝试建立连接
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
// 优雅关闭相关线程组资源
group.shutdownGracefully();
}
小结:
(1)ServerBootstrap通常使用 bind() 方法绑定本地的端口上,作为一个 Netty TCP 协议通信中的服务端,然后等待客户端的连接。
(2)Bootstrap 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。
(3)ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的处理;
(4)Bootstrap 只需要配置一个线程组— EventLoopGroup。
四、Netty,Reactor三种线程模式的实现者
4.1 Reactor三种线程模式
大部分网络框架都是基于 Reactor 模式设计开发的。
Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 IO 的场景。在 Netty 中,主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 。
我们实现服务端的时候,一般会初始化两个线程组:
bossGroup :接收连接;
workerGroup :负责具体的处理,交由对应的 Handler 处理。
下面我们来详细看一下 Netty 中的线程模型吧!
4.1.1 单线程模型
单线程模型定义:一个线程eventGroup完成所有操作,包括accept、read、decode、process、encode、send。
一个线程需要执行处理所有的 accept、read、decode、process、encode、send 事件。对于高负载、高并发,并且对性能要求比较高的场景不适用。
值得注意的是,使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2 。
对应到 Netty 代码是下面这样的:
//1.eventGroup既用于处理客户端连接,又负责具体的处理。
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
boobtstrap.group(eventGroup, eventGroup)
//......
4.1.2 多线程模型
多线程模型定义:bossGroup一个线程完成accept、read、send,workGroup多个线程完成decode、 process、encode。
一个 Acceptor 线程只负责监听客户端的连接,一个 NIO 线程池负责具体处理:accept、read、decode、process、encode、send 事件。满足绝大部分应用场景,并发连接量不大的时候没啥问题,但是遇到并发连接大的时候就可能会出现问题,成为性能瓶颈。
对应到 Netty 代码是下面这样的:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup)
//......
4.1.3 主从多线程模型
主从多线程模型定义:bossGroup中一个线程完成 accept,多个线程完成read send,workGroup多个线程完成 decode process encode。
从一个 主线程 NIO 线程池中选择一个线程作为 Acceptor 线程,绑定监听端口,接收客户端连接的连接,其他线程负责后续的接入认证等工作。连接建立完成后,Sub NIO 线程池负责具体处理 I/O 读写。如果多线程模型无法满足你的需求的时候,可以考虑使用主从多线程模型 。
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup)
//......
4.1.4 Netty如何实现Reactor的三种线程模式
金手指:Netty如何实现Reactor的三种线程模式
NioEventLoopGroup 类的两个构造函数
第一,NioEventLoopGroup 类带参构造函数:线程数由int型实参指定。一般是accept接收请求使用;
第二,NioEventLoopGroup 类无参构造函数:线程数为运行主机的 CPU 核心数*2 。一般是read/write读写使用。
所以,只要合理构造,就可以构造出Reactor三种线程模式,没什么好说的,简单。
4.2 Netty启动过程(Netty服务端启动过程 + Netty客户端启动过程)
4.2.1 Netty服务端启动过程
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1); //
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup)
// (非必备)打印日志
.handler(new LoggingHandler(LogLevel.INFO))
// 4.指定 IO 模型,使用NIO
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//5.可以自定义客户端消息的业务处理逻辑
p.addLast(new HelloServerHandler());
}
});
// 6.绑定端口,调用 sync 方法阻塞知道绑定完成
ChannelFuture f = b.bind(port).sync();
// 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)
f.channel().closeFuture().sync();
} finally {
//8.优雅关闭相关线程组资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
金手指:Netty服务端启动过程分为三步:new NioEventLoopGroup(); + new ServerBootstrap(); b.group…+ b.bind(port)
第一步,new NioEventLoopGroup();
定义两个eventloopGroup,其中,bossGroup 用于接收连接,workerGroup 用于具体的处理;
第二步,new ServerBootstrap(); b.group....
通过创建服务端启动引导/辅助类:ServerBootstrap,并给给引导类配置两大线程组,确定了线程模型,指定 IO 模型,使用NIO;
第三步,b.bind(port)
通过bind方法绑定端口,这里注意,sync方法阻塞知道绑定完成。
4.2.2 简单解析一下服务端的创建过程具体是怎样的
Netty服务端启动三步骤
第一步:首先你创建了两个 NioEventLoopGroup 对象实例:bossGroup 和 workerGroup
bossGroup : 用于处理客户端的 TCP 连接请求。
workerGroup :负责每一条连接的具体读写数据的处理逻辑,真正负责 I/O 读写操作,交由对应的 Handler 处理。
举个例子:我们把公司的老板当做 bossGroup,员工当做 workerGroup,bossGroup 在外面接完活之后,扔给 workerGroup 去处理。一般情况下我们会指定 bossGroup 的 线程数为 1(并发连接量不大的时候) ,workGroup 的线程数量为 CPU 核心数 *2 。另外,根据源码来看,使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2 。
金手指:NioEventLoopGroup 类的两个构造函数
NioEventLoopGroup 类带参构造函数:线程数由int型实参指定。一般是accept接收请求使用;
NioEventLoopGroup 类无参构造函数:线程数为运行主机的 CPU 核心数 *2 。一般是read/write读写使用。
第二步,创建了一个服务端启动引导/辅助类:ServerBootstrap,这个类将引导我们进行服务端的启动工作。通过 group() 方法给引导类 ServerBootstrap 配置两大线程组,确定了线程模型
通过下面的代码,我们实际配置的是多线程模型,这个在上面提到过。
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // accept 1个线程
EventLoopGroup workerGroup = new NioEventLoopGroup(); // read/write 多个线程 cpu核心数*2
注意:根据Netty使用Reactor实现,上面服务端使用的是Reactor第二种方式,多线程/线程池方式。
然后,通过channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO
NioServerSocketChannel :指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的ServerSocket对应
NioSocketChannel : 指定客户端的 IO 模型为 NIO, 与 BIO 编程模型中的Socket对应
再然后,通过 .childHandler()给引导类创建一个ChannelInitializer ,然后指定了服务端消息的业务处理逻辑 HelloServerHandler 对象
第三步,调用 ServerBootstrap 类的 bind()方法绑定端口
4.2.3 Netty客户端启动过程
//1.创建一个 NioEventLoopGroup 对象实例
EventLoopGroup group = new NioEventLoopGroup();
try {
//2.创建客户端启动引导/辅助类:Bootstrap
Bootstrap b = new Bootstrap();
//3.指定线程组
b.group(group)
//4.指定 IO 模型
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 5.这里可以自定义消息的业务处理逻辑
p.addLast(new HelloClientHandler(message));
}
});
// 6.尝试建立连接
ChannelFuture f = b.connect(host, port).sync();
// 7.等待连接关闭(阻塞,直到Channel关闭)
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
继续分析一下客户端的创建流程(和服务端一样,也是三个步骤):
第一步,创建一个 NioEventLoopGroup 对象实例
第二步,创建客户端启动的引导类是 Bootstrap,然后通过 .group() 方法给引导类 Bootstrap 配置一个线程组,再然后通过channel()方法给引导类 Bootstrap指定了 IO 模型为NIO,最后通过 .childHandler()给引导类创建一个ChannelInitializer ,然后指定了客户端消息的业务处理逻辑 HelloClientHandler 对象
第三步,调用 Bootstrap 类的 connect()方法进行连接,这个方法需要指定两个参数:
public ChannelFuture connect(String inetHost, int inetPort) {
return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}
public ChannelFuture connect(SocketAddress remoteAddress) {
ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
this.validate();
return this.doResolveAndConnect(remoteAddress, this.config.localAddress());
}
connect 方法返回的是一个 Future 类型的对象
public interface ChannelFuture extends Future<Void> {
......
}
也就是说这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功,进而打印出连接信息。具体做法很简单,只需要对代码进行以下改动:
ChannelFuture f = b.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else {
System.err.println("连接失败!");
}
}).sync();
五、Netty其他功能
5.1 Netty作为网络框架处理TCP 粘包/拆包
5.1.1 粘包/拆包定义
基于 TCP 发送数据的时候,出现了多个字符串“粘”在了一起或者一个字符串被“拆”开的问题,比如:
server端发送 helloworld
server端发送 helloworld
server端发送 helloworld
server端发送 helloworld
这里服务端发送四次helloworld,按理说,客户端应该收到四次helloworld,但是,有时候,情况会变为这样:
客户端收到:helloworldhelloworld(粘包)
客户端收到:hello
客户端收到:worldhelloword (拆包)
TCP 粘包/拆包不分方向:上面只是举例子,并不是只有服务端发送,客户端接收才会出现粘包、拆包,只要是使用TCP协议,一端发送,另一端接收,就会出现粘包、拆包。
5.1.2 Nettty中对于TCP粘包/拆包的处理
方式1:使用Netty自带的解码器,在发送端插入分隔符避免接收端的粘包拆包
LineBasedFrameDecoder:这是一个,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取;
DelimiterBasedFrameDecoder:这是一个自定义分隔符解码器,LineBasedFrameDecoder 实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器,是DelimiterBasedFrameDecoder 的子类;
FixedLengthFrameDecoder:这个一个固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。
方式2:自定义序列化编解码器,将要传输的JavaBean对象序列化为二进制字节流
在 Java 中自带的有实现 Serializable 接口来实现序列化,但由于它性能、安全性等原因一般情况下是不会被使用到的。
通常情况下,我们使用 Protostuff、Hessian2、json 序列方式比较多,另外还有一些序列化性能非常好的序列化方式也是很好的选择:
专门针对 Java 语言的:Kryo,FST 等等;
跨语言的:Protostuff(基于 protobuf 发展而来),ProtoBuf,Thrift,Avro,MsgPack 等等。
5.2 TCP长连接和短连接
三次握手建立连接和四次挥手释放连接
TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
短连接:
短连接定义:server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。
短连接优点:管理和实现简单(一般来说,如果一种方案的优点仅仅是简单,那么这种方案是要被放弃的,Netty使用长连接,皮!);
短连接缺点:TCP每一次的读写操作之前,都要建立连接,带来大量网络资源的消耗,并且连接的建立也需要耗费时间。
长连接:
长连接定义:client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。
长连接优点:可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。
5.3 底层原理:Netty心跳机制
问题1:为什么需要心跳机制?
回答1:核心原因在于Netty使用TCP长连接的方式,然后,在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, Netty 引入 心跳机制 。
问题2:心跳机制是如果工作的?
回答2:其一,心跳机制的工作原理:在 client 与 server 之间在一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性。
其二,TCP自带长连接,但是我们一般在应用层实现,Netty心跳机制核心类IdleStateHandler:TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler 。
附加:RPC框架中的心跳机制有三种:pull拉 push推 长连接(即ping-pong),常见的Netty是使用长连接 ping-pong,Eureak使用pull拉方式。
5.4 Netty的零拷贝技术
零拷贝的定义:
“零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。
零拷贝意义:
用于通过网络传输文件时节省 CPU 周期和内存带宽。
在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。
Netty 中的零拷贝体现在以下几个方面:
使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。
ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。
通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题。
六、尾声
Netty,网络通信的权威专家,完成了。
天天打码,天天进步!!!
以上是关于Netty,网络通信的权威专家的主要内容,如果未能解决你的问题,请参考以下文章