netty源码全解与架构思维 (netty开发思路解析)

  • 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类型。

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

在Handler中使⽤:

netty架构模式详解,netty架构实战

在pipeline中添加解码器:

netty架构模式详解,netty架构实战

1.3、编码器

编码器与解码器是相反的操作,将原有的格式转化为字节的过程,在Netty中提供了MessageToByteEncoder的抽象实现,它实现了ChannelOutboundHandler接⼝,本质上也是ChannelHandler。

⼀些实现类:

  • ObjectEncoder 将对象(需要实现Serializable接⼝)编码为字节流
  • SocksMessageEncoder 将SocksMessage编码为字节流
  • HAProxyMessageEncoder 将HAProxyMessage编码成字节流

Netty也提供了MessageToMessageEncoder,将⼀种格式转化为另⼀种格式的编码器,也提供了⼀些实现:

  • RedisEncoder 将Redis协议的对象进⾏编码
  • StringEncoder 将字符串进⾏编码操作
  • Base64Encoder 将Base64字符串进⾏编码操作

1.3.1、案例

将Integer类型编码为字节进⾏传递。

⾃定义编码器:

netty架构模式详解,netty架构实战

在Handler直接输出数字即可:

netty架构模式详解,netty架构实战

在pipeline中添加编码器:

netty架构模式详解,netty架构实战

1.4、案例:开发http服务器

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

netty架构模式详解,netty架构实战

Server

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

ServerHandler:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

RequestParser:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

1.5、对象的编解码

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

定义javabean对象:

netty架构模式详解,netty架构实战

1.5.1、服务端

NettyObjectServer:

netty架构模式详解,netty架构实战

ServerHandler:

netty架构模式详解,netty架构实战

1.5.2、客户端

NettyObjectClient:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

ClientHandler:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

1.6、Hessian编解码

JDK序列化使⽤是⽐较⽅便,但是它的性能较差,序列化后的字节⼤⼩也⽐较⼤,所以⼀般在项⽬中不会使⽤⾃带的序列化,⽽是会采⽤第三⽅的序列化框架。

我们以Hessian为例,演示下如何与Netty整合进⾏编解码处理。

导⼊Hessian依赖:

netty架构模式详解,netty架构实战

User对象:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

1.6.1、编解码器

Hessian序列化⼯具类:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

编码器:

netty架构模式详解,netty架构实战

解码器:

netty架构模式详解,netty架构实战

1.6.2、服务端

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

1.6.3、客户端

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

2、TCP粘包/拆包的问题及解决

2.1、ReplayingDecoder

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

javadoc⽂档中的⼀个示例:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

使⽤ReplayingDecoder后:

netty架构模式详解,netty架构实战

基本原理:

  • 使⽤了特殊的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的类变量时必须⼩⼼谨慎。

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

ByteToIntegerDecoder2的实现:

netty架构模式详解,netty架构实战

2.2、什么是TCP粘包/拆包问题?

TCP是流传递的,所谓流,就是⼀串没有界限的数据,服务端接收到客户端发来的数据,并不确定这是⼀条数据,还是多条数据,应该如何拆包,服务端是不知道的。

所以,客户端与服务端就需要约定好拆包的规则,客户端按照此规则进⾏粘包,⽽服务端按照此规则进⾏拆包,这就是TCP的粘包与拆包,如果不约定好,就会出现服务端不能按照期望拿到数据。

实际上,彼此约定的规则就是协议,⾃定义协议就是⾃定义规则。

2.2.1、案例:演示TCP粘包/拆包问题

客户端:向服务端发送10条消息,并且记录服务端返回的消息的数量。

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

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

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

测试结果:

netty架构模式详解,netty架构实战

2.3、解决⽅法

⼀般来讲有3中⽅法解决TCP的粘包与拆包问题:

  • 在发送的数据包中添加头,在头⾥存储数据的⼤⼩,服务端就可以按照此⼤⼩来读取数据,这样就知道界限在哪⾥了。
  • 以固定的⻓度发送数据,超出的分多次发送,不⾜的以0填充,接收端就以固定⻓度接收即可。
  • 在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

