01 RPC 概述
下面的這張圖,大概很多小夥伴都見到過,這是 Dubbo 官網中的一張圖描述了項目架構的演進過程。
它描述了每一種架構需要的具體配置和組織形態。當網站流量很小時,只需一個應用,將所有功能都部署在一起, 以減少部署節點和成本,我們通常會採用單一應用架構。之後出現了 ORM 框架,主要用於簡化增刪改查工作流的,數 據訪問框架 ORM 是關鍵。
隨着用戶量增加,當訪問量逐漸增大,單一應用增加機器,帶來的加速度越來越小 ,我們需要將應用拆分成互不 干擾的幾個應用,以提升效率,於是就出現了垂直應用架構。MVC 架構就是一種非常經典的用於加速前端頁面開發的 架構。
當垂直應用越來越多,應用之間交互不可避免,將核心業務抽取出來,作爲獨立的服逐漸形成穩定的服務中心, 使前端應用能更快速的響應,多變的市場需求,就出現了分佈式服務架構。分佈式架構下服務數量逐漸增加,爲了提 高管理效率,RPC 框架應運而生。RPC 用於提高業務複用及整合的,分佈式服務框架下 RPC 是關鍵。
下一代框架,將會是流動計算架構佔據主流。當服務越來越多,容量的評估,小服務的資源浪費等問題,逐漸明 顯。此時,需要增加一個調度中心 ,基於訪問壓力實時管理集羣容量,提高集羣利用率。SOA 架構就是用於提高及其 利用率的,資源調度和治理中心 SOA 是關鍵。
Netty 基本上是作爲架構的技術底層而存在的,主要完成高性能的網絡通信。
02 環境預設
第一步:我們先將項目環境搭建起來,創建 pom.xml 配置文件如下:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
第二步:創建項目結構。
在沒有 RPC 框架以前,我們的服務調用是這樣的,如下圖:
從上圖可以看出接口的調用完全沒有規律可循,想怎麼調,就怎麼調。這導致業務發展到一定階段之後,對接口 的維護變得非常困難。於是有人提出了服務治理的概念。所有服務間不允許直接調用,而是先到註冊中心進行登記, 再由註冊中心統一協調和管理所有服務的狀態並對外發布,調用者只需要記住服務名稱,去找註冊中心獲取服務即可。這樣,極大地規範了服務的管理,可以提高了所有服務端可控性。整個設計思想其實在我們生活中也能找到活生生的 案例。例如:我們平時工作交流,大多都是用 IM 工具,而不是面對面吼。大家只需要相互記住運營商(也就是註冊中 心)提供的號碼(如:騰訊 QQ)即可。再比如:我們打電話,所有電話號碼有運營商分配。我們需要和某一個人通 話時,只需要撥通對方的號碼,運營商(註冊中心,如中國移動、中國聯通、中國電信)就會幫我們將信號轉接過去。
目前流行的 RPC 服務治理框架主要有 Dubbo 和 Spring Cloud,下面我以比較經典的 Dubbo 爲例。Dubbo 核 心模塊主要有四個:Registry 註冊中心、Provider 服務端、Consumer 消費端、Monitor 監控中心,如下所示:
註冊中心 (Registry)
消費端 (Consumer)
服務端 (Provider)
監控中心 (Monitor)
爲了方便,我們將所有模塊全部放到一個項目中,主要模塊包括:
api:主要用來定義對外開放的功能與服務接口。
protocol:主要定義自定義傳輸協議的內容。
registry:主要負責保存所有可用的服務名稱和服務地址。
provider:實現對外提供的所有服務的具體功能。
consumer:客戶端調用。
monitor:完成調用鏈監控。
下面,我們先把項目結構搭建好,具體的項目結構截圖如下:
03 代碼實戰
3.1 創建 API 模塊
首先創建 API 模塊,provider 和 consumer 都遵循 API 模塊的規範。爲了簡化,創建兩個 Service 接口,分別是:
package com.xinfan.netty.rpc.api;
/**
* IRpcHelloService
*
* @author Lss
* @date 2020/2/18 22:50
* @Version 1.0
*/
public interface IRpcHelloService {
String hello(String name);
}
創建 IRpcService 接口,完成模擬業務加、減、乘、除運算,具體代碼如下:
package com.xinfan.netty.rpc.api;
/**
* IRpcService
*
* @author Lss
* @date 2020/2/18 22:51
* @Version 1.0
*/
public interface IRpcService {
/** 加 */
public int add(int a,int b);
/** 減 */
public int sub(int a,int b);
/** 乘 */
public int mult(int a,int b);
/** 除 */
public int div(int a,int b);
}
至此,API 模塊就定義完成了,非常簡單。接下來,我們要確定傳輸規則,也就是傳輸協議,協議內容當然要自定義, 才能體現出 Netty 的優勢。
3.2 創建自定義協議
Netty 中內置的 HTTP 協議,需要 HTTP 的編、解碼器來完成解析。我們來 看自定義協議如何設定?
在 Netty 中要完成一個自定義協議,其實非常簡單,只需要定義一個普通的 Java 類即可。我們現在手寫 RPC 主要 是完成對 Java 代碼的遠程調用(類似於 RMI,大家應該都很熟悉了),遠程調用 Java 代碼哪些內容是必須由網絡來 傳輸的呢?譬如,服務名稱?需要調用該服務的哪個方法?方法的實參是什麼?這些信息都需要通過客戶端傳送到服 務端去。
下面我們來看具體的代碼實現,定義 InvokerProtocol 類:
import lombok.Data;
import java.io.Serializable;
/**
* InvokerProtocol
*
* @author Lss
* @date 2020/2/18 22:54
* @Version 1.0
*/
@Data
public class InvokerProtocol implements Serializable {
//類名
private String className;
//函數名稱(方法名)
private String methodName;
//參數類型
private Class<?>[] parames;
//參數列表
private Object[] values;
}
從上面的代碼看出來,協議中主要包含的信息有類名、函數名、形參列表和實參列表,通過這些信息就可以定位到一 個具體的業務邏輯實現。
3.3 實現 Provider 服務端業務邏輯
我們將 API 中定義的所有功能在 provider 模塊中實現,分別創建兩個實現類:
RpcHelloServiceImpl 類:
package com.xinfan.netty.rpc.provider;
import com.xinfan.netty.rpc.api.IRpcHelloService;
/**
* RpcHelloServiceImpl
*
* @author Lss
* @date 2020/2/18 23:00
* @Version 1.0
*/
public class RpcHelloServiceImpl implements IRpcHelloService{
@Override
public String hello(String name) {
return "Hello " + name + "!";
}
}
RpcServiceImpl 類:
package com.xinfan.netty.rpc.provider;
import com.xinfan.netty.rpc.api.IRpcService;
/**
* RpcServiceImpl
*
* @author Lss
* @date 2020/2/18 23:00
* @Version 1.0
*/
public class RpcServiceImpl implements IRpcService {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a - b;
}
@Override
public int mult(int a, int b) {
return a * b;
}
@Override
public int div(int a, int b) {
return a / b;
}
}
3.4 完成 Registry 服務註冊
Registry 註冊中心主要功能就是負責將所有 Provider 的服務名稱和服務引用地址註冊到一個容器中,並對外發布。 Registry 應該要啓動一個對外的服務,很顯然應該作爲服務端,並提供一個對外可以訪問的端口。先啓動一個 Netty 服務,創建 RpcRegistry 類,具體代碼如下:
package com.xinfan.netty.rpc.registry;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
/**
* RpcRegistry
*
* @author Lss
* @date 2020/2/18 23:27
* @Version 1.0
*/
public class RpcRegistry {
private int port;
public RpcRegistry(int port){
this.port = port;
}
public void start(){
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//自定義協議解碼器
/** 入參有5個,分別解釋如下
maxFrameLength:框架的最大長度。如果幀的長度大於此值,則將拋出TooLongFrameException。
lengthFieldOffset:長度字段的偏移量:即對應的長度字段在整個消息數據中得位置
lengthFieldLength:長度字段的長度。如:長度字段是int型表示,那麼這個值就是4(long型就是8)
lengthAdjustment:要添加到長度字段值的補償值
initialBytesToStrip:從解碼幀中去除的第一個字節數
*/
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
//自定義協議編碼器
pipeline.addLast(new LengthFieldPrepender(4));
//對象參數類型編碼器
pipeline.addLast("encoder",new ObjectEncoder());
//對象參數類型解碼器
pipeline.addLast("decoder",new ObjectDecoder(Integer.MAX_VALUE,ClassResolvers.cacheDisabled(null)));
pipeline.addLast(new RegistryHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = b.bind(port).sync();
System.out.println("GP RPC Registry start listen at " + port );
future.channel().closeFuture().sync();
} catch (Exception e) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new RpcRegistry(8080).start();
}
}
在 RegistryHandler 中實現註冊的具體邏輯,上面的代碼,主要實現服務註冊和服務調用的功能。因爲所有模塊創 建在同一個項目中,爲了簡化,服務端沒有采用遠程調用,而是直接掃描本地 Class,然後利用反射調用。代碼實現如 下:
package com.xinfan.netty.rpc.registry;
import com.xinfan.netty.rpc.protocol.InvokerProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.io.File;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* RegistryHandler
*
* @author Lss
* @date 2020/2/18 23:58
* @Version 1.0
*/
public class RegistryHandler extends ChannelInboundHandlerAdapter {
//用保存所有可用的服務
public static ConcurrentHashMap<String, Object> registryMap = new ConcurrentHashMap<String,Object>();
//保存所有相關的服務類
private List<String> classNames = new ArrayList<String>();
public RegistryHandler(){
//完成遞歸掃描
scannerClass("com.xinfan.netty.rpc.provider");
doRegister();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Object result = new Object();
InvokerProtocol request = (InvokerProtocol)msg;
//當客戶端建立連接時,需要從自定義協議中獲取信息,拿到具體的服務和實參
//使用反射調用
if(registryMap.containsKey(request.getClassName())){
Object clazz = registryMap.get(request.getClassName());
Method method = clazz.getClass().getMethod(request.getMethodName(), request.getParames());
result = method.invoke(clazz, request.getValues());
}
ctx.write(result);
ctx.flush();
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
/*
* 遞歸掃描
*/
private void scannerClass(String packageName){
URL url = this.getClass().getClassLoader().getResource(packageName.replaceAll("\\.", "/"));
File dir = new File(url.getFile());
for (File file : dir.listFiles()) {
//如果是一個文件夾,繼續遞歸
if(file.isDirectory()){
scannerClass(packageName + "." + file.getName());
}else{
classNames.add(packageName + "." + file.getName().replace(".class", "").trim());
}
}
}
/**
* 完成註冊
*/
private void doRegister(){
if(classNames.size() == 0){ return; }
for (String className : classNames) {
try {
Class<?> clazz = Class.forName(className);
Class<?> i = clazz.getInterfaces()[0];
registryMap.put(i.getName(), clazz.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
至此,註冊中心的基本功能就已完成,下面來看客戶端的代碼實現。
3.5 實現 Consumer 遠程調用
梳理一下基本的實現思路,主要完成一個這樣的功能:API 模塊中的接口功能在服務端實現(並沒有在客戶端實現)。 因此,客戶端調用 API 中定義的某一個接口方法時,實際上是要發起一次網絡請求去調用服務端的某一個服務。而這 個網絡請求首先被註冊中心接收,由註冊中心先確定需要調用的服務的位置,再將請求轉發至真實的服務實現,最終 調用服務端代碼,將返回值通過網絡傳輸給客戶端。整個過程對於客戶端而言是完全無感知的,就像調用本地方法一 樣。具體調用過程如下圖所示:
下面來看代碼實現,創建 RpcProxy 類:
package com.xinfan.netty.rpc.consumer.proxy;
import com.xinfan.netty.rpc.protocol.InvokerProtocol;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* RpcProxy
*
* @author Lss
* @date 2020/2/19 12:10
* @Version 1.0
*/
public class RpcProxy {
public static <T> T create(Class<?> clazz){
//clazz傳進來本身就是interface
MethodProxy proxy = new MethodProxy(clazz);
Class<?> [] interfaces = clazz.isInterface() ?
new Class[]{clazz} :
clazz.getInterfaces();
T result = (T) Proxy.newProxyInstance(clazz.getClassLoader(),interfaces,proxy);
return result;
}
private static class MethodProxy implements InvocationHandler {
private Class<?> clazz;
public MethodProxy(Class<?> clazz){
this.clazz = clazz;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果傳進來是一個已實現的具體類(本次演示略過此邏輯)
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
t.printStackTrace();
}
//如果傳進來的是一個接口(核心)
} else {
return rpcInvoke(proxy,method, args);
}
return null;
}
/**
* 實現接口的核心方法
* @param method
* @param args
* @return
*/
public Object rpcInvoke(Object proxy,Method method,Object[] args){
//傳輸協議封裝
InvokerProtocol msg = new InvokerProtocol();
msg.setClassName(this.clazz.getName());
msg.setMethodName(method.getName());
msg.setValues(args);
msg.setParames(method.getParameterTypes());
final RpcProxyHandler consumerHandler = new RpcProxyHandler();
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//自定義協議解碼器(服務端的代碼可以複製過來)
/** 入參有5個,分別解釋如下
maxFrameLength:框架的最大長度。如果幀的長度大於此值,則將拋出TooLongFrameException。
lengthFieldOffset:長度字段的偏移量:即對應的長度字段在整個消息數據中得位置
lengthFieldLength:長度字段的長度:如:長度字段是int型表示,那麼這個值就是4(long型就是8)
lengthAdjustment:要添加到長度字段值的補償值
initialBytesToStrip:從解碼幀中去除的第一個字節數
*/
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
//自定義協議編碼器
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
//對象參數類型編碼器
pipeline.addLast("encoder", new ObjectEncoder());
//對象參數類型解碼器
pipeline.addLast("decoder", new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
pipeline.addLast("handler",consumerHandler);
}
});
ChannelFuture future = b.connect("localhost", 8080).sync();
future.channel().writeAndFlush(msg).sync();
future.channel().closeFuture().sync();
} catch(Exception e){
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
return consumerHandler.getResponse();
}
}
}
接收網絡調用的返回值 RpcProxyHandler 類:
package com.xinfan.netty.rpc.consumer.proxy;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* RpcProxyHandler
*
* @author Lss
* @date 2020/2/19 12:36
* @Version 1.0
*/
public class RpcProxyHandler extends ChannelInboundHandlerAdapter{
private Object response;
public Object getResponse() {
return response;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
response=msg;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("client exception is general");
}
}
完成客戶端調用代碼 RpcConsumer類:
package com.xinfan.netty.rpc.consumer;
import com.xinfan.netty.rpc.api.IRpcHelloService;
import com.xinfan.netty.rpc.api.IRpcService;
import com.xinfan.netty.rpc.consumer.proxy.RpcProxy;
/**
* RpcConsumer
*
* @author Lss
* @date 2020/2/19 12:55
* @Version 1.0
*/
public class RpcConsumer {
public static void main(String[] args) {
IRpcHelloService rpcHello= RpcProxy.create(IRpcHelloService.class);
System.out.println(rpcHello.hello("四川情場浪子"));
IRpcService service=RpcProxy.create(IRpcService.class);
System.out.println("8 + 2 = " + service.add(8, 2));
System.out.println("8 - 2 = " + service.sub(8, 2));
System.out.println("8 * 2 = " + service.mult(8, 2));
System.out.println("8 / 2 = " + service.div(8, 2));
//Dubbo 中的 Monitor 是用 Spring 的 AOP 埋點來實現的,我沒有引入 Spring 框架
}
}
3.6 Monitor 監控
Dubbo 中的 Monitor 是用 Spring 的 AOP 埋點來實現的,我沒有引入 Spring 框架,在本代碼中不實現監控的功 能。感興趣的小夥伴,可以回顧之前 Spring AOP 的課程自行完善此功能。
04 運行效果演示
第一步,啓動註冊中心,運行結果如下:
第二步,運行客戶端,調用結果如下:
通過以上案例演示,相信小夥伴們對 Netty 的應用已經有了一個比較深刻的印象,本次只是對 RPC 的基本實現原理做了一個簡單的實現,感興趣的小夥伴可以在本項 目的基礎上繼續完善 RPC 的其他細節。歡迎留言