Netty作爲一個異步事件驅動的網絡應用框架,可以用於快速開發可維護的高性能服務器和客戶端。國內著名的RPC框架Dubbo底層使用的是Netty作爲網絡通信的。本篇文章我們來探索一下RPC框架的本質以及使用Netty來實現一個簡單地RPC框架。
1. RPC是什麼
RPC(Remote Procedure Call),翻譯成中文就是遠程過程調用。遠程過程就是相對於本地方法而言的,是運行在某個遠程的地方而不是本地。通過RPC可以實現像本地函數調用一樣調用遠程服務,是一種進程間的通信方式。RPC調用的本質可以用下圖表示:
通過上面的描述,好像RPC與Socket非常像,都是調用遠程的方法,都是client/server模式。但是值得注意的是,RPC並不等同於Socket。Socket是RPC經常採用的通信手段之一,RPC是在Socket的基礎上實現的,它比socket需要更多的網絡和系統資源。除了Socket,RPC還有其他的通信方法,比如:http、操作系統自帶的管道等技術來實現對於遠程程序的調用。微軟的Windows系統中,RPC就是採用命名管道進行通信。需要了解Socket相關概念的,可以參考之前的這篇文章golang socket編程。
1.1 本地方法調用
本地方法調用使我們開發中最常見的,如下定義一個方法:
public String sayHello(String name) {
return "hello, " + name;
}
只需要傳入一個參數,調用sayHello方法就可以得到一個輸出,入參、出參以及方法體都在同一個進程空間中,這就是本地方法調用
1.2 Socket通信
那有沒有辦法實現不同進程之間通信呢?調用方在進程A,需要調用方法B,但是方法B在進程B中。
最容易想到的實現方式
就是使用Socket通信,使用Socket可以完成跨進程調用,我們需要約定一個進程通信協議,來進行傳參,調用函數,出參。寫過Socket應該都知道,Socket是比較原始的方式,我們需要更多的去關注一些細節問題,比如參數和函數需要轉換成字節流進行網絡傳輸,也就是序列化操作,然後出參時需要反序列化。
假如RPC就是讓我們在客戶端直接使用Socket遠程調用,那無疑是個災難。所以有沒有什麼簡單方法,讓我們的調用方不需要關注細節問題,讓調用方像調用本地函數一樣,只要傳入參數,調用方法,然後坐等返回結果就可以了呢?而這個訴求的解決方案就是RPC框架——爲使用方屏蔽底層網絡通信的細節。
1.3 RPC框架
RPC框架就是用來解決上面的問題的,它能夠讓調用方像調用本地函數一樣調用遠程服務,底層通訊細節對調用方是透明的,將各種複雜性都給屏蔽掉,給予調用方極致體驗。
當server需要對方法內實現修改時,client完全感知不到,不用做任何變更。這種方式在跨部門,跨公司合作的時候是非常方便的。
1.4 RPC調用需要關注的技術細節
前面就已經說到RPC框架,可以讓調用方像調用本地函數一樣調用遠程服務。原理就是RPC框架屏蔽Socket通信的相關細節,使調用方可以向調用本地方法一樣調用遠程方法。
在使用的時候,調用方是直接調用本地函數,傳入相應參數,其他細節它不用管,至於通訊細節交給RPC框架來實現。實際上RPC框架採用代理類的方式,具體來說是動態代理的方式,在運行時動態創建新的類,也就是代理類,在該類中實現通訊的細節問題,比如與服務端的連接、參數序列化、結果反序列化等。
除了上述動態代理,還需要約定一個雙方通信的協議格式,規定好協議格式,比如請求方法的類名、請求的方法名、請求參數的數據類型,請求的參數等,這樣根據格式進行序列化後進行網絡傳輸,然後服務端收到請求對象後按照指定格式進行解碼,這樣服務端才知道具體該調用哪個方法,傳入什麼樣的參數。
剛纔又提到網絡傳輸,RPC框架重要的一環也就是網絡傳輸,服務是部署在不同主機上的,如何高效的進行網絡傳輸,儘量不丟包,保證數據完整無誤的快速傳遞出去?實際上,就是利用我們今天的主角——Netty,Netty是一個高性能的網絡通訊框架,它足以勝任我們的任務。
前面說了這麼多,再次總結下一個RPC框架需要重點關注哪幾個點:
- 動態代理
- 通信協議
- 序列化
- 網絡傳輸
當然一個優秀的RPC框架需要關注的不止上面幾點,只不過本篇文章旨在做一個簡易的RPC框架,理解了上面關鍵的幾點就夠了
2. 基於Netty實現RPC框架
上面提到,RPC框架的幾個技術細節:動態代理、通信協議、序列化以及網絡傳輸,下面我們分別來實現。
2.1 通信協議
通信協議其實就是客戶端和服務端約定的通信規則,本質就是用來約定客戶端如何將需要調用的遠程方法的信息通知給服務端,比如請求方法的類名、請求的方法名、請求參數的數據類型,請求的參數以及服務端返回結果等。所以需要約定一個通信協議用來交互上述信息。
- 請求對象
@Data
@ToString
public class RpcRequest {
/**
* 請求對象的ID,客戶端用來驗證服務器請求和響應是否匹配
*/
private String requestId;
/**
* 類名
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 參數類型
*/
private Class<?>[] parameterTypes;
/**
* 入參
*/
private Object[] parameters;
}
- 響應對象
@Data
public class RpcResponse {
/**
* 響應ID
*/
private String requestId;
/**
* 錯誤信息
*/
private String error;
/**
* 返回的結果
*/
private Object result;
}
2.2 序列化
市面上序列化協議很多,比如jdk序列化工具(ObjectInputStream/ObjectOuputStream)、protobuf,kyro、Hessian等,只要不選擇jdk自帶的序列化方法,(因爲其性能太差,序列化後產生的碼流太大),其他方式其實都可以,這裏爲了方便起見,選用JSON作爲序列化協議,使用fastjson作爲JSON框架。
爲了後續擴展方便,先定義序列化接口:
public interface Serializer {
/**
* java對象轉換爲二進制
*
* @param object
* @return
*/
byte[] serialize(Object object) throws IOException;
/**
* 二進制轉換成java對象
*
* @param clazz
* @param bytes
* @param <T>
* @return
*/
<T> T deserialize(Class<T> clazz, byte[] bytes) throws IOException;
}
我們採用JSON的方式,這裏定義實現類JSONSerializer:
public class JSONSerializer implements Serializer{
@Override
public byte[] serialize(Object object) {
return JSON.toJSONBytes(object);
}
@Override
public <T> T deserialize(Class<T> clazz, byte[] bytes) {
return JSON.parseObject(bytes, clazz);
}
}
如果需要使用其他序列化方式,可以自行實現序列化接口。
2.3 編解碼器
約定好協議格式和序列化方式之後,我們還需要編解碼器,編碼器將請求對象轉換爲適合於傳輸的格式(一般來說是字節流),而對應的解碼器是將網絡字節流轉換回應用程序的消息格式。這裏我們通過繼承Netty提供的抽象類MessageToByteEncoder實現編碼器,繼承Netty提供的抽象類ByteToMessageDecoder實現解碼器,上述抽象類繼承關係如下:
- 編碼器
public class RpcEncoder extends MessageToByteEncoder {
private Class<?> clazz;
private Serializer serializer;
public RpcEncoder(Class<?> clazz, Serializer serializer) {
this.clazz = clazz;
this.serializer = serializer;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object msg, ByteBuf byteBuf) throws Exception {
if (clazz != null && clazz.isInstance(msg)) {
byte[] bytes = serializer.serialize(msg);
byteBuf.writeInt(bytes.length);
byteBuf.writeBytes(bytes);
}
}
}
- 解碼器
public class RpcDecoder extends ByteToMessageDecoder {
private Class<?> clazz;
private Serializer serializer;
public RpcDecoder(Class<?> clazz, Serializer serializer) {
this.clazz = clazz;
this.serializer = serializer;
}
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
//因爲之前編碼的時候寫入一個Int型,4個字節來表示長度
if (byteBuf.readableBytes() < 4) {
return;
}
//標記當前讀的位置
byteBuf.markReaderIndex();
int dataLength = byteBuf.readInt();
if (byteBuf.readableBytes() < dataLength) {
byteBuf.resetReaderIndex();
return;
}
byte[] data = new byte[dataLength];
//將byteBuf中的數據讀入data字節數組
byteBuf.readBytes(data);
Object obj = serializer.deserialize(clazz, data);
list.add(obj);
}
}
2.4 Netty客戶端
下面來看看Netty客戶端是如何實現的,也就是如何使用Netty開啓客戶端,我們需要注意以下幾點:
- 編寫啓動方法,指定傳輸使用Channel
- 指定ChannelHandler,對網絡傳輸中的數據進行讀寫處理
- 添加編解碼器
- 添加失敗重試機制
- 添加發送請求消息的方法
@Slf4j
public class NettyClient {
private EventLoopGroup eventLoopGroup;
private Channel channel;
private ClientHandler clientHandler;
private String host;
private Integer port;
public NettyClient(String host, Integer port) {
this.host = host;
this.port = port;
}
public void connect() throws InterruptedException {
clientHandler = new ClientHandler();
eventLoopGroup = new NioEventLoopGroup();
//啓動類
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
//指定傳輸使用的Channel
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加編碼器
pipeline.addLast(new RpcEncoder(RpcRequest.class, new JSONSerializer()));
//添加解碼器
pipeline.addLast(new RpcDecoder(RpcResponse.class, new JSONSerializer()));
//請求處理類
pipeline.addLast(clientHandler);
}
});
/**
* 同步獲取Netty連接
*/
channel = bootstrap.connect(host, port).sync().channel();
}
/**
* 發送消息
*
* @param request
* @return
*/
public RpcResponse send(final RpcRequest request) {
try {
channel.writeAndFlush(request).await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return clientHandler.getRpcResponse(request.getRequestId());
}
@PreDestroy
public void close() {
eventLoopGroup.shutdownGracefully();
channel.closeFuture().syncUninterruptibly();
}
}
我們對於數據的處理重點在於ClientHandler類上,它繼承了ChannelDuplexHandler類,可以對出站和入站的數據進行處理。
public class ClientHandler extends ChannelDuplexHandler {
/**
* 使用Map維護請求對象ID與響應結果Future的映射關係
*/
private final Map<String, DefaultFuture> futureMap = new ConcurrentHashMap<>();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof RpcResponse) {
//獲取響應對象
RpcResponse response = (RpcResponse) msg;
DefaultFuture defaultFuture =
futureMap.get(response.getRequestId());
//將結果寫入DefaultFuture
defaultFuture.setResponse(response);
}
super.channelRead(ctx, msg);
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof RpcRequest) {
RpcRequest request = (RpcRequest) msg;
//發送請求對象之前,先把請求ID保存下來,並構建一個與響應Future的映射關係
futureMap.putIfAbsent(request.getRequestId(), new DefaultFuture());
}
super.write(ctx, msg, promise);
}
/**
* 獲取響應結果
*
* @param requestId
* @return
*/
public RpcResponse getRpcResponse(String requestId) {
try {
DefaultFuture future = futureMap.get(requestId);
return future.getRpcResponse(10);
} finally {
//獲取成功以後,從map中移除
futureMap.remove(requestId);
}
}
}
從上面實現可以看出,我們定義了一個Map,維護請求ID與響應結果的映射關係,目的是爲了客戶端用來驗證服務端響應是否與請求相匹配,因爲Netty的channel可能被多個線程使用,當結果返回時,你不知道是從哪個線程返回的,所以需要一個映射關係。
而我們的結果是封裝在DefaultFuture中的,因爲Netty是異步框架,所有的返回都是基於Future和Callback機制的,我們這裏自定義Future來實現客戶端“異步調用”。
public class DefaultFuture {
private RpcResponse rpcResponse;
private volatile boolean isSucceed = false;
private final Object object = new Object();
public RpcResponse getRpcResponse(int timeout) {
synchronized (object) {
while (!isSucceed) {
try {
object.wait(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return rpcResponse;
}
}
public void setResponse(RpcResponse response) {
if (isSucceed) {
return;
}
synchronized (object) {
this.rpcResponse = response;
this.isSucceed = true;
object.notify();
}
}
}
2.5 Netty服務端
Netty服務端的實現跟客戶端的實現差不多,只不過要注意的是,當對請求進行解碼過後,需要通過代理的方式調用本地函數。下面是服務端實現。
@Slf4j
public class NettyServer implements InitializingBean {
private ServerHandler serverHandler;
private EventLoopGroup boss;
private EventLoopGroup worker;
private Integer serverPort;
public NettyServer(ServerHandler serverHandler, Integer serverPort) {
this.serverHandler = serverHandler;
this.serverPort = serverPort;
}
@Override
public void afterPropertiesSet() throws Exception {
//使用zookeeper做註冊中心,本文不涉及,可忽略
ServiceRegistry registry = null;
if (Objects.nonNull(serverPort)) {
start(registry);
}
}
public void start(ServiceRegistry registry) throws Exception {
//負責處理客戶端連接的線程池
boss = new NioEventLoopGroup();
//負責處理讀寫操作的線程池
worker = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加解碼器
pipeline.addLast(new RpcEncoder(RpcResponse.class, new JSONSerializer()));
//添加編碼器
pipeline.addLast(new RpcDecoder(RpcRequest.class, new JSONSerializer()));
//添加請求處理器
pipeline.addLast(serverHandler);
}
});
bind(serverBootstrap, serverPort);
}
/**
* 如果端口綁定失敗,端口數+1,重新綁定
*/
public void bind(final ServerBootstrap serverBootstrap, int port) {
serverBootstrap.bind(port).addListener(future -> {
if (future.isSuccess()) {
log.info("端口[ {} ] 綁定成功", port);
} else {
log.error("端口[ {} ] 綁定失敗", port);
bind(serverBootstrap, port + 1);
}
});
}
@PreDestroy
public void close() throws InterruptedException {
boss.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
log.info("關閉Netty");
}
}
下面是服務端核心代碼,處理讀寫操作的Handler類:
@Slf4j
public class ServerHandler extends SimpleChannelInboundHandler<RpcRequest> implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setRequestId(msg.getRequestId());
try {
Object handler = handler(msg);
log.info("獲取返回結果: {} ", handler);
rpcResponse.setResult(handler);
} catch (Throwable throwable) {
rpcResponse.setError(throwable.toString());
throwable.printStackTrace();
}
ctx.writeAndFlush(rpcResponse);
}
/**
* 服務端使用代理處理請求
*
* @param request
* @return
*/
private Object handler(RpcRequest request) throws ClassNotFoundException, InvocationTargetException {
//使用Class.forName進行加載Class文件
Class<?> clazz = Class.forName(request.getClassName());
Object serviceBean = applicationContext.getBean(clazz);
log.info("serviceBean: {}", serviceBean);
Class<?> serviceClass = serviceBean.getClass();
log.info("serverClass:{}", serviceClass);
String methodName = request.getMethodName();
Class<?>[] parameterTypes = request.getParameterTypes();
Object[] parameters = request.getParameters();
//使用CGLIB Reflect
FastClass fastClass = FastClass.create(serviceClass);
FastMethod fastMethod = fastClass.getMethod(methodName, parameterTypes);
log.info("開始調用CGLIB動態代理執行服務端方法...");
return fastMethod.invoke(serviceBean, parameters);
}
}
2.6 客戶端代理
客戶端使用Java動態代理(要求所有的RPC接口都有實現的接口),需要了解動態代理相關細節的可以參考之前的文章徹底搞懂動態代理。客戶端Java動態代理實現如下:
@Slf4j
public class RpcClientDynamicProxy<T> implements InvocationHandler {
private Class<T> interfaceClazz;
private String host;
private Integer port;
public RpcClientDynamicProxy(Class<T> interfaceClazz, String host, Integer port) {
this.interfaceClazz = interfaceClazz;
this.host = host;
this.port = port;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RpcRequest request = new RpcRequest();
String requestId = UUID.randomUUID().toString();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
request.setRequestId(requestId);
request.setClassName(className);
request.setMethodName(methodName);
request.setParameterTypes(parameterTypes);
request.setParameters(args);
log.info("請求內容: {}", request);
//開啓Netty 客戶端,直連
//這裏直接指定了server的host和port,正常的RPC框架會從註冊中心獲取
NettyClient nettyClient = new NettyClient(host, port);
log.info("開始連接服務端:{}", new Date());
nettyClient.connect();
RpcResponse send = nettyClient.send(request);
log.info("請求調用返回結果:{}", send.getResult());
return send.getResult();
}
@SuppressWarnings("unchecked")
public T getProxy() {
return (T) Proxy.newProxyInstance(
interfaceClazz.getClassLoader(),
new Class<?>[]{interfaceClazz},
this
);
}
}
在代理方法中,封裝請求對象,構建NettyClient對象,並開啓客戶端,發送請求消息。
2.7 RPC遠程調用測試
上面所有代碼,就是對RPC的實現。如果要使用我們這個自己實現的RPC框架,我們可以把上述代碼打成一個jar包,在分別在client和server端引入。爲了模擬這個過程,我這裏把上述所有代碼作爲一個單獨的module,然後分別定義兩個module來實現服務端和客戶端。
其中netty-rpc-server模塊依賴netty-rpc模塊,netty-rpc-client模塊依賴netty-rpc-server和netty-rpc模塊。這裏貼一下pom配置:
- netty-rpc
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>netty-explore</artifactId>
<groupId>com.zhuoli.service</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>netty-rpc</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
</dependencies>
</project>
- netty-rpc-server
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>netty-explore</artifactId>
<groupId>com.zhuoli.service</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>netty-rpc-server</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>com.zhuoli.service</groupId>
<artifactId>netty-rpc</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
- netty-rpc-client
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>netty-explore</artifactId>
<groupId>com.zhuoli.service</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>netty-rpc-client</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>com.zhuoli.service</groupId>
<artifactId>netty-rpc</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.zhuoli.service</groupId>
<artifactId>netty-rpc-server</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
2.7.1 RPC服務端
- 服務端RPC接口:
package com.zhuoli.service.netty.explore.netty.rpc.server.contract;
public interface HelloService {
String hello(String name);
}
- 服務端RPC接口實現:
package com.zhuoli.service.netty.explore.netty.rpc.server.impl;
import com.zhuoli.service.netty.explore.netty.rpc.server.contract.HelloService;
import org.springframework.stereotype.Service;
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String hello(String name) {
return "hello, " + name;
}
}
- 服務端啓動入口
@SpringBootApplication
@Slf4j
public class RpcServerApplicationContext {
@Value("${netty.rpc.server.port}")
private Integer port;
public static void main(String[] args) throws Exception {
SpringApplication.run(RpcServerApplicationContext.class, args);
log.info("服務端啓動成功");
}
@Bean
public NettyServer nettyServer() {
return new NettyServer(serverHandler(), port);
}
@Bean
public ServerHandler serverHandler() {
return new ServerHandler();
}
}
2.7.2 RPC客戶端
- 客戶端調用RPC:
@SpringBootApplication
@Slf4j
public class NettyRpcClientApplicationContext {
public static void main(String[] args) throws Exception {
SpringApplication.run(NettyRpcClientApplicationContext.class, args);
//這裏直接指定服務端host和port了
HelloService helloService = new RpcClientDynamicProxy<>(HelloService.class, "127.0.0.1", 3663).getProxy();
String result = helloService.hello("zhuoli");
log.info("響應結果“: {}", result);
}
}
分別啓動server端和client端,服務端日誌:
客戶端日誌:
以上我們基於Netty實現了一個非常簡陋的RPC框架,比起成熟的RPC框架來說相差甚遠,甚至說基本的註冊中心都沒有實現,但是通過本次實踐,可以說我對於RPC的理解更深了,瞭解了一個RPC框架到底需要關注哪些方面,未來當我們使用成熟的RPC框架時,比如Dubbo,能夠做到心中有數,能明白其底層不過也是使用Netty作爲基礎通訊框架。如果更深入翻看開源RPC框架源碼時,也相對比較容易。
參考鏈接: