開源專案 https://github.com/Snailclimb/guide-rpc-framework
在學習中記錄下來的大致流程梳理,其中一些細節沒有拉出來講,主要是篇幅太長。
NettyServerMain
透過@Component 註解 拿到nettyRpcServer
NettyRpcServer nettyRpcServer = (NettyRpcServer) applicationContext.getBean("nettyRpcServer");
手動註冊HelloService
HelloService helloService2 = new HelloServiceImpl2();
// 註冊到zookeeper
RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
.group("test2").version("version2").service(helloService2).build();
public class RpcServiceConfig {
/**
* service version
*/
private String version = "";
/**
* when the interface has multiple implementation classes, distinguish by group
*/
private String group = "";
/**
* target service
*/
private Object service;
public String getRpcServiceName() {
return this.getServiceName() + this.getGroup() + this.getVersion();
}
// 實現的第一個介面的全限定名
public String getServiceName() {
return this.service.getClass().getInterfaces()[0].getCanonicalName();
}
}
//註冊
nettyRpcServer.registerService(rpcServiceConfig);
// 1 會將 service 放進一個map
serviceMap.put(rpcServiceName, rpcServiceConfig.getService());
// 2 放入zookeeper 會先獲取當前服務提供方的IP地址以及埠(固定9998)組成InetSocketAddress,
new InetSocketAddress(host, NettyRpcServer.PORT)
// 以註冊的HelloService實現的介面名+組名+版本號 + 為 zk 的節點路徑,
// 如 /my-rpc/github.javaguide.HelloServicetest1version1/20.20.20.231:9998
public static final String ZK_REGISTER_ROOT_PATH = "/my-rpc";
String servicePath = CuratorUtils.ZK_REGISTER_ROOT_PATH + "/" + rpcServiceName + inetSocketAddress.toString();
//拿到 servicePath 就會以此為path建立節點
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path);
//zk有四種節點,我們建立的忍耐節點
持久(persistent)節點:
(1) session斷開後,資料不會丟失
(2)可以建立子節點
臨時(ephemeral)節點:
(1)session斷開後,資料會丟失
(2)不可以建立子節點
持久順序(PERSISTENT_SEQUENTIAL)節點:同持久節點
建立順序節點時會預設設定順序標識,即znode名稱後會附加一個 順序號 ,順序號是一個單調遞增的計數器,由父節點維護
臨時順序(EPHEMERAL_SEQUENTIAL)節點: —> ZK實現分散式鎖的基礎。
(1)、(2)同臨時節點,(3)同持久順序節點 臨時是為了防止一個鎖無法被刪除,順序:只要自己的時順序是最小的說明拿到了鎖。
// zk 的三種監聽
NodeCache:只是監聽某一個特定的節點
PathChildrenCache:監控一個ZNode的子節點.
TreeCache:可以監控整個樹上的所有節點,類似於PathChildrenCache和NodeCache的組合
服務端啟動
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
DefaultEventExecutorGroup serviceHandlerGroup = new DefaultEventExecutorGroup(
RuntimeUtil.cpus() * 2,
ThreadPoolFactoryUtil.createThreadFactory("service-handler-group", false)
);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
// TCP預設開啟了 Nagle 演算法,該演算法的作用是儘可能的傳送大資料快,減少網路傳輸。TCP_NODELAY 引數的作用就是控制是否啟用 Nagle 演算法。
.childOption(ChannelOption.TCP_NODELAY, true)
// 是否開啟 TCP 底層心跳機制
.childOption(ChannelOption.SO_KEEPALIVE, true)
//表示系統用於臨時存放已完成三次握手的請求的佇列的最大長度,如果連線建立頻繁,伺服器處理建立新連線較慢,可以適當調大這個引數
.option(ChannelOption.SO_BACKLOG, 128)
.handler(new LoggingHandler(LogLevel.INFO))
// 當客戶端第一次進行請求的時候才會進行初始化
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 30 秒之內沒有收到客戶端請求的話就關閉連線
ChannelPipeline p = ch.pipeline();
// 在 Netty 中,IdleStateHandler 是一個用於檢測連線空閒狀態的處理器。當通道在指定時間內沒有任何讀、寫或讀寫操作時,它會觸發相應的事件。
p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
p.addLast(new RpcMessageEncoder());
p.addLast(new RpcMessageDecoder());
p.addLast(serviceHandlerGroup, new NettyRpcServerHandler());
}
});
// 繫結埠,同步等待繫結成功
ChannelFuture f = b.bind(host, PORT).sync();
// 等待服務端監聽埠關閉
f.channel().closeFuture().sync();
以上方法實在try語句中,最終要呼叫shutdownGracefully用於優雅地關閉 Netty 的執行緒池。
流水線
通訊協議:
Encoder
public class RpcMessageEncoder extends MessageToByteEncoder<RpcMessage> {
private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);
@Override
protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out) {
try {
// 魔法數 用於識別該訊息是否是該接受的
// public static final byte[] MAGIC_NUMBER = {(byte) 'g', (byte) 'r', (byte) 'p', (byte) 'c'};
out.writeBytes(RpcConstants.MAGIC_NUMBER);
// 協議版本
out.writeByte(RpcConstants.VERSION);
// 留出4位元組的訊息長度欄位
out.writerIndex(out.writerIndex() + 4);
// 訊息型別 請求/心跳/
byte messageType = rpcMessage.getMessageType();
out.writeByte(messageType);
// 序列化型別
out.writeByte(rpcMessage.getCodec());
// 壓縮型別
out.writeByte(CompressTypeEnum.GZIP.getCode());
// requestId 用了原子自增
out.writeInt(ATOMIC_INTEGER.getAndIncrement());
// build full length
// 構造訊息體
byte[] bodyBytes = null;
// 常量16位元組
int fullLength = RpcConstants.HEAD_LENGTH;
// if messageType is not heartbeat message,fullLength = head length + body length
// 如果不是心跳檢測
if (messageType != RpcConstants.HEARTBEAT_REQUEST_TYPE
&& messageType != RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
// serialize the object 序列化
String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec());
log.info("codec name: [{}] ", codecName);
// 載入序列化器
Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
.getExtension(codecName);
bodyBytes = serializer.serialize(rpcMessage.getData());
// compress the bytes 壓縮
String compressName = CompressTypeEnum.getName(rpcMessage.getCompress());
Compress compress = ExtensionLoader.getExtensionLoader(Compress.class)
.getExtension(compressName);
bodyBytes = compress.compress(bodyBytes);
// 總長度 = 頭+體
fullLength += bodyBytes.length;
}
if (bodyBytes != null) {
// 寫入訊息
out.writeBytes(bodyBytes);
}
int writeIndex = out.writerIndex();
// 定位到訊息協議長度欄位 寫入長度
out.writerIndex(writeIndex - fullLength + RpcConstants.MAGIC_NUMBER.length + 1);
out.writeInt(fullLength);
// 還原index
out.writerIndex(writeIndex);
} catch (Exception e) {
log.error("Encode request error!", e);
}
}
}
Decoder
LengthFieldBasedFrameDecoder
是 Netty 提供的一個解碼器,用於處理基於長度欄位的幀。它透過在訊息中指定長度欄位,來確定每個訊息的邊界,以解決 TCP 粘包和拆包問題。
public class RpcMessageDecoder extends LengthFieldBasedFrameDecoder {
public RpcMessageDecoder() {
// lengthFieldOffset: magic code is 4B, and version is 1B, and then full length. so value is 5
// lengthFieldLength: full length is 4B. so value is 4
// lengthAdjustment: full length include all data and read 9 bytes before, so the left length is (fullLength-9). so values is -9
// initialBytesToStrip: we will check magic code and version manually, so do not strip any bytes. so values is 0
this(RpcConstants.MAX_FRAME_LENGTH, 5, 4, -9, 0);
/**
* @param maxFrameLength 最大長度,如果超過則丟棄
* @param lengthFieldOffset 長度欄位在那個位置
* @param lengthFieldLength 長度欄位的長度
* @param lengthAdjustment 以長度欄位為基準,還有幾個位元組是訊息內容,由於我們要自己處理魔法數等等,魔法書+版本+長度是9位元組 所以要減去9位元組回到開頭
* @param initialBytesToStrip Number of bytes skipped.
* If you need to receive all of the header+body data, this value is 0
* if you only want to receive the body data, then you need to skip the number of bytes consumed by the header.
*/
}
然後根據協議定義的每個欄位幾個位元組,一個一個欄位的讀取。 然後解壓縮,反序列化。
private Object decodeFrame(ByteBuf in) {
// note: must read ByteBuf in order
checkMagicNumber(in);
checkVersion(in);
int fullLength = in.readInt();
// build RpcMessage object
byte messageType = in.readByte();
byte codecType = in.readByte();
byte compressType = in.readByte();
int requestId = in.readInt();
RpcMessage rpcMessage = RpcMessage.builder()
.codec(codecType)
.requestId(requestId)
.messageType(messageType).build();
if (messageType == RpcConstants.HEARTBEAT_REQUEST_TYPE) {
rpcMessage.setData(RpcConstants.PING);
return rpcMessage;
}
if (messageType == RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
rpcMessage.setData(RpcConstants.PONG);
return rpcMessage;
}
int bodyLength = fullLength - RpcConstants.HEAD_LENGTH;
if (bodyLength > 0) {
byte[] bs = new byte[bodyLength];
in.readBytes(bs);
// decompress the bytes
String compressName = CompressTypeEnum.getName(compressType);
Compress compress = ExtensionLoader.getExtensionLoader(Compress.class)
.getExtension(compressName);
bs = compress.decompress(bs);
// deserialize the object
String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec());
log.info("codec name: [{}] ", codecName);
Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
.getExtension(codecName);
if (messageType == RpcConstants.REQUEST_TYPE) {
RpcRequest tmpValue = serializer.deserialize(bs, RpcRequest.class);
rpcMessage.setData(tmpValue);
} else {
RpcResponse tmpValue = serializer.deserialize(bs, RpcResponse.class);
rpcMessage.setData(tmpValue);
}
}
return rpcMessage;
}
NettyRpcServerHandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
if (msg instanceof RpcMessage) {
log.info("server receive msg: [{}] ", msg);
byte messageType = ((RpcMessage) msg).getMessageType();
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setCodec(SerializationTypeEnum.HESSIAN.getCode());
rpcMessage.setCompress(CompressTypeEnum.GZIP.getCode());
// 心跳檢測
if (messageType == RpcConstants.HEARTBEAT_REQUEST_TYPE) {
rpcMessage.setMessageType(RpcConstants.HEARTBEAT_RESPONSE_TYPE);
// 返回一個pong
rpcMessage.setData(RpcConstants.PONG);
} else {
// 轉換類
RpcRequest rpcRequest = (RpcRequest) ((RpcMessage) msg).getData();
// rpcRequest.getRpcServiceName() 透過serviceName 去map裡拿到對應的類,再根據方法名和引數型別確定方法反射執行
Object result = rpcRequestHandler.handle(rpcRequest);
log.info(String.format("server get result: %s", result.toString()));
// 型別為 返回
rpcMessage.setMessageType(RpcConstants.RESPONSE_TYPE);
if (ctx.channel().isActive() && ctx.channel().isWritable()) {
RpcResponse<Object> rpcResponse = RpcResponse.success(result, rpcRequest.getRequestId());
rpcMessage.setData(rpcResponse);
} else {
RpcResponse<Object> rpcResponse = RpcResponse.fail(RpcResponseCodeEnum.FAIL);
rpcMessage.setData(rpcResponse);
log.error("not writable now, message dropped");
}
}
// 如果在寫入資料的過程中出現錯誤,Netty 會關閉連線
ctx.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
} finally {
//Ensure that ByteBuf is released, otherwise there may be memory leaks
ReferenceCountUtil.release(msg);
}
}
NettyClientMain
客戶端呼叫HelloController 的test方法
然後呼叫了helloService.hello 方法,而HelloService由與服務端提供
@Component
public class HelloController {
@RpcReference(version = "version1", group = "test1")
private HelloService helloService;
public void test() throws InterruptedException {
String hello = this.helloService.hello(new Hello("111", "222"));
//如需使用 assert 斷言,需要在 VM options 新增引數:-ea
assert "Hello description is 222".equals(hello);
Thread.sleep(12000);
for (int i = 0; i < 10; i++) {
System.out.println(helloService.hello(new Hello("111", "222")));
}
}
}
在這個bean載入的時候,就將helloService 替換為了代理物件,那麼執行hello方法,是由代理物件來完成。
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> targetClass = bean.getClass();
Field[] declaredFields = targetClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
RpcReference rpcReference = declaredField.getAnnotation(RpcReference.class);
if (rpcReference != null) {
RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
.group(rpcReference.group())
.version(rpcReference.version()).build();
// rpcClient = NettyRpcClient rpcServiceConfig = 註解指定的service 結構在上文已經提過
RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient, rpcServiceConfig);
Object clientProxy = rpcClientProxy.getProxy(declaredField.getType());
declaredField.setAccessible(true);
try {
// 將
// @RpcReference(version = "version1", group = "test1")
// private HelloService helloService;
// 替換為代理物件
declaredField.set(bean, clientProxy);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return bean;
}
代理物件
代理物件的invoke 方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
log.info("invoked method: [{}]", method.getName());
// 構造RpcRequest 類結構在下一個程式碼塊
RpcRequest rpcRequest = RpcRequest.builder().methodName(method.getName())
.parameters(args)
.interfaceName(method.getDeclaringClass().getName()) // 類名 github.javaguide.HelloService
.paramTypes(method.getParameterTypes())
.requestId(UUID.randomUUID().toString())
// rpcServiceConfig 在上一塊程式碼中 初始化代理物件時傳入
.group(rpcServiceConfig.getGroup())
.version(rpcServiceConfig.getVersion())
.build();
RpcResponse<Object> rpcResponse = null;
// 基於netty
if (rpcRequestTransport instanceof NettyRpcClient) {
// 傳送request請求
CompletableFuture<RpcResponse<Object>> completableFuture = (CompletableFuture<RpcResponse<Object>>) rpcRequestTransport.sendRpcRequest(rpcRequest);
rpcResponse = completableFuture.get();
}
// 基於socket
if (rpcRequestTransport instanceof SocketRpcClient) {
rpcResponse = (RpcResponse<Object>) rpcRequestTransport.sendRpcRequest(rpcRequest);
}
this.check(rpcResponse, rpcRequest);
return rpcResponse.getData();
}
public class RpcRequest implements Serializable {
private static final long serialVersionUID = 1905122041950251207L;
private String requestId;
private String interfaceName;
private String methodName;
private Object[] parameters;
private Class<?>[] paramTypes;
private String version;
private String group;
public String getRpcServiceName() {
return this.getInterfaceName() + this.getGroup() + this.getVersion();
}
}
sendRpcRequest
@Override
public Object sendRpcRequest(RpcRequest rpcRequest) {
// build return value
CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
// 發現服務
/**
1. lookupService 中使用到了負載均衡演算法,我們首先需要再次回顧一下服務註冊時的zk節點結構
是以註冊的HelloService實現的介面名+組名+版本號 + ip port為zk的路徑,如/my-rpc/github.javaguide.HelloServicetest1version1/20.20.20.231:9998
2. 透過 rpcRequest.getRpcServiceName 得到介面名+組名+版本號 然後拿到這個節點的所有子節點 如上舉例的就是20.20.20.231:9998,如果註冊了更多,就會拿到多個ip + port,也就對應了多個服務提供方,這樣的話就由負載均衡演算法選擇一個來提供服務,負載均衡演算法這裡就不說了。
**/
InetSocketAddress inetSocketAddress = serviceDiscovery.lookupService(rpcRequest);
// 拿到對應服務方的channel 如果沒有的話 就會執行初始的連線操作。
Channel channel = getChannel(inetSocketAddress);
if (channel.isActive()) {
// put unprocessed request
unprocessedRequests.put(rpcRequest.getRequestId(), resultFuture);
RpcMessage rpcMessage = RpcMessage.builder().data(rpcRequest)
.codec(SerializationTypeEnum.HESSIAN.getCode())
.compress(CompressTypeEnum.GZIP.getCode())
.messageType(RpcConstants.REQUEST_TYPE).build();
// 將 rpcMessage 寫入到與服務端的連線中,並觸發重新整理操作,確保資料立即傳送到服務端
channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
log.info("client send message: [{}]", rpcMessage);
} else {
future.channel().close();
resultFuture.completeExceptionally(future.cause());
log.error("Send failed:", future.cause());
}
});
} else {
throw new IllegalStateException();
}
return resultFuture;
}
tips:
該專案還有Extension去載入一些類,大部分在類初始化的時候會觸發。舉個例子。
在服務註冊的時候呼叫了nettyRpcServer.registerService(rpcServiceConfig);
public class NettyRpcServer {
public static final int PORT = 9998;
// 單例獲取例項 透過反射呼叫 ZkServiceProviderImpl 的構造器 初始化例項
private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
public void registerService(RpcServiceConfig rpcServiceConfig) {
serviceProvider.publishService(rpcServiceConfig);
}
}
registerService
呼叫了 serviceProvider
的publishService
方法,至於serviceProvider
用到雙重校驗鎖來實現單例 這裡就不說了。
關鍵在於ZkServiceProviderImpl
在構造器 初始化例項時載入了服務註冊類 serviceRegistry
public class ZkServiceProviderImpl implements ServiceProvider {
/**
* key: rpc service name(interface name + version + group)
* value: service object
*/
// 實現類 ZkServiceRegistryImpl
private final ServiceRegistry serviceRegistry;
public ZkServiceProviderImpl() {
serviceRegistry = ExtensionLoader.getExtensionLoader(ServiceRegistry.class).getExtension(ServiceRegistryEnum.ZK.getName());
// 實際就是 serviceRegistry = ZkServiceRegistryImpl
}
getExtensionLoader 會返回一個 ExtensionLoader 類
public final class ExtensionLoader<T> {
private final Class<?> type;
}
主要是指定type
其中的 type屬性就是 傳入的引數 ServiceRegistry.class
也就是 github.javaguide.registry.ServiceRegistry
需要注意這個在 src/main/resources/META-INF/extensions
下有同名的檔案
然後 呼叫這個返回的ExtensionLoader.getExtension
方法,引數為常量 "zk"
你去看下那個同名檔案就知道為什麼了~
// name = zk netty socket
public T getExtension(String name) {
if (StringUtil.isBlank(name)) {
throw new IllegalArgumentException("Extension name should not be null or empty.");
}
// firstly get from cache, if not hit, create one
// 例項快取 Map<String, Holder<Object>> cachedInstances
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);// 一個空的Holder 類,holder 只有一個屬性 value 用於存放例項物件
}
// create a singleton if no instance exists
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
private T createExtension(String name) {
// load all extension classes of type T from file and get specific one by name
// getExtensionClasses() 中返回了 Map "zk" -> "class github.javaguide.registry.zk.ZkServiceRegistryImpl" 這也是上文中提到了resource檔案中的類,那個檔案指定了要載入哪些類 然後存在 cachedClasses
Class<?> clazz = getExtensionClasses().get(name);
// clazz = github.javaguide.registry.zk.ZkServiceRegistryImpl
if (clazz == null) {
throw new RuntimeException("No such extension of name " + name);
}
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
try {
// 反射例項化
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
// github.javaguide.registry.zk.ZkServiceRegistryImpl -> 例項
instance = (T) EXTENSION_INSTANCES.get(clazz);
} catch (Exception e) {
log.error(e.getMessage());
}
}
return instance;
}
private Map<String, Class<?>> getExtensionClasses() {
// get the loaded extension class from the cache
// zk -> "class github.javaguide.registry.zk.ZkServiceRegistryImpl"
Map<String, Class<?>> classes = cachedClasses.get();
// double check
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = new HashMap<>();
// load all extensions from our extensions directory
loadDirectory(classes);
// class 快取
cachedClasses.set(classes);
}
}
}
return classes;
}