- Netty编解码器
- TCP粘包/拆包的问题及解决
- ⾃研RPC实战
- Netty核⼼源码剖析
- Netty优化建议
1、Netty编解码器
1.1、什么是编解码器
在⽹络中传输数据时,⽆论以什么的格式发送(int、String、Long等)都会以字节流的⽅式进⾏传递,客户端将原来的格式数据转化为字节,称之为编码(encode),服务端将字节形式转化为原来的格式,称之为解码(decode),编解码统称为codec。
编解码器包括编码器与解码器两部分,编码器负责出站数据操作,解码器负责⼊站数据操作。
1.2、解码器
解码器是负责⼊站的数据操作,那么解码器也⼀定实现了ChannelInboundHandler接⼝,所以编解码器本质上也是ChannelHandler。
Netty中提供了ByteToMessageDecoder的抽象实现,⾃定*解义**码器只需要继承该类,实现decode()即可。Netty也提供了⼀些常⽤的解码器实现,基本都是开箱即⽤的。⽐如:
- RedisDecoder 基于Redis协议的解码器
- XmlDecoder 基于XML格式的解码器
- JsonObjectDecoder 基于json数据格式的解码器
- HttpObjectDecoder 基于http协议的解码器
Netty也提供了MessageToMessageDecoder,将⼀种格式转化为另⼀种格式的解码器,也提供了⼀些实现:
- StringDecoder 将接收到ByteBuf转化为字符串
- ByteArrayDecoder 将接收到ByteBuf转化字节数组
- Base64Decoder 将由ByteBuf或US-ASCII字符串编码的Base64解码为ByteBuf。
1.2.1、案例
将传⼊的字节流转化为Integer类型。


在Handler中使⽤:

在pipeline中添加解码器:

1.3、编码器
编码器与解码器是相反的操作,将原有的格式转化为字节的过程,在Netty中提供了MessageToByteEncoder的抽象实现,它实现了ChannelOutboundHandler接⼝,本质上也是ChannelHandler。
⼀些实现类:
- ObjectEncoder 将对象(需要实现Serializable接⼝)编码为字节流
- SocksMessageEncoder 将SocksMessage编码为字节流
- HAProxyMessageEncoder 将HAProxyMessage编码成字节流
Netty也提供了MessageToMessageEncoder,将⼀种格式转化为另⼀种格式的编码器,也提供了⼀些实现:
- RedisEncoder 将Redis协议的对象进⾏编码
- StringEncoder 将字符串进⾏编码操作
- Base64Encoder 将Base64字符串进⾏编码操作
1.3.1、案例
将Integer类型编码为字节进⾏传递。
⾃定义编码器:

在Handler直接输出数字即可:

在pipeline中添加编码器:

1.4、案例:开发http服务器
在Netty中提供了http的解码器,我们通过该解码器进⾏http服务器的开发。实现效果:

Server


ServerHandler:


RequestParser:


1.5、对象的编解码
对于JavaBean对象,Netty也⽀持了Object对象的编解码,其实也就是对象的序列化,要求java对象需要java.io.Serializable接⼝。
定义javabean对象:

1.5.1、服务端
NettyObjectServer:

ServerHandler:

1.5.2、客户端
NettyObjectClient:


ClientHandler:


1.6、Hessian编解码
JDK序列化使⽤是⽐较⽅便,但是它的性能较差,序列化后的字节⼤⼩也⽐较⼤,所以⼀般在项⽬中不会使⽤⾃带的序列化,⽽是会采⽤第三⽅的序列化框架。
我们以Hessian为例,演示下如何与Netty整合进⾏编解码处理。
导⼊Hessian依赖:

User对象:


1.6.1、编解码器
Hessian序列化⼯具类:


编码器:

解码器:

1.6.2、服务端



1.6.3、客户端




2、TCP粘包/拆包的问题及解决
2.1、ReplayingDecoder
在前⾯案例中,当需要获取int数据时,需要进⾏判断是否够4个字节,如果解码业务过于复杂的话,这样的判断会显得⾮常的繁琐,在Netty中提供了ReplayingDecoder就可以解决这样的问题。ReplayingDecoder也是继承了ByteToMessageDecoder进⾏的扩展。
javadoc⽂档中的⼀个示例:


使⽤ReplayingDecoder后:

