https://github.com/penkee/mogoLink
mogoLink是我於2016年開始設計的一個rpc框架,當時只是接觸了Netty技術,覺得它非常適合做rpc框架的底層通訊。對於編解碼產品,選用的是谷歌的protoBuf,不過苦於它的schema和每個類都得靜態編譯問題無法解決,然後耽擱下來。而那時候各大公司尚未服務化改造,故開發此框架經驗不足,考慮不周。
今年公司開展服務化工作,此時又接觸了dubbo,spring cloud相關書籍,系統介紹了設計rpc的理論基礎。故而有重新維護此項目。
那麼設計一個rpc框架,需要注意哪些地方呢?
- socket通訊框架:一般這個框架目前也只有netty開源的,上手快,性能也槓槓的
- 編解碼技術:java序列化、谷歌的protobuf、facebook thift、jboss marshaling、kryo、json、hession、aryo
- 粘包處理:消息定長、消息尾加分隔符、消息頭加表示長度的字段、其他複雜的應用層協議
- 服務註冊中心:分佈式系統用的,管理可用的服務地址
- 負載均衡算法:客戶端用的,分散請求不同的服務器
- 限流熔斷處理:服務端用,防止請求過多,造成服務中斷的
- 監控系統:監控服務調用次數,耗時,客戶端連接數等信息
- 日誌跟蹤系統:由於會級聯調用多層rpc,所以要生成唯一標識來跟蹤調用鏈的日誌收集系統
上圖是本系統的設計流程圖,雖然糙了點,但能看明白就行。
- FutureObject類,用於獲取未來的對象的。因爲netty接收消息是異步的,所以只能用此對象作爲溝通工具,當服務端接收到消息時放入此對象裏,則監聽端就立刻獲取到對象,否則超時處理
/**
* @brief 獲取未來對象
* @details (必填)
* @author 彭堃
* @date 2016年8月26日下午5:56:29
*/
public class FutureObject<T> {
private T value;
public T get(long outTime) {
if (value == null) {
synchronized (this) {
try {
this.wait(outTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return value;
}
public void set(T value) {
this.value = value;
synchronized (this) {
this.notify();
}
}
}
-
客戶端的代理方法:這樣所有加@Autowird的接口,spring自動注入成工廠類產生的代理對象,當調用服務方法時,則代碼執行代理的請求遠程的服務
<bean id="userInfoRemoteService" class="com.eastorm.mogolink.client.proxy.ProxyFactory">
<constructor-arg name="className" value="com.eastorm.mogolink.demo.client.service.api.IUserInfoService" />
</bean>
/**
* 創建動態代理對象
* 動態代理不需要實現接口,但是需要指定接口類型
* @author 慕容恪
*/
public class ProxyFactory implements FactoryBean {
private static final Logger logger = LoggerFactory.getLogger(ProxyFactory.class);
private String className;
public ProxyFactory(String className){
this.className=className;
}
public Object getProxyInstance() throws ClassNotFoundException {
Class target=Class.forName(className);
return Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//無需代理的父類方法
if("toString".equals(method.getName())){
return method.invoke(proxy,args);
}
BaseMessage req=new BaseMessage();
UUID uuid = UUID.randomUUID();
req.setRequestId(uuid.toString());
List<MethodParam> paramTypes=new ArrayList<>();
if(args!=null&&args.length>0){
for (Object arg : args) {
paramTypes.add(new MethodParam(arg.getClass().getName(),arg));
}
}
req.setParamTypes(paramTypes);
req.setServiceName(className);
req.setMethod(method.getName());
long s=System.currentTimeMillis();
ClientMsgHandler handler= ClientStarter.getHandler();
if(handler==null){
logger.info("請求失敗req={},耗時:{}ms",req.getRequestId(),System.currentTimeMillis()-s);
return null;
}
// Request and get the response.
BaseMessage resMsg = handler.getData(req);
if(resMsg!=null&&resMsg.getCode().equals(ServiceCodeEnum.SUCCCESS.getId())){
logger.info("req={},耗時:{}ms",resMsg.getRequestId(),System.currentTimeMillis()-s);
return resMsg.getReturnData();
}else{
logger.info("失敗req={},耗時:{}ms",req.getRequestId(),System.currentTimeMillis()-s);
}
return null;
}
});
}
@Override
public Object getObject() throws Exception {
return getProxyInstance();
}
@Override
public Class<?> getObjectType() {
Class target= null;
try {
target = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return target;
}
@Override
public boolean isSingleton() {
return true;
}
}
- channel連接池
一個客戶端如果只連接一個channel,那豈不是暴殄天物。一個channel是同步阻塞的,所有這裏每個客戶端要有一個channel連接池。本系統採用是apache的common-pool工具類,來維護channel連接。
- 粘包問題
本框架採用消息頭加消息長度字段的方式實現,具體的類是netty自帶的LengthFieldBasedFrameDecoder。我們在kryo編碼器里加了一行頭長度字段。
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, BaseMessage baseMessage, ByteBuf byteBuf) throws Exception {
byte[] data= messageCodec.serialize(baseMessage);
byteBuf.writeShort(data.length);
byteBuf.writeBytes(data);
}
如果能確定編碼後不包含分割符,也是可以用分隔符處理的,更節約。
- 消息體的定義:方法支持重載,故需要方法參數的class名
ublic class BaseMessage {
private String requestId;
private String code;
private String msg;
/**
* 返回的信息
*/
private Object returnData;
/**
* 服務名
*/
private String serviceName;
/**
* 方法
*/
private String method;
/**
* 方法的參數類型
* 用來轉型
*/
private List<MethodParam> paramTypes;
}