guide-rpc-framework 讲解 流程梳理
开源项目 https://github.com/Snailclimb/guide-rpc-framework
在学习中记录下来的大致流程梳理,没有说明类名,可以全局搜索进行定位。
代码部分很多截取内容,建议先熟悉下代码,也可复制全局搜索定位,不然可能感觉有些乱。
也可以先看看了解个大概~
NettyServerMain
通过@Component 注解 拿到nettyRpcServer
NettyRpcServer nettyRpcServer = (NettyRpcServer) applicationContext.getBean("nettyRpcServer");
手动注册HelloService
HelloService helloService2 = new HelloServiceImpl2();
// 注册到zookeeper
RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
.group("test2").version("version2").service(helloService2).build();
public class RpcServiceConfig {
/**
* service version
*/
private String version = "";
/**
* when the interface has multiple implementation classes, distinguish by group
*/
private String group = "";
/**
* target service
*/
private Object service;
public String getRpcServiceName() {
return this.getServiceName() + this.getGroup() + this.getVersion();
}
// 实现的第一个接口的全限定名
public String getServiceName() {
return this.service.getClass().getInterfaces()[0].getCanonicalName();
}
}
//注册
nettyRpcServer.registerService(rpcServiceConfig);
// 1 会将 service 放进一个map
serviceMap.put(rpcServiceName, rpcServiceConfig.getService());
// 2 放入zookeeper 会先获取当前服务提供方的IP地址以及端口(固定9998)组成InetSocketAddress,
new InetSocketAddress(host, NettyRpcServer.PORT)
// 以注册的HelloService实现的接口名+组名+版本号 + ip port 为 zk 的节点路径,
// 如 /my-rpc/github.javaguide.HelloServicetest1version1/20.20.20.231:9998
public static final String ZK_REGISTER_ROOT_PATH = "/my-rpc";
String servicePath = CuratorUtils.ZK_REGISTER_ROOT_PATH + "/" + rpcServiceName + inetSocketAddress.toString();
//拿到 servicePath 就会以此为path创建节点
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path);
//zk有四种节点,这里创建的持久节点
持久(persistent)节点:
(1) session断开后,数据不会丢失
(2)可以创建子节点
临时(ephemeral)节点:
(1)session断开后,数据会丢失
(2)不可以创建子节点
持久顺序(PERSISTENT_SEQUENTIAL)节点:同持久节点
创建顺序节点时会默认设置顺序标识,即znode名称后会附加一个 顺序号 ,顺序号是一个单调递增的计数器,由父节点维护
临时顺序(EPHEMERAL_SEQUENTIAL)节点: —> ZK实现分布式锁的基础。
(1)、(2)同临时节点,(3)同持久顺序节点 临时是为了防止一个锁无法被删除,顺序:只要自己的时顺序是最小的说明拿到了锁。
// zk 的三种监听
NodeCache:只是监听某一个特定的节点
PathChildrenCache:监控一个ZNode的子节点.
TreeCache:可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合
服务端启动
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
DefaultEventExecutorGroup serviceHandlerGroup = new DefaultEventExecutorGroup(
RuntimeUtil.cpus() * 2,
ThreadPoolFactoryUtil.createThreadFactory("service-handler-group", false)
);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
// TCP默认开启了 Nagle 算法,该算法的作用是尽可能的发送大数据快,减少网络传输。TCP_NODELAY 参数的作用就是控制是否启用 Nagle 算法。
.childOption(ChannelOption.TCP_NODELAY, true)
// 是否开启 TCP 底层心跳机制
.childOption(ChannelOption.SO_KEEPALIVE, true)
//表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数
.option(ChannelOption.SO_BACKLOG, 128)
.handler(new LoggingHandler(LogLevel.INFO))
// 当客户端第一次进行请求的时候才会进行初始化
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 30 秒之内没有收到客户端请求的话就关闭连接
ChannelPipeline p = ch.pipeline();
// 在 Netty 中,IdleStateHandler 是一个用于检测连接空闲状态的处理器。当通道在指定时间内没有任何读、写或读写操作时,它会触发相应的事件。
p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
p.addLast(new RpcMessageEncoder());
p.addLast(new RpcMessageDecoder());
p.addLast(serviceHandlerGroup, new NettyRpcServerHandler());
}
});
// 绑定端口,同步等待绑定成功
ChannelFuture f = b.bind(host, PORT).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
流水线
通信协议:
* 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
* +-----+-----+-----+-----+--------+----+----+----+------+-----------+-------+----- --+-----+-----+-------+
* | magic code |version | full length | messageType| codec|compress| RequestId |
* +-----------------------+--------+---------------------+-----------+-----------+-----------+------------+
* | |
* | body |
* | |
* | ... ... |
* +-------------------------------------------------------------------------------------------------------+
* 4B magic code(魔法数) 1B version(版本) 4B full length(消息长度) 1B messageType(消息类型)
* 1B compress(压缩类型) 1B codec(序列化类型) 4B requestId(请求的Id)
* body(object类型数据)
Encoder
public class RpcMessageEncoder extends MessageToByteEncoder<RpcMessage> {
private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);
@Override
protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out) {
try {
// 魔法数 用于识别该消息是否是该接受的
// public static final byte[] MAGIC_NUMBER = {(byte) 'g', (byte) 'r', (byte) 'p', (byte) 'c'};
out.writeBytes(RpcConstants.MAGIC_NUMBER);
// 协议版本
out.writeByte(RpcConstants.VERSION);
// 留出4字节的消息长度字段
out.writerIndex(out.writerIndex() + 4);
// 消息类型 请求/心跳/
byte messageType = rpcMessage.getMessageType();
out.writeByte(messageType);
// 序列化类型
out.writeByte(rpcMessage.getCodec());
// 压缩类型
out.writeByte(CompressTypeEnum.GZIP.getCode());
// requestId 用了原子自增
out.writeInt(ATOMIC_INTEGER.getAndIncrement());
// build full length
// 构造消息体
byte[] bodyBytes = null;
// 常量16字节
int fullLength = RpcConstants.HEAD_LENGTH;
// if messageType is not heartbeat message,fullLength = head length + body length
// 如果不是心跳检测
if (messageType != RpcConstants.HEARTBEAT_REQUEST_TYPE
&& messageType != RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
// serialize the object 序列化
String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec());
log.info("codec name: [{}] ", codecName);
// 加载序列化器
Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
.getExtension(codecName);
bodyBytes = serializer.serialize(rpcMessage.getData());
// compress the bytes 压缩
String compressName = CompressTypeEnum.getName(rpcMessage.getCompress());
Compress compress = ExtensionLoader.getExtensionLoader(Compress.class)
.getExtension(compressName);
bodyBytes = compress.compress(bodyBytes);
// 总长度 = 头+体
fullLength += bodyBytes.length;
}
if (bodyBytes != null) {
// 写入消息
out.writeBytes(bodyBytes);
}
int writeIndex = out.writerIndex();
// 定位到消息协议长度字段 写入长度
out.writerIndex(writeIndex - fullLength + RpcConstants.MAGIC_NUMBER.length + 1);
out.writeInt(fullLength);
// 还原index
out.writerIndex(writeIndex);
} catch (Exception e) {
log.error("Encode request error!", e);
}
}
}
Decoder
LengthFieldBasedFrameDecoder
是 Netty 提供的一个解码器,用于处理基于长度字段的帧。它通过在消息中指定长度字段,来确定每个消息的边界,以解决 TCP 粘包和拆包问题。
public class RpcMessageDecoder extends LengthFieldBasedFrameDecoder {
public RpcMessageDecoder() {
// lengthFieldOffset: magic code is 4B, and version is 1B, and then full length. so value is 5
// lengthFieldLength: full length is 4B. so value is 4
// lengthAdjustment: full length include all data and read 9 bytes before, so the left length is (fullLength-9). so values is -9
// initialBytesToStrip: we will check magic code and version manually, so do not strip any bytes. so values is 0
this(RpcConstants.MAX_FRAME_LENGTH, 5, 4, -9, 0);
/**
* @param maxFrameLength 最大长度,如果超过则丢弃
* @param lengthFieldOffset 长度字段在那个位置
* @param lengthFieldLength 长度字段的长度
* @param lengthAdjustment 由于长度字段包括了消息头和消息体,假设长度字段为N,长度字段在9字节,还剩下N-9个字节要读取。
* @param initialBytesToStrip Number of bytes skipped.
* If you need to receive all of the header+body data, this value is 0
* if you only want to receive the body data, then you need to skip the number of bytes consumed by the header.
*/
}
然后根据协议定义的每个字段几个字节,一个一个字段的读取。 然后解压缩,反序列化。
private Object decodeFrame(ByteBuf in) {
// note: must read ByteBuf in order
checkMagicNumber(in);
checkVersion(in);
int fullLength = in.readInt();
// build RpcMessage object
byte messageType = in.readByte();
byte codecType = in.readByte();
byte compressType = in.readByte();
int requestId = in.readInt();
RpcMessage rpcMessage = RpcMessage.builder()
.codec(codecType)
.requestId(requestId)
.messageType(messageType).build();
if (messageType == RpcConstants.HEARTBEAT_REQUEST_TYPE) {
rpcMessage.setData(RpcConstants.PING);
return rpcMessage;
}
if (messageType == RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
rpcMessage.setData(RpcConstants.PONG);
return rpcMessage;
}
int bodyLength = fullLength - RpcConstants.HEAD_LENGTH;
if (bodyLength > 0) {
byte[] bs = new byte[bodyLength];
in.readBytes(bs);
// decompress the bytes
String compressName = CompressTypeEnum.getName(compressType);
Compress compress = ExtensionLoader.getExtensionLoader(Compress.class)
.getExtension(compressName);
bs = compress.decompress(bs);
// deserialize the object
String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec());
log.info("codec name: [{}] ", codecName);
Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
.getExtension(codecName);
if (messageType == RpcConstants.REQUEST_TYPE) {
RpcRequest tmpValue = serializer.deserialize(bs, RpcRequest.class);
rpcMessage.setData(tmpValue);
} else {
RpcResponse tmpValue = serializer.deserialize(bs, RpcResponse.class);
rpcMessage.setData(tmpValue);
}
}
return rpcMessage;
}
NettyRpcServerHandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
if (msg instanceof RpcMessage) {
log.info("server receive msg: [{}] ", msg);
byte messageType = ((RpcMessage) msg).getMessageType();
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setCodec(SerializationTypeEnum.HESSIAN.getCode());
rpcMessage.setCompress(CompressTypeEnum.GZIP.getCode());
// 心跳检测
if (messageType == RpcConstants.HEARTBEAT_REQUEST_TYPE) {
rpcMessage.setMessageType(RpcConstants.HEARTBEAT_RESPONSE_TYPE);
// 返回一个pong
rpcMessage.setData(RpcConstants.PONG);
} else {
// 转换类
RpcRequest rpcRequest = (RpcRequest) ((RpcMessage) msg).getData();
// rpcRequest.getRpcServiceName() 通过serviceName 去map里拿到对应的类,再根据方法名和参数类型确定方法反射执行
Object result = rpcRequestHandler.handle(rpcRequest);
log.info(String.format("server get result: %s", result.toString()));
// 类型为 返回
rpcMessage.setMessageType(RpcConstants.RESPONSE_TYPE);
if (ctx.channel().isActive() && ctx.channel().isWritable()) {
RpcResponse<Object> rpcResponse = RpcResponse.success(result, rpcRequest.getRequestId());
rpcMessage.setData(rpcResponse);
} else {
RpcResponse<Object> rpcResponse = RpcResponse.fail(RpcResponseCodeEnum.FAIL);
rpcMessage.setData(rpcResponse);
log.error("not writable now, message dropped");
}
}
// 如果在写入数据的过程中出现错误,Netty 会关闭连接
ctx.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
} finally {
//Ensure that ByteBuf is released, otherwise there may be memory leaks
ReferenceCountUtil.release(msg);
}
}
NettyClientMain
客户端调用HelloController 的test方法
然后调用了helloService.hello 方法,而HelloService由与服务端提供
@Component
public class HelloController {
@RpcReference(version = "version1", group = "test1")
private HelloService helloService;
public void test() throws InterruptedException {
String hello = this.helloService.hello(new Hello("111", "222"));
//如需使用 assert 断言,需要在 VM options 添加参数:-ea
assert "Hello description is 222".equals(hello);
Thread.sleep(12000);
for (int i = 0; i < 10; i++) {
System.out.println(helloService.hello(new Hello("111", "222")));
}
}
}
在这个bean加载的时候,就将helloService 替换为了代理对象,那么执行hello方法,是由代理对象来完成。
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> targetClass = bean.getClass();
Field[] declaredFields = targetClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
RpcReference rpcReference = declaredField.getAnnotation(RpcReference.class);
if (rpcReference != null) {
RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
.group(rpcReference.group())
.version(rpcReference.version()).build();
// rpcClient = NettyRpcClient rpcServiceConfig = 注解指定的service 结构在上文已经提过
RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient, rpcServiceConfig);
Object clientProxy = rpcClientProxy.getProxy(declaredField.getType());
declaredField.setAccessible(true);
try {
// 将
// @RpcReference(version = "version1", group = "test1")
// private HelloService helloService;
// 替换为代理对象
declaredField.set(bean, clientProxy);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return bean;
}
代理对象
代理对象的invoke 方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
log.info("invoked method: [{}]", method.getName());
// 构造RpcRequest 类结构在下一个代码块
RpcRequest rpcRequest = RpcRequest.builder().methodName(method.getName())
.parameters(args)
.interfaceName(method.getDeclaringClass().getName()) // 类名 github.javaguide.HelloService
.paramTypes(method.getParameterTypes())
.requestId(UUID.randomUUID().toString())
// rpcServiceConfig 在上一块代码中 初始化代理对象时传入
.group(rpcServiceConfig.getGroup())
.version(rpcServiceConfig.getVersion())
.build();
RpcResponse<Object> rpcResponse = null;
// 基于netty
if (rpcRequestTransport instanceof NettyRpcClient) {
// 发送request请求
CompletableFuture<RpcResponse<Object>> completableFuture = (CompletableFuture<RpcResponse<Object>>) rpcRequestTransport.sendRpcRequest(rpcRequest);
rpcResponse = completableFuture.get();
}
// 基于socket
if (rpcRequestTransport instanceof SocketRpcClient) {
rpcResponse = (RpcResponse<Object>) rpcRequestTransport.sendRpcRequest(rpcRequest);
}
this.check(rpcResponse, rpcRequest);
return rpcResponse.getData();
}
public class RpcRequest implements Serializable {
private static final long serialVersionUID = 1905122041950251207L;
private String requestId;
private String interfaceName;
private String methodName;
private Object[] parameters;
private Class<?>[] paramTypes;
private String version;
private String group;
public String getRpcServiceName() {
return this.getInterfaceName() + this.getGroup() + this.getVersion();
}
}
sendRpcRequest
@Override
public Object sendRpcRequest(RpcRequest rpcRequest) {
// build return value
CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
// 发现服务
/**
1. lookupService 中使用到了负载均衡算法,我们首先需要再次回顾一下服务注册时的zk节点结构
是以注册的HelloService实现的接口名+组名+版本号 + ip port为zk的路径,如/my-rpc/github.javaguide.HelloServicetest1version1/20.20.20.231:9998
2. 通过 rpcRequest.getRpcServiceName 得到接口名+组名+版本号 然后拿到这个节点的所有子节点 如上举例的就是20.20.20.231:9998,如果注册了更多,就会拿到多个ip + port,也就对应了多个服务提供方,这样的话就由负载均衡算法选择一个来提供服务,负载均衡算法这里就不说了。
**/
InetSocketAddress inetSocketAddress = serviceDiscovery.lookupService(rpcRequest);
// 拿到对应服务方的channel 如果没有的话 就会执行初始的连接操作。
Channel channel = getChannel(inetSocketAddress);
if (channel.isActive()) {
// put unprocessed request
unprocessedRequests.put(rpcRequest.getRequestId(), resultFuture);
RpcMessage rpcMessage = RpcMessage.builder().data(rpcRequest)
.codec(SerializationTypeEnum.HESSIAN.getCode())
.compress(CompressTypeEnum.GZIP.getCode())
.messageType(RpcConstants.REQUEST_TYPE).build();
// 将 rpcMessage 写入到与服务端的连接中,并触发刷新操作,确保数据立即发送到服务端
channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
log.info("client send message: [{}]", rpcMessage);
} else {
future.channel().close();
resultFuture.completeExceptionally(future.cause());
log.error("Send failed:", future.cause());
}
});
} else {
throw new IllegalStateException();
}
return resultFuture;
}
tips:
该项目还有Extension去加载一些类,大部分在类初始化的时候会触发。举个例子。
在服务注册的时候调用了nettyRpcServer.registerService(rpcServiceConfig);
public class NettyRpcServer {
public static final int PORT = 9998;
// 单例获取实例 通过反射调用 ZkServiceProviderImpl 的构造器 初始化实例
private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
public void registerService(RpcServiceConfig rpcServiceConfig) {
serviceProvider.publishService(rpcServiceConfig);
}
}
registerService
调用了 serviceProvider
的publishService
方法,至于serviceProvider
用到双重校验锁来实现单例 这里就不说了。
关键在于ZkServiceProviderImpl
在构造器 初始化实例时加载了服务注册类 serviceRegistry
public class ZkServiceProviderImpl implements ServiceProvider {
/**
* key: rpc service name(interface name + version + group)
* value: service object
*/
// 实现类 ZkServiceRegistryImpl
private final ServiceRegistry serviceRegistry;
public ZkServiceProviderImpl() {
serviceRegistry = ExtensionLoader.getExtensionLoader(ServiceRegistry.class).getExtension(ServiceRegistryEnum.ZK.getName());
// 实际就是 serviceRegistry = ZkServiceRegistryImpl
}
getExtensionLoader 会返回一个 ExtensionLoader 类
public final class ExtensionLoader<T> {
private final Class<?> type;
}
主要是指定type
其中的 type属性就是 传入的参数 ServiceRegistry.class
也就是 github.javaguide.registry.ServiceRegistry
需要注意这个在 src/main/resources/META-INF/extensions
下有同名的文件
然后 调用这个返回的ExtensionLoader.getExtension
方法,参数为常量 "zk"
你去看下那个同名文件就知道为什么了~
// name = zk netty socket
public T getExtension(String name) {
if (StringUtil.isBlank(name)) {
throw new IllegalArgumentException("Extension name should not be null or empty.");
}
// firstly get from cache, if not hit, create one
// 实例缓存 Map<String, Holder<Object>> cachedInstances
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);// 一个空的Holder 类,holder 只有一个属性 value 用于存放实例对象
}
// create a singleton if no instance exists
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
private T createExtension(String name) {
// load all extension classes of type T from file and get specific one by name
// getExtensionClasses() 中返回了 Map "zk" -> "class github.javaguide.registry.zk.ZkServiceRegistryImpl" 这也是上文中提到了resource文件中的类,那个文件指定了要加载哪些类 然后存在 cachedClasses
Class<?> clazz = getExtensionClasses().get(name);
// clazz = github.javaguide.registry.zk.ZkServiceRegistryImpl
if (clazz == null) {
throw new RuntimeException("No such extension of name " + name);
}
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
try {
// 反射实例化
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
// github.javaguide.registry.zk.ZkServiceRegistryImpl -> 实例
instance = (T) EXTENSION_INSTANCES.get(clazz);
} catch (Exception e) {
log.error(e.getMessage());
}
}
return instance;
}
private Map<String, Class<?>> getExtensionClasses() {
// get the loaded extension class from the cache
// zk -> "class github.javaguide.registry.zk.ZkServiceRegistryImpl"
Map<String, Class<?>> classes = cachedClasses.get();
// double check
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = new HashMap<>();
// load all extensions from our extensions directory
loadDirectory(classes);
// class 缓存
cachedClasses.set(classes);
}
}
}
return classes;
}
本文作者:Shie1d
本文链接:https://www.cnblogs.com/ganyq/p/18347876
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步