基本原理:
- 使⽤了特殊的ByteBuf,叫做ReplayingDecoderByteBuf,扩展了ByteBuf
- 重写了ByteBuf的readXxx()等⽅法,会先检查可读字节⻓度,⼀旦检测到不满⾜要求就直接抛出REPLAY(REPLAY继承ERROR)
- ReplayingDecoder重写了ByteToMessageDecoder的callDecode()⽅法,捕获Signal并在catch块中重置ByteBuf的readerIndex。
- 继续等待数据,直到有了数据后继续读取,这样就可以保证读取到需要读取的数据。
- 类定义中的泛型 S 是⼀个⽤于记录解码状态的状态机枚举类,在state(S s)、checkpoint(S s)等⽅法中会⽤到。在简单解码时也可以⽤java.lang.Void来占位。
需要注意:
- buffer的部分操作(readBytes(ByteBuffer dst)、retain()、release()等⽅法会直接抛出异常)
- 在某些情况下会影响性能(如多次对同⼀段消息解码)
TCP是基于流的,只保证接收到数据包分⽚顺序,⽽不保证接收到的数据包每个分⽚⼤⼩。因此在使⽤ReplayingDecoder时,即使不存在多线程,同⼀个线程也可能多次调⽤decode()⽅法。在decode中修改ReplayingDecoder的类变量时必须⼩⼼谨慎。


ByteToIntegerDecoder2的实现:

2.2、什么是TCP粘包/拆包问题?
TCP是流传递的,所谓流,就是⼀串没有界限的数据,服务端接收到客户端发来的数据,并不确定这是⼀条数据,还是多条数据,应该如何拆包,服务端是不知道的。
所以,客户端与服务端就需要约定好拆包的规则,客户端按照此规则进⾏粘包,⽽服务端按照此规则进⾏拆包,这就是TCP的粘包与拆包,如果不约定好,就会出现服务端不能按照期望拿到数据。
实际上,彼此约定的规则就是协议,⾃定义协议就是⾃定义规则。
2.2.1、案例:演示TCP粘包/拆包问题
客户端:向服务端发送10条消息,并且记录服务端返回的消息的数量。


服务端:接收消息并且记录消息的数量,向客户端发送响应。




测试结果:

2.3、解决⽅法
⼀般来讲有3中⽅法解决TCP的粘包与拆包问题:
- 在发送的数据包中添加头,在头⾥存储数据的⼤⼩,服务端就可以按照此⼤⼩来读取数据,这样就知道界限在哪⾥了。
- 以固定的⻓度发送数据,超出的分多次发送,不⾜的以0填充,接收端就以固定⻓度接收即可。
- 在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
2.4、实战:解决TCP的粘包/拆包问题
2.4.1、⾃定义协议

2.4.2、编解码器
编码器:

解码器:

2.4.3、客户端




2.4.4、服务端


2.4.5、测试


3、⾃研RPC实战
3.1、⾃研RPC设计说明

服务端
- 接收到客户端发来的消息后,进⾏解码操作
- 根据消息中的接⼝信息,通过反射找到其实现类,执⾏⽬标⽅法
- 将返回的数据再进⾏编码操作,发送给客户端
客户端
- 客户端通过代理的⽅式,获取到接⼝的代理类
- 通过代理类进⾏发送消息,实际上是向服务端发送消息
- 通过编码器将消息进⾏编码操作
- 接收到服务端的响应后,进⾏解码操作,返回数据给⽅法的调⽤⽅
注册中⼼
- 注册中⼼并⾮课程重点,故不做实现

⼯程说明:
- myrpc-core rpc核⼼实现
- myrpc-demo-api 示例api定义
- myrpc-demo-client 客户端示例
- myrpc-demo-server 服务端示例
3.2、⾃定义协议
⾃定义协议,需要分别定义请求对象和响应对象,⽤于数据的传输。



3.3、编解码器
编解码器是在服务端以及客户端通⽤的,所以设计时需要考虑其通⽤性,不能将泛型对象硬编码到代码中。
编码器:

解码器:

Hessian序列化:



3.4、服务端
3.4.1、NettyServer


3.4.2、ServerInitializer

3.4.3、ServerHandler


3.4.4、ClassUtil





3.5、客户端
3.5.1、NettyClient


3.5.2、ClientInitializer

3.5.3、ClientHandler

3.5.4、RpcFutureResponse



3.5.5、ClientInvocationHandler


3.5.6、BeanFactory

3.6、示例
3.6.1、myrpc-demo-api
pom.xml:

定义pojo:




服务接⼝定义:

3.6.2、myrpc-demo-server
pom.xml:

服务实现:

启动服务:

3.6.3、myrpc-demo-client
pom.xml:

ClientServer:


3.7、测试


