完工项目地址
RPC远程过程调用
rpc全称Remote Procedure Call
, 是指服务端程序在A机器, 而B机器上的程序想调用服务端程序的方法, 就只能通过网络传输去调用, 比如使用springboot去启动一个web项目, 使用@PostMapping, 这样其它客户端就可以通过http请求的方式调用到这个方法, 但是这样做实在是太麻烦了, 如果有一种方式能够实现像调本地方法那样直接去调用服务端程序的方法, 那既不会麻烦, 调用的方式也很有可读性. 比如如下这样
仅仅导入了服务端的门面接口, 再通过一个方法获取到一个实现接口的实例, 再调用接口中的方法, 就能调用到服务端程序的方法实现, 并得到服务端程序给出的返回
.
很明显, 这就是dubbo的做法, 下面我们就仿照dubbo的实现框架, 自己手撕一个rpc框架出来.
RPC核心组件
注册中心
一个RPC调用分为生产者和消费者, 生产者负责提供服务, 消费者负责调用服务
, 那么消费者如何获取服务呢? 只能通过注册中心来获取
. 生产者暴露服务的时候, 给每个服务制造一些标志性的字段, 如dubbo中的group, version, 注册到注册中心中, 然后服务端和客户端双方通过一些人为的交流, 统一这些标志性的字段名, 字段值, 这样客户端就可以通过这些标志性的字段在注册中心中找到对应的服务端接口, 进而进行调用.
因此, RPC的第一核心就是注册中心
.
服务器
RPC调用得满足在不同的程序中进行通信, 那么实现的唯一方式就是生产者端绑定端口, 消费者端通过注册中心拿到的服务信息, 得到生产者的ip, 端口号, 然后才能通过网络传输调用到真正生产者端提供的方法实现.
因此, RPC的第二核心就是服务器.
协议
消费者端通过网络传输去调用服务端逻辑, 可是网络通讯传输的都是字节, 生产者端即便是接收到了这些字节数据, 也无法知道该如何解析这些字节, 因此生产者端和消费者端需要统一一个消息发送的格式, 也就是RPC通讯的协议, 这个协议可以是http, 但是也可以自定义一个协议, 比如dubbo就是自定义了一个dubbo协议, 我们这里的自实现将会实现一个http协议和一个自定义的"dark"协议.
因此, RPC的第三核心就是通讯双方的协议
.
开始手撕
生产者
要手撕生产者, 首先得知道生产者在启动的时候需要做什么事情.
1. 生产者需要实现接口的逻辑
.
2. 生产者启动需要启动一个服务器
.
3. 生产者需要将实现的这个服务注册到注册中心
.
4. 生产者/消费者通信时, 生产者需要根据协议去解析消费者传递过来的二进制字节流, 得到本次请求的具体服务实现, 然后调用服务
.
在rpc框架中, 不需要做第一点, 但是第二, 三, 四点是必须在框架中实现的, 因此下面我们将会实现这三个功能, 这三个功能实现之后, 服务就可以暴露出去了.
启动服务器
说到启动服务器, 问题就来了, 启动什么服务器? 这就要看用什么通讯协议了, 假如说用http协议, 那么就直接使用tomcat就可以了, 如果说使用其它的自定义协议, 那么支持最好的当然就是netty了. 本demo中将会实现一个http协议的服务器和一个自定义"dark"协议的服务器, 因此启动生产者的时候, 就要指定协议
, 这样我们的服务端就可以根据用户的需求去创建不同的服务器了.
因此创建一个协议模块, 构造一个Protocol接口. 该接口具有一个根据传递过来的端口启动对应的服务器的方法.
protocol接口:
public interface Protocol {
void startServer(Integer port);
}
但是第二个问题随之也来了, 上面说协议通过用户自己指定, 那么就是一个变量了, 一般我们就用if...else...去做判断, 但是如果说协议增多的时候呢? 写十几个if...else...吗? 当然不行了, 因此这里我借用了dubbo的spi实现, 将协议抽象为一个Protocol接口, 不同的协议对应Protocol的不同实现类, 接口提供startServer方法, 在不同的实现中去启动不同的服务器
, 这样代码就不会充斥着if...else...了, 大概就长这样:
// 启动服务器
Protocol protocolServer = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(protocol);
// 开启线程启动服务器, 协议为http就启动tomcat, 协议为dark就启动netty
new Thread(() -> protocolServer.startServer(protocolPort)).start();
port++;
在使用dubbo的spi的时候, 需要给接口加一个@SPI注解, 然后再在META-INF/dubbo/internal文件夹中增加一个文件, 文件名为接口的全限定名, 文件内容使用key=value的形式, key就是键, value就是接口对应的实现类
, 这样就能够做到通过传递不同的key得到不同的接口实现类, 而不用再去if...else...new...去创建了. 具体dubbo的SPI用法未来可能我会去专门写一个学习笔记, 这里介绍到这可以了, 下面粘贴代码:
协议接口
@SPI
public interface Protocol {
void startServer(Integer port);
}
spi文件
dubbo-spi依赖:
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
<version>2.7.9</version>
</dependency>
文件位置:
文件内容:
http=com.darkness.rpc.framework.protocol.http.HttpProtocol
dark=com.darkness.rpc.framework.protocol.dark.DarkProtocol
协议实现类
http:
public class HttpProtocol implements Protocol {
@Override
public void startServer(Integer port) {
Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(port);
Engine engine = new StandardEngine();
engine.setDefaultHost("localhost");
Host host = new StandardHost();
host.setName("localhost");
String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet());
context.addServletMappingDecoded("/*", "dispatcher");
try {
tomcat.start();
tomcat.getServer().await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
dark:
public class DarkProtocol implements Protocol {
@Override
public void startServer(Integer port) {
NioEventLoopGroup parent = new NioEventLoopGroup(1);
NioEventLoopGroup children = new NioEventLoopGroup(8);
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(parent, children);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
socketChannel.pipeline()
.addLast(new DarkProtocolEncoder())
.addLast(new DarkProtocolDecoder())
.addLast(new DarkProtocolHandler());
}
});
System.out.println("netty server start on " + port);
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
parent.shutdownGracefully();
children.shutdownGracefully();
}
}
}
注册中心注册服务
服务发现注册中心
新建一个registry模块, 这个模块负责注册服务到注册中心. 由于注册中心也是一个有多种可能的实现, 比如使用nacos, zk等等, 这里我们简单使用一个文件注册, 当然为了拓展, 注册中心的核心注册功能应该被抽象出来, 因此定义一个Register接口, 内部提供register方法和getService方法.
先来思考register方法需要传递哪些参数, 回想dubbo的使用, 我们使用@DubboService的时候, 用于区分服务都是用group, interface和version
, 其中group和version是由dubbo自己定义的标识性字段, 考虑到自己定义字段的成本(其实就是懒), 我们也使用group和version.
除了group, version, 当然interface也是不可少的, interface可以使用接口的全限定名来确定, 因此第三个参数就是interfaceName, 那么有没有interfaceImpl呢? 显然是没有的, 因为我们是要注册到外部的注册中心, 用于消费者去做服务发现的, 消费者无须知道该服务的具体实现是什么, 所以不需要在注册中心注册interfaceImplName
.
以上对于服务的区分的字段已经确定完了, 目前还缺少生产者的ip和端口号, 因为消费者在注册中心发现服务之后, 就需要通过网络传输去调用, 就必须知道提供服务一方的地址和端口, 因此生产者注册注册中心时, 还需要将自己的地址和端口号一起注册进去. 所以register的参数就要有地址和端口号.
至于getService方法就简单了, getService用于消费者端去发现服务, 消费者发现服务无非就三个参数, group, version和interfaceName, 那么返回值呢? 其实只需要一个地址和端口号就行了, 因为消费者自己清楚自己要调用的服务是哪一个, group, version, 接口是哪个, 所以注册中心只需要告知消费者它想调用的这个服务在哪台主机的哪个端口暴露着就行了
, 因此定义一个RegisteredService类, 属性就2个: hostName和port.
这样我们就确定了Register的方法, 代码如下:
@SPI
public interface Register {
void register(String group, String version, String interfaceName, String hostName, Integer port) ;
RegisteredServiceInfo getService(String group, String version, String interfaceName);
}
RegisteredService类代码(省略getter/setter):
public class RegisteredServiceInfo {
private String hostName;
private Integer port;
}
为了拓展方便, 我们依然借用dubbo的SPI机制
, 给Register接口一个@SPI注解.
下面我们编写一个FileRegister类, 去实现Register, 目标功能是在文件中注册我们的服务
. 由于太过简单, 就直接粘贴代码.
public class FileRegister implements Register {
@Override
public void register(String group, String version, String interfaceName, String hostName, Integer port) {
FileOutputStream fileOut = null;
try {
fileOut = new FileOutputStream("D:/registerCenter.txt", true);
fileOut.write((group).getBytes(StandardCharsets.UTF_8));
fileOut.write((" " + version).getBytes(StandardCharsets.UTF_8));
fileOut.write((" " + interfaceName).getBytes(StandardCharsets.UTF_8));
fileOut.write((" " + hostName).getBytes(StandardCharsets.UTF_8));
fileOut.write((" " + port).getBytes(StandardCharsets.UTF_8));
fileOut.write("\n".getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
} finally {
if (fileOut != null) {
try {
fileOut.close();
} catch (IOException e) {
}
}
}
}
@Override
public RegisteredServiceInfo getService(String group, String version, String interfaceName) {
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("D:/registerCenter.txt")));
String s;
while ((s = bufferedReader.readLine()) != null) {
String[] s1 = s.split(" ");
String groupF = s1[0];
String versionF = s1[1];
String interfaceNameF = s1[2];
String hostName = s1[3];
String port = s1[4];
if (group.equals(groupF) && version.equals(versionF) && interfaceName.equals(interfaceNameF)) {
return new RegisteredServiceInfo(hostName, Integer.parseInt(port));
}
}
} catch (Exception e) {
return null;
}
return null;
}
}
当然, 在META-INF/dubbo/internal中, 我们需要一个com.darkness.rpc.registercenter.Register文件:
文件内容就是实现了Register的接口, 其中file就是文件注册, boot就是启动一个springboot项目, 在项目的缓存中注册:
file=com.darkness.rpc.registercenter.file.FileRegister
boot=com.darkness.rpc.registercenter.boot.BootRegister
本地注册中心
本地注册中心这个概念就很奇怪, 为什么需要本地注册中心? 别忘了, 我们在注册服务的时候, 并没有指定实现类是哪个, 因此生产者接收到消费者调用的时候, 就需要到一个地方去寻找这个服务的实现类, 这个地方直接通过本地缓存实现就行了
, 因此叫做本地注册. 实现起来也很简单, 只要生产者在启动的时候, 注册完注册中心, 再put到本地的某个map缓存中就行了, 重点在于map的缓存结构如何做. 代码如下:
public class LocalRegister {
// 先group区分, 再itfClass区分, 再version区分, 最终得到一个实现类结果
private static final Map<String, Map<String, Map<String, Class<?>>>> implServiceMap = new ConcurrentHashMap<>();
public static Class<?> getImplClass(String group, String itfClassName, String version) {
return implServiceMap.get(group).get(itfClassName).get(version);
}
public static void registerLocal(String group, String itfClassName, String version, Class<?> implClass) {
Map<String, Map<String, Class<?>>> itfMap = implServiceMap.computeIfAbsent(group, k -> new HashMap<>());
Map<String, Class<?>> versionMap = itfMap.computeIfAbsent(itfClassName, k -> new HashMap<>());
versionMap.put(version, implClass);
}
}
很简单, 一个三级map就搞定了. 就不多细说了, 无非就是在本地map中, 通过group, interfaceName, version, 最终确定出一个interface的实现类出来
.
生产者启动类
启动类概览
生产者启动, 需要提供一个静态方法startProvider, 那么这个方法需要哪些参数呢?
首先要确定生产者启动需要做什么工作? 上面已经说过了, 注册服务到注册中心, 启动服务器, 并且实现接收到消费者调用之后, 解析协议, 最终调用到目标实现类的功能
. 下面我们就一点点实现.
在前面我们已经做好了注册服务到注册中心的类, 因此直接就粘贴代码.
public class RpcProvider {
// 由于要根据协议注册, 因此key就是协议名, value是该协议下要注册的服务列表
private final Map<String, List<Service>> servicesMap;
public RpcProvider(Map<String, List<Service>> servicesMap) {
this.servicesMap = servicesMap;
}
public void startProvider(String registry, Integer port) {
try {
// 注册服务(注册中心), 根据注册中心类型选择不同实现类去注册, 本demo使用file(本地文件)或者boot(另开一个springboot程序注册)
Register register = ExtensionLoader.getExtensionLoader(Register.class).getExtension(registry);
for (String protocol : servicesMap.keySet()) {
int protocolPort = port;
List<Service> services = servicesMap.get(protocol);
for (Service service : services) {
register.register(service.getGroup(), service.getVersion(), service.getItfClass().getName(), InetAddress.getLocalHost().getHostAddress(), port);
// 服务端需要在本地缓存注册一下实现类, 实现类只需要服务提供者自己知道, 因此使用本地缓存注册
LocalRegister.registerLocal(service.getGroup(), service.getItfClass().getName(), service.getVersion(), service.getImplClass());
}
// 启动服务器
Protocol protocolServer = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(protocol);
// 开启线程启动服务器, 协议为http就启动tomcat, 协议为dark就启动netty
new Thread(() -> protocolServer.startServer(protocolPort)).start();
// 端口号递增, 防止端口冲突
port++;
}
} catch (Exception e) {
}
}
}
上面的代码已经很明朗了, 通过RpcProvider的构造方法, 传递进去一个map, 这个map的key就是协议, value是service列表. service类如下所示(省略getter/setter/构造方法), 里面包含了4个字段, group, version, 服务接口, 服务接口实现类.
public class Service {
private String group;
private String version;
private Class<?> itfClass;
private Class<?> implClass;
这样就能够实现注册多个服务到注册中心, 并且根据不同协议去启动不同的服务器
(当然端口不能冲突, 本例中为了方便使用port++来保证端口不一样).
解析协议
invocation对象
消费者在调用过来的时候, 会将调用的服务信息(group, version, interface, 方法名字, 参数类型, 参数列表)通过网络传输传递过来, 对于rpc的生产者来说, 就需要将这个二进制字节流转换成一个它自己认识的玩意儿
, 那么对于java来说, 就直接构造一个对象就行了, 因此我们创建一个Invocation类, 这个类位于一个common模块, 因为本demo图方便, Invocation类也会让消费者调用的时候使用.
代码如下所示(省略getter/setter/构造方法).
public class Invocation implements Serializable {
private String group;
private String version;
private String interfaceName;
private String methodName;
private Class[] paramTypes;
private Object[] args;
由于这是一个对象, 我们如果要使对象能够在网络中使用二进制传输, 就需要将对象实现Serializable接口, 拿到消费者传递过来的二进制字节流时, 直接将这个字节数组转换为对象即可
.
通过invocation对象调用服务
rpc框架肯定是要基于稳定可靠的协议来的, 在传输层协议, 我们自然是要选择tcp协议, 而tcp协议是一个面向流的协议, 在socket的缓冲区中, 可能会发生黏包拆包的现象
, 而我们使用http协议的时候利用了tomcat服务器, 而tomcat自身实现了一套http协议, 自然也就处理了这个问题, 所以我们不需要关心这个问题, 直接从HttpServletRequest中拿到输入流进行解析即可.
public class HttpServerHandler {
public void handler(HttpServletRequest request, HttpServletResponse response) {
try {
ServletInputStream in = request.getInputStream();
byte[] bytes = new byte[1024];
int len;
StringBuilder reqJson = new StringBuilder();
while ((len = in.read(bytes)) != -1) {
reqJson.append(new String(bytes, 0, len));
}
Invocation invocation = JSONObject.parseObject(reqJson.toString(), Invocation.class);
String interfaceName = invocation.getInterfaceName();
String methodName = invocation.getMethodName();
Class<?>[] paramTypes = invocation.getParamTypes();
Object[] args = invocation.getArgs();
Class<?> implClass = LocalRegister.getImplClass(invocation.getGroup(), interfaceName, invocation.getVersion());
Class<?> aClass = Class.forName(interfaceName);
Method method = aClass.getMethod(methodName, paramTypes);
Serializable result = (Serializable) method.invoke(implClass.newInstance(), args);
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bo);
out.writeObject(result);
byte[] resultBytes = bo.toByteArray();
response.getOutputStream().write(resultBytes);
bo.close();
out.close();
} catch (Exception e) {
}
}
}
而当使用netty的时候, 我们就需要自己去处理这个问题了
, 我们使用一个非常通用的方法, 其实也和http协议的contentLength的思想是一样的, 我们在发送数据的时候, 指定某个字段为本次包的请求体的长度就行了, 因此构造一个Dark协议, 发送包的前面4个字节被当做本次包的长度, 后面的字节根据前面4个字节解析出的int类型值决定本次请求包的发送结尾
.
如此来, 解码和编码器就成为我们启动的Netty服务的入站出站的handler.
public class DarkProtocolDecoder extends ByteToMessageDecoder {
private int length = 0;
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() >= 4) {
if (length == 0) {
// 这里判断length=0是因为可能这次过来的数据是上次没读完的数据, 如果上次读过length, 但是没读content, 这次就不能读length了
// message中, 前面4个字节是int类型, 标志着content的长度, 如果在bytebuf中可读超过4个字节, 就可以将这4个字节拿出来得到一个int, 这个int就是content的长度
length = in.readInt();
}
if (in.readableBytes() < length) {
// 如果读完一个int, 剩下的字节数小于length, 说明还没从socket缓冲区读进来, 因此不能读, 直接返回, 等待eventpoll的下一次触发
return;
}
byte[] content = new byte[length];
if (in.readableBytes() >= length){
in.readBytes(content);
// 使用ProtoStuff将读取到的字节数组解析成Invocation对象,传递到下一个handler业务处理
Invocation invocation = ProtostuffUtil.deserializer(content, Invocation.class);
out.add(invocation);
}
// 重置当前处理器对象的length属性
length = 0;
}
}
}
public class DarkProtocolEncoder extends MessageToByteEncoder<Serializable> {
@Override
protected void encode(ChannelHandlerContext ctx, Serializable result, ByteBuf out) throws Exception {
byte[] serializer = ProtostuffUtil.serializer(result);
int length = serializer.length;
out.writeInt(length);
out.writeBytes(serializer);
}
}
上面的代码中涉及到一个ProtostuffUtil工具类
, 因为这种方式比较高效, 而且使用方便:
public class ProtostuffUtil {
private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<Class<?>, Schema<?>>();
private static <T> Schema<T> getSchema(Class<T> clazz) {
@SuppressWarnings("unchecked")
Schema<T> schema = (Schema<T>) cachedSchema.get(clazz);
if (schema == null) {
schema = RuntimeSchema.getSchema(clazz);
if (schema != null) {
cachedSchema.put(clazz, schema);
}
}
return schema;
}
public static <T> byte[] serializer(T obj) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) obj.getClass();
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
Schema<T> schema = getSchema(clazz);
return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
}
public static <T> T deserializer(byte[] data, Class<T> clazz) {
try {
T obj = clazz.newInstance();
Schema<T> schema = getSchema(clazz);
ProtostuffIOUtil.mergeFrom(data, obj, schema);
return obj;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
}
而我们的业务逻辑处理handler, 就是将Decoder解码得到的Invocation对象进行解析, 然后在本地注册中找到对应的实现类, 反射调用, 然后交给Encoder出站handler
.
public class DarkProtocolHandler extends SimpleChannelInboundHandler<Invocation> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Invocation invocation) throws Exception {
String interfaceName = invocation.getInterfaceName();
String methodName = invocation.getMethodName();
Class<?>[] paramTypes = invocation.getParamTypes();
Object[] args = invocation.getArgs();
Class<?> implClass = LocalRegister.getImplClass(invocation.getGroup(), interfaceName, invocation.getVersion());
Class<?> aClass = Class.forName(interfaceName);
Method method = aClass.getMethod(methodName, paramTypes);
Serializable result = (Serializable) method.invoke(implClass.newInstance(), args);
ctx.writeAndFlush(result);
}
}
消费者
消费者需要通过group, version以及一个接口, 来得到一个对象, 然后再根据协议去调用生产者提供的服务, 因此消费者需要提供一个方法getConsumer来获取这个接口的代理对象, 我们这里就使用jdk动态代理
.
消费者需要做什么
在写RpcConsumer之前, 需要明确consumer获取的代理对象在代理逻辑中需要做什么.
1. 首先需要从注册中心中拿到服务的host和port, 因此getConsumer方法需要提供一个参数registry, 来区分注册中心类型, 比如本demo中就使用file, 文件注册中心
.
2. 封装Invocation对象, 通过jdk动态代理调用被代理的方法的时候, 将会得到如下几个信息: 方法名, 方法参数列表, 参数列表, 接口类型, 本Consumer对象中也封装了group, version这些标识性字段, 因此Invocation对象的参数齐全, 直接封装
.
3. 通过传递过来的registry从注册中心中拿到服务的host和port, 使用SPI机制, 通过传递过来的协议名来得到具体的RequestHnadler, 使用handler来发送请求
.
实现消费者类
根据如上三点, 我们先来创建RequestHandler接口和其对应的http协议/dark协议的实现类, 以及其spi文件
.
@SPI
public interface RequestHandler {
Object handleRequest(String ip, Integer port, Invocation request, Class<?> returnType);
}
public class DarkRequestHandler implements RequestHandler {
@Override
public Object handleRequest(String ip, Integer port, Invocation request, Class<?> returnType) {
try {
Socket socket = new Socket(ip, port);
OutputStream outputStream = socket.getOutputStream();
byte[] requestBytes = ProtostuffUtil.serializer(request);
int length = requestBytes.length;
byte[] bytes = new byte[4];
// int是4个字节,存放入 byte数组
for (int i = 3; i >= 0; i--) {
bytes[i] = (byte) length;
// 对原数据进行右移8位,获得第二个字节
length = length >> 8;
}
outputStream.write(bytes);
outputStream.write(requestBytes);
InputStream inputStream = socket.getInputStream();
byte[] readInt = new byte[4];
byte[] resultBytes = new byte[8192];
// 将4个字节的length读出来忽略
inputStream.read(readInt);
int read = inputStream.read(resultBytes);
byte[] res = Bytes.copyOf(resultBytes, read);
return ProtostuffUtil.deserializer(res, returnType);
} catch (Exception e) {
}
return null;
}
}
public class DarkRequestHandler implements RequestHandler {
@Override
public Object handleRequest(String ip, Integer port, Invocation request, Class<?> returnType) {
try {
Socket socket = new Socket(ip, port);
OutputStream outputStream = socket.getOutputStream();
byte[] requestBytes = ProtostuffUtil.serializer(request);
int length = requestBytes.length;
byte[] bytes = new byte[4];
// int是4个字节,存放入 byte数组
for (int i = 3; i >= 0; i--) {
bytes[i] = (byte) length;
// 对原数据进行右移8位,获得第二个字节
length = length >> 8;
}
outputStream.write(bytes);
outputStream.write(requestBytes);
InputStream inputStream = socket.getInputStream();
byte[] readInt = new byte[4];
byte[] resultBytes = new byte[8192];
// 将4个字节的length读出来忽略
inputStream.read(readInt);
int read = inputStream.read(resultBytes);
byte[] res = Bytes.copyOf(resultBytes, read);
return ProtostuffUtil.deserializer(res, returnType);
} catch (Exception e) {
}
return null;
}
}
作为RpcConsumer, 我们的基本理念是, 一个Consumer就对应一个服务的调用者, 因此RpcConsumer需要一个构造方法, 里面封装group, version, 接口的Class, 以及一个RequestHandler对象
.
同样的RpcConsumer的getConsumer方法, 就需要对其封装的itfClass创建动态代理, 我们使用jdk动态代理, 在代理逻辑中, 进行服务发现, 使用handler进行服务调用.
public class RpcConsumer<T> {
private String group;
private Class<T> itfClass;
private String version;
private RequestHandler handler;
public RpcConsumer(String group, Class<T> itfClass, String version, String protocol) {
this.group = group;
this.itfClass = itfClass;
this.version = version;
this.handler = ExtensionLoader.getExtensionLoader(RequestHandler.class).getExtension(protocol);
}
@SuppressWarnings("unchecked")
public T getConsumer(String registry) {
return (T) Proxy.newProxyInstance(RpcConsumer.class.getClassLoader(), new Class[]{itfClass}, (proxy, method, args) -> {
Class<?>[] parameterTypes = method.getParameterTypes();
Class<?> returnType = method.getReturnType();
Invocation invocation = new Invocation();
invocation.setInterfaceName(itfClass.getName());
invocation.setGroup(group);
invocation.setArgs(args);
invocation.setMethodName(method.getName());
invocation.setVersion(version);
invocation.setParamTypes(parameterTypes);
Register register = ExtensionLoader.getExtensionLoader(Register.class).getExtension(registry);
RegisteredServiceInfo service = register.getService(group, version, itfClass.getName());
return handler.handleRequest(service.getHostName(), service.getPort(), invocation, returnType);
});
}
}
测试
至此, 我们的rpc框架就已经搞定了, 使用maven安转到本地仓库, 我们新建两个模块, 一个provider-test, 一个consumer-test, 来看看是否能够成功调用到provider提供的方法.
门面
在使用之前, 我们需要先建立一个门面模块darkness-rpc-api, 用来提供一个接口, 该接口就是消费者和生产者之间沟通的桥梁. 结构如下:
非常简单, 就是一个接口而已. 生产者需要实现它, 消费者需要通过它获取一个代理对象
, 调用生产者实现的逻辑.
public interface TestItf {
String sayHello(String name);
}
使用maven安装到本地仓库, 然后让生产者和消费者都引入它
.
provider测试类
新建provider模块, 如下所示:
pom.xml中需要引入我们刚才写好的框架的provider模块以及刚才的api模块
<dependency>
<groupId>com.darkness.rpc.framework</groupId>
<artifactId>darkness-rpc-framework-provider</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.darkness.rpc</groupId>
<artifactId>darkness-rpc-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
实现4个TestItf的实现类, TestItfImplDark1, TestItfImplDark2, TestItfImplHttp1, TestItfImplHttp2, http协议和dark协议各2个, 版本不同
.
新建ProviderMain, 启动消费者. 启动的时候, 在service列表中填装4个实现类, http协议有2个, 版本分别为1.0.1和1.0.2, dark协议的也有2个, 版本分别为1.0.1和1.0.2. 填装好之后, 创建RpcProvider对象, 启动生产者, 并指定注册中心和端口号.
public class ProviderMain {
public static void main(String[] args) {
Service httpService1 = new Service("httpGroup", "1.0.1", TestItf.class, TestItfImplHttp1.class);
Service httpService2 = new Service("httpGroup", "1.0.2", TestItf.class, TestItfImplHttp1.class);
List<Service> httpServices = new ArrayList<>();
httpServices.add(httpService1);
httpServices.add(httpService2);
Service darkService1 = new Service("darkGroup", "1.0.1", TestItf.class, TestItfImplDark1.class);
Service darkService2 = new Service("darkGroup", "1.0.2", TestItf.class, TestItfImplDark2.class);
List<Service> darkServices = new ArrayList<>();
darkServices.add(darkService1);
darkServices.add(darkService2);
Map<String, List<Service>> servicesMap = new HashMap<>();
servicesMap.put("http", httpServices);
servicesMap.put("dark", darkServices);
RpcProvider provider = new RpcProvider(servicesMap);
provider.startProvider("file", 9955);
}
}
消费者
消费者很简单, 直接通过不同的协议, 版本号获取不同的4个消费者对象, 拿到对应的服务调用的代理对象, 进行调用.
因此创建模块consumer:
模块的pom.xml引入api和consumer依赖:
<dependency>
<groupId>com.darkness.rpc</groupId>
<artifactId>darkness-rpc-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.darkness.rpc.framework</groupId>
<artifactId>darkness-rpc-framework-consumer</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
编写ConsumerMain, 只有一个main方法, 获取接口代理对象, 直接进行调用, 代码如下:
public class ConsumerMain {
public static void main(String[] args) {
String registry = "file";
RpcConsumer<TestItf> httpConsumer1 = new RpcConsumer<>("httpGroup", TestItf.class, "1.0.1", "http");
TestItf httpItf1 = httpConsumer1.getConsumer(registry);
System.out.println(httpItf1.sayHello("http"));
RpcConsumer<TestItf> httpConsumer2 = new RpcConsumer<>("httpGroup", TestItf.class, "1.0.2", "http");
TestItf httpItf2 = httpConsumer2.getConsumer(registry);
System.out.println(httpItf2.sayHello("http"));
RpcConsumer<TestItf> darkConsumer1 = new RpcConsumer<>("darkGroup", TestItf.class, "1.0.1", "dark");
TestItf darkItf1 = darkConsumer1.getConsumer(registry);
System.out.println(darkItf1.sayHello("dark"));
RpcConsumer<TestItf> darkConsumer2 = new RpcConsumer<>("darkGroup", TestItf.class, "1.0.2", "dark");
TestItf darkItf2 = darkConsumer2.getConsumer(registry);
System.out.println(darkItf2.sayHello("dark"));
}
}
测试结果
直接运行ConsumerMain, 得到如下结果:
大功告成, 得到的结果正是我们在Provider中实现的4个实现类的处理结果
.