基于Netty实现的RPC(版本二笔记)
网络传输从BIO到NIO,序列化要减少字节流长度,提高序列化反序列化的效率
一、Netty服务端和客户端
1、服务端server
1.1 NettyServer
服务端接收客户端的RpcRquest请求,其中执行链中添加了对应的处理器,分别是编码器、拆包器、解码器、客户端处理器
- 编码器(对象 -> 字节数组 -> ByteBuf(自定义协议)):发送RpcResponse响应对象,经过CommonEncoder编码按照自定义协议编码成ByteBuf对象
- 拆包器:接收客户端请求的RpcRequest对象编码成的ByteBuf对象,按照基于固定长度域的拆包器进行拆包
- 解码器(ByteBuf -> 字节数组 -> 对象(自定义协议)):对ByteBuf对象按照自定义协议进行解码成POJO对象
- 服务端处理器:NettyServerHandler,接收客户端传送的RpcReuqest,执行客户端调用对应接口的服务方法,返回响应对象RpcResponse
/**
* Netty中处理RpcRequest的Handler
* @ClassName: NettyServerHandler
* @Author: whc
* @Date: 2021/05/29/21:49
*/
public class NettyServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);
private static RequestHandler requestHandler;
private static ServiceRegistry serviceRegistry;
static {
requestHandler = new RequestHandler();
serviceRegistry = new DefaultServiceRegistry();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) throws Exception {
try {
logger.info("服务器接收到消息:{}", msg);
String interfaceName = msg.getInterfaceName();
Object server = serviceRegistry.getService(interfaceName);
Object result = requestHandler.handle(msg, server);
// 向客户端返回响应数据
// 注意服务端处理器这里的执行链顺序,因为是ctx.writeAndFlush而不是ch.writeAndFlush,所以在执行出栈(out)时,是从当前ctx处理器从后往前找,而不是从通道最后从后往前找
ChannelFuture future = ctx.writeAndFlush(RpcResponse.success(result, msg.getRequestId()));
// 消息发送完毕关闭连接
future.addListener(ChannelFutureListener.CLOSE);
} finally {
// 记得释放对象,防止内存泄漏
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("处理过程调用时有错误发生");
cause.printStackTrace();
ctx.close();
}
}
1.2 NettyServerHandler
位于服务端责任链的尾部,用于接收RpcRequest,并且执行调用,执行真正的接口调用方法,返回处理结果。
/**
* Netty中处理RpcRequest的Handler
* @ClassName: NettyServerHandler
* @Author: whc
* @Date: 2021/05/29/21:49
*/
public class NettyServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);
private static RequestHandler requestHandler;
private static ServiceRegistry serviceRegistry;
static {
requestHandler = new RequestHandler();
serviceRegistry = new DefaultServiceRegistry();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) throws Exception {
try {
logger.info("服务器接收到消息:{}", msg);
String interfaceName = msg.getInterfaceName();
Object server = serviceRegistry.getService(interfaceName);
Object result = requestHandler.handle(msg, server);
// 向客户端返回响应数据
ChannelFuture future = ctx.writeAndFlush(RpcResponse.success(result, msg.getRequestId()));
// 消息发送完毕关闭连接
future.addListener(ChannelFutureListener.CLOSE);
} finally {
// 记得释放对象,防止内存泄漏
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("处理过程调用时有错误发生");
cause.printStackTrace();
ctx.close();
}
}
2、客户端client
2.1 NettyClient
负责执行客户端调用远程服务端服务的sendRequest请求,其中执行链中添加了对应的处理器,分别是编码器、拆包器、解码器、客户端处理器
- 编码器(对象 -> 字节数组 -> ByteBuf(自定义协议)):发送RpcRequest请求对象,经过CommonEncoder编码按照自定义协议编码成ByteBuf对象
- 拆包器:接收服务端响应回来的RpcResponse对象编码成的ByteBuf对象,然后对网络数据包按照基于固定长度域的拆包器进行拆包
- 解码器(ByteBuf -> 字节数组 -> 对象(自定义协议)):对ByteBuf对象按照自定义协议进行解码成POJO对象
- 客户端处理器:NettyClientHandler,接收服务端传送的RpcReponse,设置服务端响应的RpcResponse标识
/**
* NIO方式消费者客户端类
* @ClassName: NettyClient
* @Author: whc
* @Date: 2021/05/29/23:07
*/
public class NettyClient implements RpcClient {
private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);
private static final Bootstrap bootstrap;
private CommonSerializer serializer;
static {
EventLoopGroup group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
// 1. 指定线程模型
bootstrap.group(group)
// 2. 指定IO类型为NIO
.channel(NioSocketChannel.class)
// 开启TCP底层心跳机制
.option(ChannelOption.SO_KEEPALIVE, true);
}
private String host;
private int port;
public NettyClient(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public Object sendRequest(RpcRequest rpcRequest) {
if(serializer == null) {
logger.error("未设置序列化器");
throw new RpcException(RpcError.SERIALIZER_NOT_FOUND);
}
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 执行链: head -> CommonEncoder(out) -> Spliter -> CommonDecoder -> NettyClientHandler -> tail
// out出栈主要是对写回结果进行加工
// in入栈主要是用来读取服务端数据,写回结果
// 发送RpcRequest请求对象,经过CommonEncoder编码按照自定义协议编码成ByteBuf对象
pipeline.addLast(new CommonEncoder(serializer))
// 接收服务端响应回来的RpcResponse对象,经过Spliter,对网络数据包按照基于固定长度域的拆包器进行拆包
.addLast(new Spliter())
// 对数据包按照自定义协议进行解码成POJO对象
.addLast(new CommonDecoder())
// 客户端对解码出来的POJO对象进行调用处理
.addLast(new NettyClientHandler());
}
});
try {
ChannelFuture future = bootstrap.connect(host, port).sync();
logger.info("客户端连接到服务器 {}:{}", host, port);
Channel channel = future.channel();
if(channel != null) {
// 发送数据
channel.writeAndFlush(rpcRequest).addListener(future1 -> {
if(future1.isSuccess()) {
logger.info(String.format("客户端发送消息: %s", rpcRequest.toString()));
} else {
logger.error("发送消息时有错误发生: ", future1.cause());
}
});
// 为了让netty不会关闭
channel.closeFuture().sync();
// 通过给channel设计别名,获取特定名字下的channel中的内容(这个在hanlder中设置)
// AttributeKey是,线程隔离的,不会有线程安全问题。
AttributeKey<RpcResponse> key = AttributeKey.valueOf("rpcResponse" + rpcRequest.getRequestId());
RpcResponse rpcResponse = channel.attr(key).get();
RpcMessageChecker.check(rpcRequest, rpcResponse);
return rpcResponse.getData();
}
} catch (InterruptedException e) {
logger.error("发送消息时有错误发生: ", e);
}
return null;
}
@Override
public void setSerializer(CommonSerializer serializer) {
this.serializer = serializer;
}
}
2.2 NettyClientHandler
位于客户端责任链的尾部,用于接收RpcResponse,并且执行调用,给channel设计别名
(判断客户端接收服务端传过来的RpcResponse是否能接收成功,客户端先通过对接收到的RpcResponse在通道中设置一个标识,接着客户端在后续操作中只要从通道获取这个标识符,取出然后判断客户端传递过去的RpcRequest中的请求号是否和服务端传递过来的RpcRespons中的响应请求号相等(对应RpcRequest的请求号),则说明双方成功通信)
/**
* Netty客户端处理器
* @ClassName: NettyClientHandler
* @Author: whc
* @Date: 2021/05/30/0:06
*/
public class NettyClientHandler extends SimpleChannelInboundHandler<RpcResponse> {
private static final Logger logger = LoggerFactory.getLogger(NettyClientHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponse msg) throws Exception {
try {
logger.info(String.format("客户端接收到消息: %s", msg));
// 接收到response, 给channel设计别名,让sendRequest里读取response
AttributeKey<RpcResponse> key = AttributeKey.valueOf("rpcResponse" + msg.getRequestId());
ctx.channel().attr(key).set(msg);
ctx.channel().close();
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("过程调用时有错误发生:");
cause.printStackTrace();
ctx.close();
}
}
二、自定义协议和编解码器
1、协议
在传输过程中,在发送的数据上加上各种必要的数据,形成自定义的协议,而自动加上这个数据就是编码器的工作,解析数据获得原始数据就是解码器的工作。
自定义协议如下:
+---------------+---------------+-----------------+-------------+
| Magic Number | Package Type | Serializer Type | Data Length |
| 4 bytes | 4 bytes | 4 bytes | 4 bytes |
+---------------+---------------+-----------------+-------------+
| Data Bytes |
| Length: ${Data Length} |
+---------------------------------------------------------------+
- Magic Number : 魔数,标识一个协议包
- Package Type:标明是一个调用请求还是调用响应
- Serializer Type:标明实际数据使用的序列化器(客户端和服务端应使用统一标准)
- Data Length:实际数据的长度
2、编码器
/**
* 通信协议的设计
* 通用的编码拦截器
* 负责将 POJO 对象编码成 ByteBuf
* @ClassName: CommonEncoder
* @Author: whc
* @Date: 2021/05/29/20:48
*/
public class CommonEncoder extends MessageToByteEncoder {
private static final int MAGIC_NUMBER = 0xCAFEBABE;
private final CommonSerializer serializer;
public CommonEncoder(CommonSerializer serializer) {
this.serializer = serializer;
}
// 自定义传输协议,防止粘包
// 消息格式为: [魔数][数据包类型][序列化器类型][数据长度][数据]
// 4字节 4字节 4字节 4字节
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
// 魔数
out.writeInt(MAGIC_NUMBER);
// 数据包类型
if (msg instanceof RpcRequest) {
out.writeInt(PackageType.REQUEST_PACK.getCode());
} else {
out.writeInt(PackageType.RESPONSE_PACK.getCode());
}
// 序列化器类型
out.writeInt(serializer.getCode());
byte[] bytes = serializer.serialize(msg);
// 数据长度
out.writeInt(bytes.length);
// 数据
out.writeBytes(bytes);
}
}
3、解码器
/**
* 通用的解码拦截器
* 完成 ByteBuf 到 POJO 对象的解码
* @ClassName: CommonDecoder
* @Author: whc
* @Date: 2021/05/29/21:24
*/
public class CommonDecoder extends ReplayingDecoder {
private static final Logger logger = LoggerFactory.getLogger(CommonDecoder.class);
private static final int MAGIC_NUMBER = 0xCAFEBABE;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magic = in.readInt();
if(magic != MAGIC_NUMBER) {
logger.error("不识别的协议包:{}", magic);
throw new RpcException(RpcError.UNKNOWN_PROTOCOL);
}
int packageCode = in.readInt();
Class<?> packageClass;
if(packageCode == PackageType.REQUEST_PACK.getCode()) {
packageClass = RpcRequest.class;
} else if(packageCode == PackageType.RESPONSE_PACK.getCode()) {
packageClass = RpcResponse.class;
} else {
logger.error("不识别的数据包:{}", packageCode);
throw new RpcException(RpcError.UNKNOWN_PACKAGE_TYPE);
}
int serializerCode = in.readInt();
// 获取序列化器类型
CommonSerializer serializer = CommonSerializer.getByCode(serializerCode);
if(serializer == null) {
logger.error("不识别的反序列化器:{}", serializerCode);
throw new RpcException(RpcError.UNKNOWN_SERIALIZER);
}
// 数据长度
int length = in.readInt();
byte[] bytes = new byte[length];
// 填充数据
in.readBytes(bytes);
// 反序列化
Object obj = serializer.deserialize(bytes, packageClass);
out.add(obj);
}
}
4、拆包器
/**
* Netty 提供了 LengthFieldBasedFrameDecoder,自动屏蔽 TCP 底层的拆包和粘包问题,只需要传入正确的参数,即可轻松解决“读半包“问题。
*
* Spliter作用:
* 1. 基于固定长度域的拆包器,根据我们的自定义协议,把数据拼装成一个个符合我们自定义数据包大小的ByteBuf,接着根据我们的自定义协议解码器去解码
* 2. 拒绝非本协议连接
*
* @ClassName: Spliter
* @Author: whc
* @Date: 2021/06/04/22:16
*/
public class Spliter extends LengthFieldBasedFrameDecoder {
private static final int MAGIC_NUMBER = 0xCAFEBABE;
// 消息格式为: [魔数][数据包类型][序列化器类型][数据长度][数据]
// 4字节 4字节 4字节 4字节
private static final int LENGTH_FIELD_OFFSET = 12;
// 数据长度
private static final int LENGTH_FIELD_LENGTH = 4;
public Spliter() {
super(Integer.MAX_VALUE, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if(in.getInt(in.readerIndex()) != MAGIC_NUMBER) {
ctx.channel().closeFuture();
return null;
}
return super.decode(ctx, in);
}
}
5、补充知识
5.1 TCP粘包拆包问题
-
什么是粘包和半包?
Netty发送和读取数据的单位,可以形象的使用ByteBuf来充当。 每一次发送,就是向Channel写入一个ByteBuf;每一次读取,就是从Channel读到一个ByteBuf 例如发送一次数据:channel.writeAndFlush(buffer); 读取一次数据: ByteBuf byteBuf = (ByteBuf)msg; 理想情况下,发送端每发送一个buffer,接收端就能收到一个一模一样的buffer,但是在实际的通讯过程中,实际情况并不是如此。
-
粘包
就是接收端读取的时候,多个发送过来的 ByteBuf “粘”在了一起。
换句话说,接收端读取一次ByteBuf,读取了多个发送的ByteBuf
-
半包
就是接收端将一个发送端的ByteBuf “拆”开了,形成一个破碎的包,我们定义这种 ByteBuf 为半包。
换句话说,接收端读取一次的 ByteBuf ,读到了发送端的一个 ByteBuf的一部分,是为半包。
-
-
为什么会粘包、半包?
因为Netty底层是走的TCP协议,说白了传输的是就是字节流,消息与消息之间是没有边界的。发生TCP粘包拆包的原因主要有:
- 当连续发送数据时,由于TCP协议的nagle算法,会将较小的内容拼接成较大的包一次性发送到服务器端,因而导致粘包;
- 当发送的内容较大时,由于服务器端的recv(buffer_size)方法中buffer_size较小,不能一次性读完所有数据,从而导致一个消息分拆成多次读取,就是半包的情况。
首先,上层应用层每次读取底层缓冲的数据容量是有限制的,当TCP底层缓冲数据包比较大时,将被分成多次读取,造成断包,在应用层来说,就是半包。 其次,如果上层应用层一次读到多个底层缓冲数据包,就是粘包。
-
如何解决粘包、 半包问题?
基本思路是:在接收端,需要根据自定义协议来读取底层的数据包,重新组装我们应用层的数据包,这个过程通常在接收端称为拆包。
-
拆包的原理
-
接收端应用层不断从底层的TCP 缓冲区中读取数据。
-
每次读取完,判断一下是否为一个完整的应用层数据包。如果是,上层应用层数据包读取完成。
-
如果不是,那就保留该数据在应用层缓冲区,然后继续从 TCP 缓冲区中读取,直到得到一个完整的应用层数据包为止。
-
至此,半包问题得以解决。
-
如果从TCP底层读到了多个应用层数据包,则将整个应用层缓冲区,拆成一个一个的独立的应用层数据包,返回给调用程序。
-
至此,粘包问题得以解决。
-
-
Netty常用的拆包器解决粘包拆包问题
LengthFieldBasedFrameDecoder
:基于长度域的拆包器LineBasedFrameDecoder
:行拆包器DelimiterBasedFrameDecoder
:基于分隔符拆包器FixedLengthFrameDecoder
:定长拆包器
三、序列化接口
public interface CommonSerializer {
static CommonSerializer getByCode(int code) {
switch (code) {
case 0:
return new KryoSerializer();
case 1:
return new JsonSerializer();
case 2:
return new HessianSerializer();
default:
return null;
}
}
byte[] serialize(Object obj);
Object deserialize(byte[] bytes, Class<?> clazz);
int getCode();
}
- 序列化
- 反序列化
- 获取序列化器的编号
- 根据编号获取序列化器
1、JsonSerializer
/**
* 使用JSON格式的序列化器
* @ClassName: JsonSerializer
* @Author: whc
* @Date: 2021/05/29/20:51
*/
public class JsonSerializer implements CommonSerializer{
private static final Logger logger = LoggerFactory.getLogger(JsonSerializer.class);
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public byte[] serialize(Object obj) {
try {
// 对象转byte数组
return objectMapper.writeValueAsBytes(obj);
} catch (JsonProcessingException e) {
logger.error("序列化时有错误发生:", e);
throw new SerializeException("序列化时有错误发生");
}
}
@Override
public Object deserialize(byte[] bytes, Class<?> clazz) {
try {
// byte数组转对象
Object obj = objectMapper.readValue(bytes, clazz);
if(obj instanceof RpcRequest) {
obj = handleRequest(obj);
}
return obj;
} catch (IOException e) {
logger.error("序列化时有错误发生:", e);
throw new SerializeException("序列化时有错误发生");
}
}
/**
* 由于这里使用JSON序列化和反序列化时Object数组,无法保证反序列化时后仍然为原实例类型
* 需要重新判断处理
*/
private Object handleRequest(Object obj) throws IOException {
RpcRequest rpcRequest = (RpcRequest)obj;
for (int i = 0; i < rpcRequest.getParamTypes().length; i++) {
Class<?> clazz = rpcRequest.getParamTypes()[i];
if(!clazz.isAssignableFrom(rpcRequest.getParameters()[i].getClass())) {
byte[] bytes = objectMapper.writeValueAsBytes(rpcRequest.getParameters()[i]);
rpcRequest.getParameters()[i] = objectMapper.readValue(bytes, clazz);
}
}
return rpcRequest;
}
@Override
public int getCode() {
return SerializerCode.valueOf("JSON").getCode();
}
}
2、HessianSerializer
public class HessianSerializer implements CommonSerializer {
private static final Logger logger = LoggerFactory.getLogger(HessianSerializer.class);
@Override
public byte[] serialize(Object obj) {
HessianOutput hessianOutput = null;
try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(obj);
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
logger.error("序列化时有错误发生", e);
throw new SerializeException("序列化时有错误发生");
} finally {
if(hessianOutput != null) {
try {
hessianOutput.close();
} catch (IOException e) {
logger.error("关闭流时有错误发生:", e);
}
}
}
}
@Override
public Object deserialize(byte[] bytes, Class<?> clazz) {
HessianInput hessianInput = null;
try(ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
hessianInput = new HessianInput(byteArrayInputStream);
return hessianInput.readObject();
} catch (IOException e) {
logger.error("序列化时有错误发生:", e);
throw new SerializeException("序列化时有错误发生");
} finally {
if(hessianInput != null) {
hessianInput.close();
}
}
}
@Override
public int getCode() {
return SerializerCode.valueOf("HESSIAN").getCode();
}
}
3、KryoSerializer
public class KryoSerializer implements CommonSerializer {
private static final Logger logger = LoggerFactory.getLogger(KryoSerializer.class);
private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
kryo.register(RpcResponse.class);
kryo.register(RpcRequest.class);
kryo.setReferences(true);
kryo.setRegistrationRequired(false);
return kryo;
});
@Override
public byte[] serialize(Object obj) {
try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
Output output = new Output(byteArrayOutputStream);
Kryo kryo = kryoThreadLocal.get();
kryo.writeObject(output, obj);
kryoThreadLocal.remove();
return output.toBytes();
} catch (Exception e) {
logger.error("序列化时有错误发生:", e);
throw new SerializeException("序列化时有错误发生");
}
}
@Override
public Object deserialize(byte[] bytes, Class<?> clazz) {
try(ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
Input input = new Input(byteArrayInputStream);
Kryo kryo = kryoThreadLocal.get();
Object o = kryo.readObject(input, clazz);
kryoThreadLocal.remove();
return o;
} catch (Exception e) {
logger.error("序列化时有错误发生:", e);
throw new SerializeException("序列化时有错误发生");
}
}
@Override
public int getCode() {
return SerializerCode.valueOf("KRYO").getCode();
}
}
四、测试结果
服务端
客户端
五、版本二特点
优点:
- 使用Netty实现了客户端与服务端的通信
- 自定义了消息格式,使之支持多种消息类型、序列化方式(Json、Hessian、Kryo)
- 使用Netty的拆包器解决了粘包问题
缺点:
- 服务端和客户端通信的host和port预先就必须知道,每一个客户端都必须知道对应的ip和端口号,并且如果服务挂了或者地址换了,就很麻烦。
待解决:
- 服务器注册与发现的实现用zookeeper作为注册中心
- 负载均衡的策略的实现