尚硅谷的源码部分暂时不再记录笔记,因为我觉得源码这东西,韩老师讲的不太好,弹幕和评论也有说。源码这东西,先把入门的消化一下,然后通过书或者博客来看源码会比较好,你只有先会用,看源码才会有感觉。
Google Protobuf
编码和解码的基本介绍
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码[示意图]
codec
(编解码器)的组成部分有两个:decoder
(解码器)和 encoder
(编码器)。encoder
负责把业务数据转换成字节码数据,decoder
负责把字节码数据转换成业务数据
Netty 本身的编码解码的机制和问题分析
Netty
自身提供了一些 codec
(编解码器)
Netty
提供的编码器
StringEncoder
:对字符串数据进行编码。
ObjectEncoder
:对Java对象进行编码。
Netty
提供的解码器
StringDecoder
,对字符串数据进行解码
ObjectDecoder
,对 Java 对象进行解码
Netty
本身自带的 ObjectDecoder
和 ObjectEncoder
可以用来实现 POJO
对象或各种业务对象的编码和解码,底层使用的仍是Java序列化技术,而Java序列化技术本身效率就不高,存在如下问题
- 无法跨语言
- 序列化后的体积太大,是二进制编码的5倍多。
- 序列化性能太低
- 引出新的解决方案[
Google
的 Protobuf
]
Protobuf
Protobuf
基本介绍和使用示意图
Protobuf
是 Google
发布的开源项目,全称 Google Protocol Buffers
,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC
[远程过程调用 remote procedure call
]数据交换格式。目前很多公司 从http + json 转向tcp + protobuf
,效率会更高。
- 参考文档:https://developers.google.com/protocol-buffers/docs/proto 语言指南
Protobuf
是以 message
的方式来管理数据的.
- 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的](支持目前绝大多数语言,例如
C++
、C#
、Java
、python
等)
- 高性能,高可靠性
- 使用
protobuf
编译器能自动生成代码,Protobuf
是将类的定义使用 .proto
文件进行描述。说明,在 idea
中编写 .proto
文件时,会自动提示是否下载 .ptoto
编写插件.可以让语法高亮。
- 然后通过
protoc.exe
编译器根据 .proto
自动生成 .java
文件
protobuf
使用示意图
Protobuf 快速入门实例
编写程序,使用 Protobuf
完成如下功能
- 客户端可以发送一个
StudentPoJo
对象到服务器(通过 Protobuf
编码)
- 服务端能接收
StudentPoJo
对象,并显示信息(通过 Protobuf
解码)
Student.proto
编译
protoc.exe —java_out=.Student.proto
将生成的 StudentPOJO 放入到项目使用
生成的StudentPOJO代码太长就不贴在这里了
NettyServer
NettyServerHandler
NettyClient
NettyClientHandler
Protobuf 快速入门实例 2
- 编写程序,使用
Protobuf
完成如下功能
- 客户端可以随机发送
StudentPoJo
/ WorkerPoJo
对象到服务器(通过 Protobuf
编码)
- 服务端能接收
StudentPoJo
/ WorkerPoJo
对象(需要判断是哪种类型),并显示信息(通过 Protobuf
解码)
proto
NettyServer
NettyServerHandler
NettyClient
NettyClientHandler
Netty 编解码器和 Handler 调用机制
基本说明
Netty
的组件设计:Netty
的主要组件有 Channel
、EventLoop
、ChannelFuture
、ChannelHandler
、ChannelPipe
等
ChannelHandler
充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现 ChannelInboundHandler
接口(或 ChannelInboundHandlerAdapter
),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从 ChannelInboundHandler
冲刷数据。业务逻辑通常写在一个或者多个 ChannelInboundHandler
中。ChannelOutboundHandler
原理一样,只不过它是用来处理出站数据的
ChannelPipeline
提供了 ChannelHandler
链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过 pipeline
中的一系列 ChannelOutboundHandler
,并被这些 Handler
处理,反之则称为入站的
出站,入站如果搞不清楚,看下面的Netty的handler链的调用机制,通过一个例子和图讲清楚
编码解码器
- 当
Netty
发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如 java
对象);如果是出站消息,它会被编码成字节。
Netty
提供一系列实用的编解码器,他们都实现了 ChannelInboundHadnler
或者 ChannelOutboundHandler
接口。在这些类中,channelRead
方法已经被重写了。以入站为例,对于每个从入站 Channel
读取的消息,这个方法会被调用。随后,它将调用由解码器所提供的 decode()
方法进行解码,并将已经解码的字节转发给 ChannelPipeline
中的下一个 ChannelInboundHandler
。
解码器 - ByteToMessageDecoder
- 关系继承图
- 由于不可能知道远程节点是否会一次性发送一个完整的信息,
tcp
有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理.【后面有说TCP的粘包和拆包问题】
- 一个关于
ByteToMessageDecoder
实例分析
Netty的handler链的调用机制
实例要求:
- 使用自定义的编码器和解码器来说明
Netty
的 handler
调用机制
客户端发送 long
→ 服务器
服务端发送 long
→ 客户端
读者可以看下这个图,带着这个图去看下面的例子。
MyServer
MyServerInitializer
MyServerHandler
MyClient
MyClientInitializer
MyClientHandler
MyByteToLongDecoder
MyLongToByteEncoder
效果
出站入站
关于出站入站,很多人可能有点迷糊
1)客户端有出站入站,服务端也有出站入站
2)以客户端为例,如果有服务端传送的数据到达客户端,那么对于客户端来说就是入站;
如果客户端传送数据到服务端,那么对于客户端来说就是出站;
同理,对于服务端来说,也是一样的,有数据来就是入站,有数据输出就是出站
3)为什么服务端和客户端的Serverhandler都是继承SimpleChannelInboundHandler
,而没有ChannelOutboundHandler
出站类?
实际上当我们在handler中调用ctx.writeAndFlush()方法后,就会将数据交给ChannelOutboundHandler进行出站处理,只是我们没有去定义出站类而已,若有需求可以自己去实现ChannelOutboundHandler出站类
4)总结就是客户端和服务端都有出站和入站的操作
**服务端发数据给客户端:**服务端-⇒出站-⇒Socket通道-⇒入站-⇒客户端
**客户端发数据给服务端:**客户端-⇒出站-⇒Socket通道-⇒入站-⇒服务端
下面是Netty官方源码给的图,我个人觉的不是太好理解,上面的图好理解一些
ByteToMessageDecoder的小细节
- 由于发送的字符串是16字节,根据上面注释说的内容,decode会被调用两次
如下图验证结果:
-
同时又引出了一个小问题
当我们MyClientHandler
传一个Long时,会调用我们的MyLongToByteEncoder
的编码器。那么控制台就会打印这样一句话:MyLongToByteEncoder encode 被调用。但是这里并没有调用编码器,这是为什么呢?
MyClientHandler
这个处理器的后一个处理器是MyLongToByteEncoder
MyLongToByteEncoder
的父类是MessageToByteEncoder
,在MessageToByteEncoder
中有下面的一个方法
- 当我们以这样的形式发送数据
这两个类型并不匹配,也就不会走编码器。因此我们编写 Encoder 是要注意传入的数据类型和处理的数据类型一致
结论:
- 不论解码器
handler
还是编码器 handler
即接收的消息类型必须与待处理的消息类型一致,否则该 handler
不会被执行
- 在解码器进行数据解码时,需要判断缓存区(
ByteBuf
)的数据是否足够,否则接收到的结果会期望结果可能不一致。
解码器 - ReplayingDecoder
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
ReplayingDecoder
扩展了 ByteToMessageDecoder
类,使用这个类,我们不必调用 readableBytes()
方法,也就不用判断还有没有足够的数据来读取。参数 S
指定了用户状态管理的类型,其中 Void
代表不需要状态管理
- 应用实例:使用
ReplayingDecoder
编写解码器,对前面的案例进行简化[案例演示]
ReplayingDecoder
使用方便,但它也有一些局限性:
- 并不是所有的
ByteBuf
操作都被支持,如果调用了一个不被支持的方法,将会抛出一个 UnsupportedOperationException
。
ReplayingDecoder
在某些情况下可能稍慢于 ByteToMessageDecoder
,例如网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢
其它编解码器
LineBasedFrameDecoder
:这个类在 Netty
内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据。
DelimiterBasedFrameDecoder
:使用自定义的特殊字符作为消息的分隔符。
HttpObjectDecoder
:一个 HTTP
数据的解码器
LengthFieldBasedFrameDecoder
:通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息。
Log4j 整合到 Netty
- 在
Maven
中添加对 Log4j
的依赖在 pom.xml
- 配置
Log4j
,在 resources/log4j.properties
- 演示整合
![](./image/introduction/chapter_003/0014.jpg”/>
TCP 粘包和拆包及解决方案
TCP 粘包和拆包基本介绍
TCP
是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket
,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle
算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
- 由于
TCP
无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图
TCP
粘包、拆包图解
假设客户端分别发送了两个数据包 D1
和 D2
给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是
D1
和 D2
,没有粘包和拆包
- 服务端一次接受到了两个数据包,
D1
和 D2
粘合在一起,称之为 TCP
粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的
D1
包和 D2
包的部分内容,第二次读取到了 D2
包的剩余内容,这称之为 TCP
拆包
- 服务端分两次读取到了数据包,第一次读取到了
D1
包的部分内容 D1_1
,第二次读取到了 D1
包的剩余部分内容 D1_2
和完整的 D2
包。
TCP 粘包和拆包现象实例
在编写 Netty
程序时,如果没有做处理,就会发生粘包和拆包的问题
看一个具体的实例:
MyServer
MyServerInitializer
MyServerHandler
MyClient
MyClientInitializer
MyClientHandler
效果
第一次运行:
Client
Server
第二次运行:
Client
Server
可以看到第一次运行时,服务器一次性将10个数据都接收了,第二次运行时分六次接收的,这就很形象的看出了TCP的粘包现象。
TCP 粘包和拆包解决方案
- 常用方案:使用自定义协议+编解码器来解决
- 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的
TCP
粘包、拆包。
看一个具体的实例
- 要求客户端发送
5
个 Message
对象,客户端每次发送一个 Message
对象
- 服务器端每次接收一个
Message
,分 5
次进行解码,每读取到一个 Message
,会回复一个 Message
对象给客户端。
MessageProtocol
MyServer
MyServerInitializer
MyServerHandler
MyClient
MyClientInitializer
MyClientHandler
MyMessageDecoder
MyMessageEncoder
效果
Client输出
Server输出
无论运行几次,Server都是分5次接收的,这样就解决了TCP粘包问题。
用 Netty 自己实现简单的RPC
RPC 基本介绍
RPC(Remote Procedure Call)
—远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程
- 两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样(如图)
过程:
-
调用者(Caller
),调用远程API(Remote API
)
-
调用远程API会通过一个RPC代理(RpcProxy
)
-
RPC代理再去调用RpcInvoker
(这个是PRC的调用者)
-
RpcInvoker
通过RPC连接器(RpcConnector
)
-
RPC连接器用两台机器规定好的PRC协议(RpcProtocol
)把数据进行编码
-
接着RPC连接器通过RpcChannel通道发送到对方的PRC接收器(RpcAcceptor)
-
PRC接收器通过PRC协议进行解码拿到数据
-
然后将数据传给RpcProcessor
-
RpcProcessor
再传给RpcInvoker
-
RpcInvoker
调用Remote API
-
最后推给被调用者(Callee)
-
常见的 RPC
框架有:比较知名的如阿里的 Dubbo
、Google
的 gRPC
、Go
语言的 rpcx
、Apache
的 thrift
,Spring
旗下的 SpringCloud
。
我们的RPC 调用流程图
RPC 调用流程说明
- 服务消费方(
client
)以本地调用方式调用服务
client stub
接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
client stub
将消息进行编码并发送到服务端
server stub
收到消息后进行解码
server stub
根据解码结果调用本地的服务
- 本地服务执行并将结果返回给
server stub
server stub
将返回导入结果进行编码并发送至消费方
client stub
接收到消息并进行解码
- 服务消费方(
client
)得到结果
小结:RPC
的目标就是将 2 - 8
这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用
己实现 Dubbo RPC(基于 Netty)
需求说明
Dubbo
底层使用了 Netty
作为网络通讯框架,要求用 Netty
实现一个简单的 RPC
框架
- 模仿
Dubbo
,消费者和提供者约定接口和协议,消费者远程调用提供者的服务,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用 Netty 4.1.20
设计说明
- 创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。
- 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。
- 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用
Netty
请求提供者返回数据
- 开发的分析图
代码
封装的RPC
可以把这块代码理解成封装的dubbo
NettyServer
NettyServerHandler
NettyClientHandler
NettyClient
接口
服务端(provider)
HelloServiceImpl
ServerBootstrap
客户端(消费者)
调用过程
ClientBootstrap#main
发起调用
- 走到下面这一行代码后
-
调用NettyClient#getBean
,在此方法里与服务端建立链接。
-
于是就执行NettyClientHandler#channelActive
-
接着回到NettyClient#getBean
调用NettyClientHandler#setPara
,调用完之后再回到NettyClient#getBean
,用线程池提交任务
-
因为用线程池提交了任务,就准备执行NettyClientHandler#call
线程任务
-
在NettyClientHandler#call
中发送数据给服务提供者
由于还没收到服务提供者的数据结果,所以wait住
-
来到了服务提供者这边,从Socket通道中收到了数据,所以执行NettyServerHandler#channelRead
,然后因为此方法中执行了
-
就去HelloServiceImpl#hello
中执行业务逻辑,返回数据给NettyServerHandler#channelRead
,NettyServerHandler#channelRead
再把数据发给客户端
-
NettyClientHandler#channelRead
收到服务提供者发来的数据,唤醒之前wait的线程
-
所以之前wait的线程从NettyClientHandler#call
苏醒,返回result给NettyClient#getBean
-
NettyClient#getBean
get()到数据,ClientBootstrap#main
中的此函数调用返回,得到服务端提供的数据。
13.至此,一次RPC调用结束。
效果
ClientBootstrap打印
ServerBootstrap打印