2.4、实战:解决TCP的粘包/拆包问题

2.4.1、⾃定义协议

netty架构模式详解,netty架构实战

2.4.2、编解码器

编码器:

netty架构模式详解,netty架构实战

解码器:

netty架构模式详解,netty架构实战

2.4.3、客户端

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

2.4.4、服务端

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

2.4.5、测试

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3、⾃研RPC实战

3.1、⾃研RPC设计说明

netty架构模式详解,netty架构实战

服务端

  • 接收到客户端发来的消息后,进⾏解码操作
  • 根据消息中的接⼝信息,通过反射找到其实现类,执⾏⽬标⽅法
  • 将返回的数据再进⾏编码操作,发送给客户端

客户端

  • 客户端通过代理的⽅式,获取到接⼝的代理类
  • 通过代理类进⾏发送消息,实际上是向服务端发送消息
  • 通过编码器将消息进⾏编码操作
  • 接收到服务端的响应后,进⾏解码操作,返回数据给⽅法的调⽤⽅

注册中⼼

  • 注册中⼼并⾮课程重点,故不做实现

netty架构模式详解,netty架构实战

⼯程说明:

  • myrpc-core rpc核⼼实现
  • myrpc-demo-api 示例api定义
  • myrpc-demo-client 客户端示例
  • myrpc-demo-server 服务端示例

3.2、⾃定义协议

⾃定义协议,需要分别定义请求对象和响应对象,⽤于数据的传输。

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.3、编解码器

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

编码器:

netty架构模式详解,netty架构实战

解码器:

netty架构模式详解,netty架构实战

Hessian序列化:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.4、服务端

3.4.1、NettyServer

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.4.2、ServerInitializer

netty架构模式详解,netty架构实战

3.4.3、ServerHandler

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.4.4、ClassUtil

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.5、客户端

3.5.1、NettyClient

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.5.2、ClientInitializer

netty架构模式详解,netty架构实战

3.5.3、ClientHandler

netty架构模式详解,netty架构实战

3.5.4、RpcFutureResponse

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.5.5、ClientInvocationHandler

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.5.6、BeanFactory

netty架构模式详解,netty架构实战

3.6、示例

3.6.1、myrpc-demo-api

pom.xml:

netty架构模式详解,netty架构实战

定义pojo:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

服务接⼝定义:

netty架构模式详解,netty架构实战

3.6.2、myrpc-demo-server

pom.xml:

netty架构模式详解,netty架构实战

服务实现:

netty架构模式详解,netty架构实战

启动服务:

netty架构模式详解,netty架构实战

3.6.3、myrpc-demo-client

pom.xml:

netty架构模式详解,netty架构实战

ClientServer:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

3.7、测试

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

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中:

netty架构模式详解,netty架构实战

netty架构模式详解,netty架构实战

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:

netty架构模式详解,netty架构实战

直接使⽤channel.writeAndFlush(data);

前者会直接放⼊channel所对应的EventLoop的执⾏队列,⽽后者会导致线程的切换。

5.3、减少ChannelPipline的调⽤⻓度

netty架构模式详解,netty架构实战

前者是将msg从整个ChannelPipline中⾛⼀遍,所有的handler都要经过,⽽后者是从当前handler⼀直到pipline的尾部,调⽤更短。

5.4、减少ChannelHandler的创建

如果channelhandler是⽆状态的(即不需要保存任何状态参数),那么使⽤Sharable注解,并在bootstrap时只创建⼀个实例,减少GC。否则每次连接都会new出handler对象。

netty架构模式详解,netty架构实战

同时需要注意ByteToMessageDecoder之类的编解码器是有状态的,不能使⽤Sharable注解。

5.5、⼀些配置参数的设置

ServerBootstrap启动时,通常bossG