文章目录
- 干货1:从Linux五种IO到Netty?从Socket网络编程到Netty?(刚开始,一定要从底层源头上说,拉高逼格,两个底层源头,NIO + Socket网络编程)
- 干货2:Netty 可以用来做什么
- 干货3:Netty核心组件(Channel)
- 组件1:Channel(NioServerSocketChannel服务端 和NioSocketChannel客户端)
- 组件2:EventLoop事件循环,表示事件实体(Netty最核心的概念)
- 组件3:ChannelFuture(事件监听者listener)
- 组件4:ChannelHandler 和 ChannelPipeline
- 干货4:Netty中的 EventloopGroup类 和 EventLoop类
- 干货5:Netty中 Bootstrap类和 ServerBootstrap类
- 干货6:NioEventLoopGroup 无参构造函数,启动cpu核心数*2的线程
- 干货7:Reactor三种线程模式(从Reactor到Netty,Netty如何实现Reactor的三种线程模式),Netty三种线程模式
- 干货8:Netty启动过程(Netty服务端启动过程 + Netty客户端启动过程)
- 干货9:名词解释:TCP 粘包/拆包?
- 第一,介绍粘包/拆包定义
- **第二,Nettty中对于TCP粘包/拆包的处理:**
- 干货10:名词解释:TCP 长连接和短连接(简单,对比方式,字面意思,长期连接和短期连接)
- 干货11:底层原理:Netty 心跳机制
- 干货12:Netty零拷贝
金手指:对于面试中开放性试题:就是你表现的的时候了,一定要全程好好表现,拉高逼格
第一,组织好面试语言
优先:表格对比、语言描述(示例图用于记忆语言);
不需要:源码、举例子
第二,多讲干货,少说套话
干货1:从Linux五种IO到Netty?从Socket网络编程到Netty?(刚开始,一定要从底层源头上说,拉高逼格,两个底层源头,NIO + Socket网络编程)
支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
Netty的应用(干货):很多中间件使用Netty作为网络通信框架,比如 阿里的 Dubbo、淘宝的RocketMQ、索引库Elasticsearch、RPC框架 gRPC 、 大数据Hadoop,都默认使用Netty作为网络通信框架。
金手指:
Netty是最好的网络通信框架之一,
从Java层面,Netty涉及Java NIO和Java 网络编程(Socket通信)两个方面的内容,
在IO处理方面,它封装了基于NIO的Reactor,
在网络通信方面,它基于Java Socket网络编程封装了TCP UDP 通信方式,
**1、Java 三种IO是基于Linux底层五种IO实现的(相关博客好了:两篇博客:Linux五种IO+Java三种IO)
2、Reactor三种模式是基于Java NIO实现的,是对Nio的封装(相关博客好了:一篇博客:从NIO到Reactor三种模式)
3、Netty是基于Reactor实现的(Nio+socket 变为了 网络框架)(两篇博客:netty源码解析,解释netty是如何基于Reactor实现的)
4、RPC可以使用netty + spring + zookeeper可以实现一个RPC框架(一篇博客:实践类:使用netty + spring + zookeeper可以实现一个RPC框架)
4、很多RPC框架使用netty作为通信:阿里Dubbo RPC框架、淘宝的RocketMQ 消息中间件、游戏领域、大数据领域Hadoop **
序列化和RPC的关系:RPC存在网络传输,所以需要一种对Java Bean的序列化技术
Netty是什么?
通俗的说:Netty一个好使的处理Socket的工具。
1)本质:JBoss做的一个Jar包
2)目的:快速开发高性能、高可靠性的网络服务器和客户端程序
3)优点:提供异步的、事件驱动的网络应用程序框架和工具
如果没有Netty?
远古:java.net + java.io
近代:java.nio
现在:替代品:Mina,Grizzly
对比:Netty与Mina两个网络通信框架
1、都是Trustin Lee的作品,Netty更晚;
2、它们的架构差别不大,Mina靠apache生存,而Netty靠jboss,和jboss的结合度非常高,Netty有对google protocal buf的支持,有更完整的ioc容器支持(spring,guice,jbossmc和osgi);
Netty应用:Dubbo RocketMQ
随着网站规模的不断扩大,系统并发访问量也越来越高,传统基于 Tomcat 等 Web 容器的垂直架构已经无法满足需求,需要拆分应用进行服务化,以提高开发和维护效率。从组网情况看,垂直的架构拆分之后,系统采用分布式部署,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。
典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。它的架构图如下:
其中,服务提供者和服务消费者之间,服务提供者、服务消费者和性能统计节点之间使用 Netty 进行异步/同步通信。
除了 Dubbo 之外,淘宝的消息中间件 RocketMQ 的消息生产者和消息消费者之间,也采用 Netty 进行高性能、异步通信。
除了阿里系和淘宝系之外,很多其它的大型互联网公司或者电商内部也已经大量使用 Netty 构建高性能、分布式的网络服务器。
注意:Netty应该划分到Java网络编程模块,但是因为我的博客Java IO单独划分一个二级分类,所以将Netty放到Java IO分类里面。
干货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 来做的。
干货3:Netty核心组件(Channel)
Netty核心组件:
Channel、EventLoop、ChannelFuture、ChannelHandler 和 ChannelPipeline
对于Netty,直接使用Reactor类比类学习比较好
Reactor服务端NIO三个核心组件:
Dispatcher/Reactor负责事件分发、Acceptor负责处理客户端连接、Handler处理非连接事件(例如:读写事件)。
组件1:Channel(NioServerSocketChannel服务端 和NioSocketChannel客户端)
Channel 接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。
比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 NIO 编程模型中的ServerSocketChannel 以及 SocketChannel 两个概念对应上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。
组件2:EventLoop事件循环,表示事件实体(Netty最核心的概念)
EventLoop 中文译为事件循环,定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
说白了,EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
金手指:Netty的两个组件:Channel 和 EventLoop 是什么关系?
Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。
Reactor服务端NIO三个核心组件:
Dispatcher/Reactor负责事件分发、Acceptor负责处理客户端连接、Handler处理非连接事件(例如:读写事件)。
组件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()方法让异步的操作变成同步的。
组件4:ChannelHandler 和 ChannelPipeline
下面这段代码使用过 Netty 的小伙伴应该不会陌生,我们指定了序列化编解码器(序列化解码器用于处理Tcp 粘包/拆包)以及自定义的 ChannelHandler 处理消息。
b.group(eventLoopGroup).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected 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 。
金手指:
干货4: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 。
干货5:Netty中 Bootstrap类和 ServerBootstrap类
ServerBootstrap 客户端的启动引导类/辅助类,具体使用方法如下:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {//2.创建服务端启动引导/辅助类:ServerBootstrapServerBootstrap 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 {//创建客户端启动引导/辅助类:BootstrapBootstrap b = new Bootstrap();//指定线程模型b.group(group).......// 尝试建立连接ChannelFuture f = b.connect(host, port).sync();f.channel().closeFuture().sync();
} finally {// 优雅关闭相关线程组资源group.shutdownGracefully();
}
小结:
ServerBootstrap通常使用 bind() 方法绑定本地的端口上,作为一个 Netty TCP 协议通信中的服务端,然后等待客户端的连接。
Bootstrap 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。
ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的处理;
Bootstrap 只需要配置一个线程组— EventLoopGroup。
干货6: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类
每个NioEventLoopGroup对象内部都会分配一组NioEventLoop,其大小是 nThreads, 这样就构成了一个线程池, 每一个NIOEventLoop 和一个线程相对应,这和我们上面说的 EventloopGroup 和 EventLoop关系这部分内容相对应。
NioEventLoopGroup类和NioEventLoopGroup类直接没有继承关系,但是
EventLoopGroup和EventLoop是父子继承关系,EventLoopGroup类是父类,EventLoop类是子类
干货7:Reactor三种线程模式(从Reactor到Netty,Netty如何实现Reactor的三种线程模式),Netty三种线程模式
大部分网络框架都是基于 Reactor 模式设计开发的。
“Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 IO 的场景。
在 Netty 中,主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 。
我们实现服务端的时候,一般会初始化两个线程组:
bossGroup :接收连接。
workerGroup :负责具体的处理,交由对应的 Handler 处理。
下面我们来详细看一下 Netty 中的线程模型吧!
1.单线程模型 :
一个线程需要执行处理所有的 accept、read、decode、process、encode、send 事件。对于高负载、高并发,并且对性能要求比较高的场景不适用。
对应到 Netty 代码是下面这样的
“使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2 。
//1.eventGroup既用于处理客户端连接,又负责具体的处理。
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
boobtstrap.group(eventGroup, eventGroup)
//…
2.多线程模型
一个 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)
//…
3.主从多线程模型
从一个 主线程 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)
//…
*金手指:Netty如何实现Reactor的三种线程模式
NioEventLoopGroup 类的两个构造函数
NioEventLoopGroup 类带参构造函数:线程数由int型实参指定。 一般是accept接收请求使用
NioEventLoopGroup 类无参构造函数:线程数为运行主机的 CPU 核心数 2 。 一般是read/write读写使用
只要合理构造,就可以构造出Reactor三种线程模式,没什么好说的,简单
干货8:Netty启动过程(Netty服务端启动过程 + Netty客户端启动过程)
Netty 服务端 启动过程
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理EventLoopGroup bossGroup = new NioEventLoopGroup(1); // EventLoopGroup workerGroup = new NioEventLoopGroup();try {//2.创建服务端启动引导/辅助类:ServerBootstrapServerBootstrap b = new ServerBootstrap();//3.给引导类配置两大线程组,确定了线程模型b.group(bossGroup, workerGroup)// (非必备)打印日志.handler(new LoggingHandler(LogLevel.INFO))// 4.指定 IO 模型,使用NIO.channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overridepublic 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)
详见:Netty服务端初始三篇博客
第一步,new NioEventLoopGroup(); // 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
第二步,new ServerBootstrap(); b.group… //2.创建服务端启动引导/辅助类:ServerBootstrap,并给给引导类配置两大线程组,确定了线程模型,指定 IO 模型,使用NIO
第三步,b.bind(port) // bind 方法绑定端口, 金手指:sync 方法阻塞知道绑定完成
简单解析一下服务端的创建过程具体是怎样的(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()方法绑定端口
Netty 客户端 启动过程
//1.创建一个 NioEventLoopGroup 对象实例
EventLoopGroup group = new NioEventLoopGroup();
try {//2.创建客户端启动引导/辅助类:BootstrapBootstrap b = new Bootstrap();//3.指定线程组b.group(group)//4.指定 IO 模型.channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overridepublic 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()方法进行连接,这个方法需要指定两个参数:
inetHost : ip 地址
inetPort : 端口号
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 {
…
}
也就是说这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功,进而打印出连接信息。具体做法很简单,只需要对代码进行以下改动:
ChannelFuture f = b.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println(“连接成功!”);
} else {
System.err.println(“连接失败!”);
}
}).sync();
干货9:名词解释:TCP 粘包/拆包?
第一,介绍粘包/拆包定义
基于 TCP 发送数据的时候,出现了多个字符串“粘”在了一起或者一个字符串被“拆”开的问题。比如:
server端发送 helloworld
server端发送 helloworld
server端发送 helloworld
server端发送 helloworld
这里服务端发送四次helloworld,按理说,客户端应该收到四次helloworld,但是,有时候,情况会变为这样
客户端收到:helloworldhelloworld(粘包)
客户端收到:hello
客户端收到:worldhelloword (拆包)
注意:这里只是举例子,并不是只有服务端发送,客户端接收才会出现粘包、拆包,只要是使用TCP协议,一端发送,另一端接收,就会出现粘包、拆包。
第二,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 等等
干货10:名词解释:TCP 长连接和短连接(简单,对比方式,字面意思,长期连接和短期连接)
三次握手建立连接和四次挥手释放连接:
TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
短连接:
短连接定义: server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。
短连接优点:管理和实现简单;(一般来说,如果一种方案的优点仅仅是简单,那么这种方案是要被放弃的,Netty使用长连接)
短连接缺点:TCP每一次的读写操作之前,都要建立连接,带来大量网络资源的消耗,并且连接的建立也需要耗费时间。
长连接:
长连接定义:client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。
长连接优点:可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。
干货11:底层原理:Netty 心跳机制
第一,首先解释为什么需要心跳机制:
从长连接出发:接上面,Netty使用TCP长连接的方式,然后,在 TCP 保持长连接的过程中,
核心原因:可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。
为了解决这个问题, Netty 引入 心跳机制 。
第二,解释心跳机制工作原理:
心跳机制的工作原理:在 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
pull拉 Eureak使用这种方式
干货12: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 方式导致的内存拷贝问题.