通用呼叫
java 從零開始手寫 RPC (01) 基於 socket 實現
java 從零開始手寫 RPC (02)-netty4 實現客戶端和服務端
java 從零開始手寫 RPC (03) 如何實現客戶端呼叫服務端?
前面我們的例子是一個固定的出參和入參,固定的方法實現。
本節將實現通用的呼叫,讓框架具有更廣泛的實用性。
基本思路
所有的方法呼叫,基於反射進行相關處理實現。
服務端
核心類
- RpcServer
調整如下:
serverBootstrap.group(workerGroup, bossGroup)
.channel(NioServerSocketChannel.class)
// 列印日誌
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
// 解碼 bytes=>resp
.addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)))
// request=>bytes
.addLast(new ObjectEncoder())
.addLast(new RpcServerHandler());
}
})
// 這個引數影響的是還沒有被accept 取出的連線
.option(ChannelOption.SO_BACKLOG, 128)
// 這個引數只是過一段時間內客戶端沒有響應,服務端會傳送一個 ack 包,以判斷客戶端是否還活著。
.childOption(ChannelOption.SO_KEEPALIVE, true);
其中 ObjectDecoder 和 ObjectEncoder 都是 netty 內建的實現。
RpcServerHandler
package com.github.houbb.rpc.server.handler;
import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.rpc.common.rpc.domain.RpcRequest;
import com.github.houbb.rpc.common.rpc.domain.impl.DefaultRpcResponse;
import com.github.houbb.rpc.server.service.impl.DefaultServiceFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @author binbin.hou
* @since 0.0.1
*/
public class RpcServerHandler extends SimpleChannelInboundHandler {
private static final Log log = LogFactory.getLog(RpcServerHandler.class);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
final String id = ctx.channel().id().asLongText();
log.info("[Server] channel {} connected " + id);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
final String id = ctx.channel().id().asLongText();
log.info("[Server] channel read start: {}", id);
// 接受客戶端請求
RpcRequest rpcRequest = (RpcRequest)msg;
log.info("[Server] receive channel {} request: {}", id, rpcRequest);
// 回寫到 client 端
DefaultRpcResponse rpcResponse = handleRpcRequest(rpcRequest);
ctx.writeAndFlush(rpcResponse);
log.info("[Server] channel {} response {}", id, rpcResponse);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
/**
* 處理請求資訊
* @param rpcRequest 請求資訊
* @return 結果資訊
* @since 0.0.6
*/
private DefaultRpcResponse handleRpcRequest(final RpcRequest rpcRequest) {
DefaultRpcResponse rpcResponse = new DefaultRpcResponse();
rpcResponse.seqId(rpcRequest.seqId());
try {
// 獲取對應的 service 實現類
// rpcRequest=>invocationRequest
// 執行 invoke
Object result = DefaultServiceFactory.getInstance()
.invoke(rpcRequest.serviceId(),
rpcRequest.methodName(),
rpcRequest.paramTypeNames(),
rpcRequest.paramValues());
rpcResponse.result(result);
} catch (Exception e) {
rpcResponse.error(e);
log.error("[Server] execute meet ex for request", rpcRequest, e);
}
// 構建結果值
return rpcResponse;
}
}
和以前類似,不過 handleRpcRequest 要稍微麻煩一點。
這裡需要根據發射,呼叫對應的方法。
pojo
其中使用的出參、入參實現如下:
RpcRequest
package com.github.houbb.rpc.common.rpc.domain;
import java.util.List;
/**
* 序列化相關處理
* (1)呼叫建立時間-createTime
* (2)呼叫方式 callType
* (3)超時時間 timeOut
*
* 額外資訊:
* (1)上下文資訊
*
* @author binbin.hou
* @since 0.0.6
*/
public interface RpcRequest extends BaseRpc {
/**
* 建立時間
* @return 建立時間
* @since 0.0.6
*/
long createTime();
/**
* 服務唯一標識
* @return 服務唯一標識
* @since 0.0.6
*/
String serviceId();
/**
* 方法名稱
* @return 方法名稱
* @since 0.0.6
*/
String methodName();
/**
* 方法型別名稱列表
* @return 名稱列表
* @since 0.0.6
*/
List<String> paramTypeNames();
// 呼叫引數資訊列表
/**
* 呼叫引數值
* @return 引數值陣列
* @since 0.0.6
*/
Object[] paramValues();
}
RpcResponse
package com.github.houbb.rpc.common.rpc.domain;
/**
* 序列化相關處理
* @author binbin.hou
* @since 0.0.6
*/
public interface RpcResponse extends BaseRpc {
/**
* 異常資訊
* @return 異常資訊
* @since 0.0.6
*/
Throwable error();
/**
* 請求結果
* @return 請求結果
* @since 0.0.6
*/
Object result();
}
BaseRpc
package com.github.houbb.rpc.common.rpc.domain;
import java.io.Serializable;
/**
* 序列化相關處理
* @author binbin.hou
* @since 0.0.6
*/
public interface BaseRpc extends Serializable {
/**
* 獲取唯一標識號
* (1)用來唯一標識一次呼叫,便於獲取該呼叫對應的響應資訊。
* @return 唯一標識號
*/
String seqId();
/**
* 設定唯一標識號
* @param traceId 唯一標識號
* @return this
*/
BaseRpc seqId(final String traceId);
}
ServiceFactory-服務工廠
為了便於對所有的 service 實現類統一管理,這裡定義 service 工廠類。
ServiceFactory
package com.github.houbb.rpc.server.service;
import com.github.houbb.rpc.server.config.service.ServiceConfig;
import com.github.houbb.rpc.server.registry.ServiceRegistry;
import java.util.List;
/**
* 服務方法類倉庫管理類-介面
*
*
* (1)對外暴露的方法,應該儘可能的少。
* (2)對於外部的呼叫,後期比如 telnet 治理,可以使用比如有哪些服務列表?
* 單個服務有哪些方法名稱?
*
* 等等基礎資訊的查詢,本期暫時全部隱藏掉。
*
* (3)前期儘可能的少暴露方法。
* @author binbin.hou
* @since 0.0.6
* @see ServiceRegistry 服務註冊,將服務資訊放在這個類中,進行統一的管理。
* @see ServiceMethod 方法資訊
*/
public interface ServiceFactory {
/**
* 註冊服務列表資訊
* @param serviceConfigList 服務配置列表
* @return this
* @since 0.0.6
*/
ServiceFactory registerServices(final List<ServiceConfig> serviceConfigList);
/**
* 直接反射呼叫
* (1)此處對於方法反射,為了提升效能,所有的 class.getFullName() 進行拼接然後放進 key 中。
*
* @param serviceId 服務名稱
* @param methodName 方法名稱
* @param paramTypeNames 引數型別名稱列表
* @param paramValues 引數值
* @return 方法呼叫返回值
* @since 0.0.6
*/
Object invoke(final String serviceId, final String methodName,
List<String> paramTypeNames, final Object[] paramValues);
}
DefaultServiceFactory
作為預設實現,如下:
package com.github.houbb.rpc.server.service.impl;
import com.github.houbb.heaven.constant.PunctuationConst;
import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.heaven.util.lang.reflect.ReflectMethodUtil;
import com.github.houbb.heaven.util.util.CollectionUtil;
import com.github.houbb.rpc.common.exception.RpcRuntimeException;
import com.github.houbb.rpc.server.config.service.ServiceConfig;
import com.github.houbb.rpc.server.service.ServiceFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 預設服務倉庫實現
* @author binbin.hou
* @since 0.0.6
*/
public class DefaultServiceFactory implements ServiceFactory {
/**
* 服務 map
* @since 0.0.6
*/
private Map<String, Object> serviceMap;
/**
* 直接獲取對應的 method 資訊
* (1)key: serviceId:methodName:param1@param2@param3
* (2)value: 對應的 method 資訊
*/
private Map<String, Method> methodMap;
private static final DefaultServiceFactory INSTANCE = new DefaultServiceFactory();
private DefaultServiceFactory(){}
public static DefaultServiceFactory getInstance() {
return INSTANCE;
}
/**
* 服務註冊一般在專案啟動的時候,進行處理。
* 屬於比較重的操作,而且一個服務按理說只應該初始化一次。
* 此處加鎖為了保證執行緒安全。
* @param serviceConfigList 服務配置列表
* @return this
*/
@Override
public synchronized ServiceFactory registerServices(List<ServiceConfig> serviceConfigList) {
ArgUtil.notEmpty(serviceConfigList, "serviceConfigList");
// 集合初始化
serviceMap = new HashMap<>(serviceConfigList.size());
// 這裡只是預估,一般為2個服務。
methodMap = new HashMap<>(serviceConfigList.size()*2);
for(ServiceConfig serviceConfig : serviceConfigList) {
serviceMap.put(serviceConfig.id(), serviceConfig.reference());
}
// 存放方法名稱
for(Map.Entry<String, Object> entry : serviceMap.entrySet()) {
String serviceId = entry.getKey();
Object reference = entry.getValue();
//獲取所有方法列表
Method[] methods = reference.getClass().getMethods();
for(Method method : methods) {
String methodName = method.getName();
if(ReflectMethodUtil.isIgnoreMethod(methodName)) {
continue;
}
List<String> paramTypeNames = ReflectMethodUtil.getParamTypeNames(method);
String key = buildMethodKey(serviceId, methodName, paramTypeNames);
methodMap.put(key, method);
}
}
return this;
}
@Override
public Object invoke(String serviceId, String methodName, List<String> paramTypeNames, Object[] paramValues) {
//引數校驗
ArgUtil.notEmpty(serviceId, "serviceId");
ArgUtil.notEmpty(methodName, "methodName");
// 提供 cache,可以根據前三個值快速定位對應的 method
// 根據 method 進行反射處理。
// 對於 paramTypes 進行 string 連線處理。
final Object reference = serviceMap.get(serviceId);
final String methodKey = buildMethodKey(serviceId, methodName, paramTypeNames);
final Method method = methodMap.get(methodKey);
try {
return method.invoke(reference, paramValues);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RpcRuntimeException(e);
}
}
/**
* (1)多個之間才用 : 分隔
* (2)引數之間採用 @ 分隔
* @param serviceId 服務標識
* @param methodName 方法名稱
* @param paramTypeNames 引數型別名稱
* @return 構建完整的 key
* @since 0.0.6
*/
private String buildMethodKey(String serviceId, String methodName, List<String> paramTypeNames) {
String param = CollectionUtil.join(paramTypeNames, PunctuationConst.AT);
return serviceId+PunctuationConst.COLON+methodName+PunctuationConst.COLON
+param;
}
}
ServiceRegistry-服務註冊類
介面
package com.github.houbb.rpc.server.registry;
/**
* 服務註冊類
* (1)每個應用唯一
* (2)每個服務的暴露協議應該保持一致
* 暫時不提供單個服務的特殊處理,後期可以考慮新增
*
* @author binbin.hou
* @since 0.0.6
*/
public interface ServiceRegistry {
/**
* 暴露的 rpc 服務埠資訊
* @param port 埠資訊
* @return this
* @since 0.0.6
*/
ServiceRegistry port(final int port);
/**
* 註冊服務實現
* @param serviceId 服務標識
* @param serviceImpl 服務實現
* @return this
* @since 0.0.6
*/
ServiceRegistry register(final String serviceId, final Object serviceImpl);
/**
* 暴露所有服務資訊
* (1)啟動服務端
* @return this
* @since 0.0.6
*/
ServiceRegistry expose();
}
實現
package com.github.houbb.rpc.server.registry.impl;
import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.rpc.common.config.protocol.ProtocolConfig;
import com.github.houbb.rpc.server.config.service.DefaultServiceConfig;
import com.github.houbb.rpc.server.config.service.ServiceConfig;
import com.github.houbb.rpc.server.core.RpcServer;
import com.github.houbb.rpc.server.registry.ServiceRegistry;
import com.github.houbb.rpc.server.service.impl.DefaultServiceFactory;
import java.util.ArrayList;
import java.util.List;
/**
* 預設服務端註冊類
* @author binbin.hou
* @since 0.0.6
*/
public class DefaultServiceRegistry implements ServiceRegistry {
/**
* 單例資訊
* @since 0.0.6
*/
private static final DefaultServiceRegistry INSTANCE = new DefaultServiceRegistry();
/**
* rpc 服務端埠號
* @since 0.0.6
*/
private int rpcPort;
/**
* 協議配置
* (1)預設只實現 tcp
* (2)後期可以擴充實現 web-service/http/https 等等。
* @since 0.0.6
*/
private ProtocolConfig protocolConfig;
/**
* 服務配置列表
* @since 0.0.6
*/
private List<ServiceConfig> serviceConfigList;
private DefaultServiceRegistry(){
// 初始化預設引數
this.serviceConfigList = new ArrayList<>();
this.rpcPort = 9527;
}
public static DefaultServiceRegistry getInstance() {
return INSTANCE;
}
@Override
public ServiceRegistry port(int port) {
ArgUtil.positive(port, "port");
this.rpcPort = port;
return this;
}
/**
* 註冊服務實現
* (1)主要用於後期服務呼叫
* (2)如何根據 id 獲取實現?非常簡單,id 是唯一的。
* 有就是有,沒有就丟擲異常,直接返回。
* (3)如果根據 {@link com.github.houbb.rpc.common.rpc.domain.RpcRequest} 獲取對應的方法。
*
* 3.1 根據 serviceId 獲取唯一的實現
* 3.2 根據 {@link Class#getMethod(String, Class[])} 方法名稱+引數型別唯一獲取方法
* 3.3 根據 {@link java.lang.reflect.Method#invoke(Object, Object...)} 執行方法
*
* @param serviceId 服務標識
* @param serviceImpl 服務實現
* @return this
* @since 0.0.6
*/
@Override
@SuppressWarnings("unchecked")
public synchronized DefaultServiceRegistry register(final String serviceId, final Object serviceImpl) {
ArgUtil.notEmpty(serviceId, "serviceId");
ArgUtil.notNull(serviceImpl, "serviceImpl");
// 構建對應的其他資訊
ServiceConfig serviceConfig = new DefaultServiceConfig();
serviceConfig.id(serviceId).reference(serviceImpl);
serviceConfigList.add(serviceConfig);
return this;
}
@Override
public ServiceRegistry expose() {
// 註冊所有服務資訊
DefaultServiceFactory.getInstance()
.registerServices(serviceConfigList);
// 暴露 netty server 資訊
new RpcServer(rpcPort).start();
return this;
}
}
ServiceConfig 是一些服務的配置資訊,介面定義如下:
package com.github.houbb.rpc.server.config.service;
/**
* 單個服務配置類
*
* 簡化使用者使用:
* 在使用者使用的時候,這個類應該是不可見的。
* 直接提供對應的服務註冊類即可。
*
* 後續擴充
* (1)版本資訊
* (2)服務端超時時間
*
* @author binbin.hou
* @since 0.0.6
* @param <T> 實現類泛型
*/
public interface ServiceConfig<T> {
/**
* 獲取唯一標識
* @return 獲取唯一標識
* @since 0.0.6
*/
String id();
/**
* 設定唯一標識
* @param id 標識資訊
* @return this
* @since 0.0.6
*/
ServiceConfig<T> id(String id);
/**
* 獲取引用實體實現
* @return 實體實現
* @since 0.0.6
*/
T reference();
/**
* 設定引用實體實現
* @param reference 引用實現
* @return this
* @since 0.0.6
*/
ServiceConfig<T> reference(T reference);
}
測試
maven 引入
引入服務端的對應 maven 包:
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>rpc-server</artifactId>
<version>0.0.6</version>
</dependency>
服務端啟動
// 啟動服務
DefaultServiceRegistry.getInstance()
.register(ServiceIdConst.CALC, new CalculatorServiceImpl())
.expose();
這裡註冊了一個計算服務,並且設定對應的實現。
和以前實現類似,此處不再贅述。
啟動日誌:
[DEBUG] [2021-10-05 13:39:42.638] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2021-10-05 13:39:42.645] [Thread-0] [c.g.h.r.s.c.RpcServer.run] - RPC 服務開始啟動服務端
十月 05, 2021 1:39:43 下午 io.netty.handler.logging.LoggingHandler channelRegistered
資訊: [id: 0xec4dc74f] REGISTERED
十月 05, 2021 1:39:43 下午 io.netty.handler.logging.LoggingHandler bind
資訊: [id: 0xec4dc74f] BIND: 0.0.0.0/0.0.0.0:9527
十月 05, 2021 1:39:43 下午 io.netty.handler.logging.LoggingHandler channelActive
資訊: [id: 0xec4dc74f, L:/0:0:0:0:0:0:0:0:9527] ACTIVE
[INFO] [2021-10-05 13:39:43.893] [Thread-0] [c.g.h.r.s.c.RpcServer.run] - RPC 服務端啟動完成,監聽【9527】埠
ps: 寫到這裡忽然發現忘記新增對應的 register 日誌了,這裡可以新增對應的 registerListener 擴充。
小結
為了便於大家學習,以上原始碼已經開源:
https://github.com/houbb/rpc
希望本文對你有所幫助,如果喜歡,歡迎點贊收藏轉發一波。
我是老馬,期待與你的下次重逢。