- 前言 项目使用Java开发,实现类似dubbo的框架 纯属个人兴趣实现,如果您觉得项目框架存在问题,或者实现有问题,欢迎您告诉我,可以加我QQ或者github上提交issue,同时我自己认为有问题的地方已经写在Readme中,谢谢!!!
QQ号:1758619238
项目地址:github.com/orgs/Hongko…
准备工作——技术选用和参考
代理(Proxy)
客户端调用接口时需要用到代理
- Java动态代理(InvocationHandler)
- CGLIB
- Javassist
序列化
客户端与服务端数据传输时需要使用序列化
- Java序列化(Serializable)
- ProtoBuf(google提供的高效议数据交换格式工具库)
- JSON(Jackson、fastJSON等)
- kyro(二进制字节数组)
消费者和提供者交互
- Netty(dubbo)
- Http(Spring cloud)
注册中心
- zookeeper
- nacos
- eureke(不推荐使用)
服务提供者和消费者配置
- xml(自定义xsd文件,xml解析类,一般来说配置信息不多,使用DOM解析即可)
- 注解(自定义扫描类或者整合Spring,使用Spring Context)
项目实现
dubbo包括Registry、Provider、Consumer和Monitor。目前只实现Registry、Provider、Consumer,也没有实现Spring结合的部分。同时注册中心只支持Zookeeper,其他没有实现。
服务注册
实现 ServiceRegistry 接口,首先扫描包文件获取需要注册的服务,我是自定义实现的工具类(可能存在问题,如果方便还是使用Spring的上下文获取简单而且不易出错),将服务转换为路径保存在注册中心,路径格式 casio/serviceName/provider/hostname,注册时选择 CreateMode.EPHEMERAL(临时型节点),这样服务宕机了,注册中心保存的节点会删除。
@SPI("zk")
public interface ServiceRegistry {
void register(ServiceConfig serviceConfig) throws Exception;
}
服务发现
实现 ServiceDiscovery 接口,首先将要发现的服务转换成对应的路径(dubbo中包装为URL类实现,我在项目中简化了,只是一个String),通过注册中心找到服务的提供者主机名(ip+port),使用负载均衡选取其中一个主机名,连接服务提供者的NettyServer。消费者调用时需要注意几点,首先不要注册多个bootstrap,连接不同ip时会生成不同的 Channel,消息的发现也是通过 Channel 实现,同时数据发送给提供者后需要等待提供者处理完之后返回,因此需要 Future 模式来获取数据,这里使用 CompletableFuture 类处理。
@SPI("zk")
public interface ServiceDiscovery {
// 服务发现接口
InetSocketAddress lookup(RpcRequest rpcRequest);
}
协议
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| BYTE | BYTE | BYTE | int | ........ | |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| magic | version| type | content length | content byte[] |
+--------+--------------------------------------------+--------+--------+--------+--------+--------+
协议通过RpcMessage类包装,magic等静态变量放在ProtocolConstants类中
@Data
public class RpcMessage implements Serializable {
private byte type; //类型:请求还是响应
private byte version; // 版本
private byte[] content; // 具体传递的内容
}
一次RPC调用需要请求和返回两个实体类,分别使用RpcRequest和RpcResponse表示,也是RpcMessage的content属性部分。
将请求和响应包装成协议的格式进行传输和解析,需要继承Netty的 LengthFieldBasedFrameDecoder 类将数据包装成 RpcMessage 类,通过 MessageToByteEncoder 将数据解析,同时判断接收的数据是否存在丢失。
// 实体包装成协议
protected void encode(ChannelHandlerContext channelHandlerContext, RpcMessage rpcMessage, ByteBuf byteBuf) {
byteBuf.writeByte(ProtocolConstants.MAGIC);
byteBuf.writeByte(ProtocolConstants.VERSION);
byteBuf.writeByte(rpcMessage.getType());
byteBuf.writeInt(rpcMessage.getContent().length);
byteBuf.writeBytes(rpcMessage.getContent());
}
// 读取将二进制转换为RpcMessage实体类
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
if (in.readableBytes() < ProtocolConstants.MIN_LENGTH) {
log.error("数据解码有误,长度小于" + ProtocolConstants.MIN_LENGTH);
return null;
}
int i;
// 判断魔数是否正确
while (true) {
i = in.readerIndex();
in.markReaderIndex();
if (in.readByte() == ProtocolConstants.MAGIC) {
break;
}
in.resetReaderIndex();
in.readByte();
if (in.readableBytes() < ProtocolConstants.MIN_LENGTH) return null;
}
byte version = in.readByte();
byte type = in.readByte();
int length = in.readInt();
// 判断包长度是否完整,否则还原指针位置
if (in.readableBytes() < length) {
in.readerIndex(i);
return null;
}
byte[] data = new byte[length];
in.readBytes(data);
RpcMessage message = new RpcMessage();
message.setVersion(version);
message.setType(type);
message.setContent(data);
return message;
}
SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,主要用于查找对应的服务。Java的SPI机制默认放在Resource的 META-INF/services/ 文件夹下,文件名为接口名,内容为具体实现类,一行一个实现类,由于Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,无法指定实例化其中一个实现类。
因此dubbo自己实现了SPI的机制,内容变为name=具体实现类名的格式,和@SPI。通过@SPI的value值选择一个,调用getDefaultExtension时只实例化指定的实现类。
下面是读取 META-INF 的文件,加载到缓存中,参考dubbo,相对源码做了许多简化。
private Map<String, Class<?>> loadExtensionClasses() throws ExtensionException {
SPI spi = type.getAnnotation(SPI.class);
if (spi != null) {
String value = spi.value();
if (StringUtils.isNotBlank(value)) {
String[] names = value.split(NAME_SEPARATOR);
if (names.length == 1) {
defaultName = names[0];
} else {
throw new ExtensionException("type:" + type.getName() + " @SPI value is error");
}
}
}
Map<String, Class<?>> extensionClasses = new HashMap<>();
loadDirectory(extensionClasses, CASIO_PATH);
loadDirectory(extensionClasses, SERVICES_PATH);
return extensionClasses;
}
// 加载 resource/META-INF指定文件夹下的文件信息
private void loadDirectory(Map<String, Class<?>> extensionClasses, String path) {
String fileName = path + type.getName();
ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
Enumeration<URL> resources;
try {
if (classLoader == null) {
resources = ClassLoader.getSystemResources(fileName);
} else {
resources = classLoader.getResources(fileName);
}
if (resources != null) {
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
loadResource(extensionClasses, url);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void loadResource(Map<String, Class<?>> extensionClasses, URL url) {
try (
BufferedReader reader =
new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))
) {
String line;
while ((line = reader.readLine()) != null) {
String[] ss = line.split(NAME_SEPARATOR);
if (ss.length == 2) {
String name = ss[0];
String className = ss[1];
//
Class<?> clazz = Class.forName(className);
extensionClasses.putIfAbsent(name, clazz);
}
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
获取指定name的实现类
public T getDefaultExtension() throws ExtensionException {
getExtensionClasses();
return getExtension(defaultName);
}
@SuppressWarnings("unchecked")
public T getExtension(String name) throws ExtensionException {
if (StringUtils.isBlank(name)) {
throw new ExtensionException("extension name is null");
}
if ("true".equals(name)) {
// dubbo中getDefaultExtension()
throw new ExtensionException("extension name is true");
}
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
@SuppressWarnings("unchecked")
public T createExtension(String name) throws ExtensionException {
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw new ExtensionException("name: " + name + " has no class");
}
try {
Object instance = EXTENSION_INSTANCE_MAP.get(clazz);
if (instance == null) {
EXTENSION_INSTANCE_MAP.putIfAbsent(clazz, clazz.newInstance());
instance = EXTENSION_INSTANCE_MAP.get(clazz);
}
return (T) instance;
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
参考
参考阿里文章:zhuanlan.zhihu.com/p/388848964
dubbo项目:github.com/apache/dubb…
几位大佬自己实现的RPC框架(相比我的完善许多,给我提供了许多思路):github.com/Snailclimb/…
最后,希望大家能够star一下,如果有兴趣差不多可以一起写项目,加入Hongkong-Reporters这个组,谢谢!!!