原文地址: haifeiWu和他朋友們的部落格
部落格地址:www.hchstudio.cn
歡迎轉載,轉載請註明作者及出處,謝謝!
服務端開發都會或多或少的涉及到 RPC 的使用,當然如果止步於會用,對自己的成長很是不利,所以樓主今天本著知其然,且知其所以然的精神來探討一下 RPC 這個東西。
child-rpc模型
child-rpc 採用 socket 直連的方式來實現服務的遠端呼叫,然後使用 jdk 動態代理的方式讓呼叫者感知不到遠端呼叫。
child-rpc 開箱使用
釋出服務
RPC 服務類要監聽指定IP埠,設定要釋出的服務的實現及其介面的引用,並指定序列化的方式,目前 child-rpc 支援 Hessian,JACKSON 兩種序列化方式。
/**
* @author wuhf
* @Date 2018/9/1 18:30
**/
public class ServerTest {
public static void main(String[] args) {
ServerConfig serverConfig = new ServerConfig();
serverConfig.setSerializer(Serializer.SerializeEnum.HESSIAN.serializer)
.setPort(5201)
.setInterfaceId(HelloService.class.getName())
.setRef(HelloServiceImpl.class.getName());
ServerProxy serverProxy = new ServerProxy(new NettyServer(),serverConfig);
try {
serverProxy.export();
while (true){
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製程式碼
引用服務
RPC 客戶端要連結遠端 IP 埠,並註冊要引用的服務,然後呼叫 sayHi 方法,輸出結果
/**
* @author wuhf
* @Date 2018/9/1 18:31
**/
public class ClientTest {
public static void main(String[] args) {
ClientConfig clientConfig = new ClientConfig();
clientConfig.setHost("127.0.0.1")
.setPort(5201)
.setTimeoutMillis(100000)
.setSerializer(Serializer.SerializeEnum.HESSIAN.serializer);
ClientProxy clientProxy = new ClientProxy(clientConfig,new NettyClient(),HelloService.class);
for (int i = 0; i < 10; i++) {
HelloService helloService = (HelloService) clientProxy.refer();
System.out.println(helloService.sayHi());
}
}
}
複製程式碼
執行
server 端輸出
client 端輸出
child-rpc 具體實現
RPC 請求,響應訊息實體定義
定義訊息請求響應格式,訊息型別、訊息唯一 ID 和訊息的 json 序列化字串內容。訊息唯一 ID 是用來客戶端驗證伺服器請求和響應是否匹配。
// rpc 請求
public class RpcRequest implements Serializable {
private static final long serialVersionUID = -4364536436151723421L;
private String requestId;
private long createMillisTime;
private String className;
private String methodName;
private Class<?>[] parameterTypes;
private Object[] parameters;
// set get 方法省略掉
}
// rpc 響應
public class RpcResponse implements Serializable {
private static final long serialVersionUID = 7329530374415722876L;
private String requestId;
private Throwable error;
private Object result;
// set get 方法省略掉
}
複製程式碼
網路傳輸過程中的編碼解碼
訊息編碼解碼使用自定義的編解碼器,根據服務初始化是使用的序列化器來將資料序列化成位元組流,拆包的策略是設定指定長度的資料包,對 socket 粘包,拆包感興趣的小夥伴請移步 Socket 中粘包問題淺析及其解決方案
下面是解碼器程式碼實現 :
public class NettyDecoder extends ByteToMessageDecoder {
private Class<?> genericClass;
private Serializer serializer;
public NettyDecoder(Class<?> genericClass, Serializer serializer) {
this.genericClass = genericClass;
this.serializer = serializer;
}
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
if (byteBuf.readableBytes() < 4) {
return;
}
byteBuf.markReaderIndex();
// 讀取訊息長度
int dataLength = byteBuf.readInt();
if (dataLength < 0) {
channelHandlerContext.close();
}
if (byteBuf.readableBytes() < dataLength) {
byteBuf.resetReaderIndex();
return;
}
try {
byte[] data = new byte[dataLength];
byteBuf.readBytes(data);
Object object = serializer.deserialize(data,genericClass);
list.add(object);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製程式碼
下面是編碼器的實現:
public class NettyEncoder extends MessageToByteEncoder<Object> {
private Class<?> genericClass;
private Serializer serializer;
public NettyEncoder(Class<?> genericClass,Serializer serializer) {
this.serializer = serializer;
this.genericClass = genericClass;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object object, ByteBuf byteBuf) throws Exception {
if (genericClass.isInstance(object)) {
byte[] data = serializer.serialize(object);
byteBuf.writeInt(data.length);
byteBuf.writeBytes(data);
}
}
}
複製程式碼
RPC 業務邏輯處理 handler
server 端業務處理 handler 實現 : 主要業務邏輯是 通過 java 的反射實現方法的呼叫。
public class NettyServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, RpcRequest rpcRequest) throws Exception {
// invoke 通過呼叫反射方法獲取 rpcResponse
RpcResponse response = RpcInvokerHandler.invokeService(rpcRequest);
channelHandlerContext.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error(">>>>>>>>>>> child-rpc provider netty server caught exception", cause);
ctx.close();
}
}
public class RpcInvokerHandler {
public static Map<String, Object> serviceMap = new HashMap<String, Object>();
public static RpcResponse invokeService(RpcRequest request) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Object serviceBean = serviceMap.get(request.getClassName());
RpcResponse response = new RpcResponse();
response.setRequestId(request.getRequestId());
try {
Class<?> serviceClass = serviceBean.getClass();
String methodName = request.getMethodName();
Class<?>[] parameterTypes = request.getParameterTypes();
Object[] parameters = request.getParameters();
Method method = serviceClass.getMethod(methodName, parameterTypes);
method.setAccessible(true);
Object result = method.invoke(serviceBean, parameters);
response.setResult(result);
} catch (Throwable t) {
t.printStackTrace();
response.setError(t);
}
return response;
}
}
複製程式碼
client 端主要業務實現是等待 server 響應返回。程式碼比較簡單就不貼程式碼了,詳情請看下面給出的 github 連結。
RPC 服務端與客戶端啟動
因為服務端與客戶端啟動都是 Netty 的模板程式碼,因為篇幅原因就不貼出來了,感興趣的夥伴請移步 造個輪子---RPC動手實現。
小結
因為只是為了理解 RPC 的本質,所以在實現細節上還有好多沒有仔細去雕琢的地方。不過 RPC 的目的就是允許像呼叫本地服務一樣呼叫遠端服務,對呼叫者透明,於是我們使用了動態代理。並使用 Netty 的 handler 傳送資料和響應資料,總的來說該框架實現了簡單的 RPC 呼叫。程式碼比較簡單,主要是思路,以及瞭解 RPC 底層的實現。