4、Netty核⼼源码剖析
4.1、服务端启动过程剖析
4.1.1、创建服务端Channel
主要流程:
- ServerBootstrap对象的bind()⽅法,也是⼊⼝⽅法
- AbstractBootstrap中的initAndRegister()进⾏创建Channel
- 创建Channel的⼯作由ReflectiveChannelFactory反射类中的newChannel()⽅法完成。
- NioServerSocketChannel中的构造⽅法中,通过jdk nio底层的SelectorProvider打开ServerSocketChannel。
- 在AbstractNioChannel的构造⽅法中,设置channel为⾮阻塞:ch.configureBlocking(false);
- 通过的AbstractChannel的构造⽅法,创建了id、unsafe、pipeline内容。
- 通过NioServerSocketChannelConfig获取tcp底层的⼀些参数
4.1.2、初始化服务端Channel
主流程:
- AbstractBootstrap中的initAndRegister()进⾏初始化channel,代码:init(channel);
- 在ServerBootstrap中的init()⽅法设置channelOptions以及Attributes。
- 紧接着,将⽤户⾃定义参数、属性保存到局部变量currentChildOptions、currentChildAttrs,以供后⾯使⽤
- 如果设置了serverBootstrap.handler()的话,会加⼊到pipeline中。
- 添加连接器ServerBootstrapAcceptor,有新连接加⼊后,将⾃定义的childHandler加⼊到连接的pipeline中:


4.1.3、注册selector
主要流程:
- initAndRegister()⽅法中的ChannelFuture regFuture = config().group().register(channel); 进⾏注册
- 在io.netty.channel.AbstractChannel.AbstractUnsafe#register()中完成实际的注册
- AbstractChannel.this.eventLoop = eventLoop; 进⾏eventLoop的赋值操作,后续的IO事件⼯作将在由该eventLoop执⾏。
- 调⽤register0(promise)中的doRegister()进⾏实际的注册
- io.netty.channel.nio.AbstractNioChannel#doRegister进⾏了⽅法实现
4.1.4、绑定端⼝
主要流程:
- ⼊⼝在io.netty.bootstrap.AbstractBootstrap#doBind0(),启动⼀个线程进⾏执⾏绑定端⼝操作
- 调⽤io.netty.channel.AbstractChannelHandlerContext#bind(java.net.SocketAddress,
- io.netty.channel.ChannelPromise)⽅法,再次启动线程执⾏
- 最终调⽤io.netty.channel.socket.nio.NioServerSocketChannel#doBind()⽅法进⾏绑定操作
4.2、连接请求过程源码剖析
4.2.1、新连接的接⼊
主要流程:
- ⼊⼝在io.netty.channel.nio.NioEventLoop#processSelectedKey(java.nio.channels.SelectionKey,
- io.netty.channel.nio.AbstractNioChannel)中进⼊NioMessageUnsafe的read()⽅法
- 调⽤io.netty.channel.socket.nio.NioServerSocketChannel#doReadMessages() ⽅法,创建jdk底层的channel,封装成NioSocketChannel添加到List容器中
- 创建NioSocketChannel对象
- new NioSocketChannel(this, ch),通过new的⽅式进⾏创建调⽤super的构造⽅法传⼊SelectionKey.OP_READ事件标识
- 创建id、unsafe、pipeline对象
- 设置⾮阻塞 ch.configureBlocking(false);
- 创建NioSocketChannelConfig对象
5、Netty优化建议
5.1、零拷⻉
Netty的零拷⻉主要体现在三个⽅⾯:
- Bytebuf 使⽤的是⽤池化的Direct Buffer类型使⽤的堆外内存,不需要进⾏字节缓冲区的⼆次拷⻉,如果使⽤堆内存,JVM会先拷⻉到堆内,再写⼊Socket,就多了⼀次拷⻉。
- CompositeByteBuf将多个ByteBuf封装成⼀个ByteBuf,在添加ByteBuf时不需要进程拷⻉。
- Netty的⽂件传输类DefaultFileRegion的transferTo⽅法将⽂件发送到⽬标channel中,不需要进⾏循环拷⻉,提升了性能。
5.2、使⽤EventLoop的任务调度
在EventLoop的⽀持线程外使⽤channel:

直接使⽤channel.writeAndFlush(data);
前者会直接放⼊channel所对应的EventLoop的执⾏队列,⽽后者会导致线程的切换。
5.3、减少ChannelPipline的调⽤⻓度

前者是将msg从整个ChannelPipline中⾛⼀遍,所有的handler都要经过,⽽后者是从当前handler⼀直到pipline的尾部,调⽤更短。
5.4、减少ChannelHandler的创建
如果channelhandler是⽆状态的(即不需要保存任何状态参数),那么使⽤Sharable注解,并在bootstrap时只创建⼀个实例,减少GC。否则每次连接都会new出handler对象。

同时需要注意ByteToMessageDecoder之类的编解码器是有状态的,不能使⽤Sharable注解。
5.5、⼀些配置参数的设置
ServerBootstrap启动时,通常bossG