基於Netty 手寫 Dubbo 框架
1、Dubbo是什麼,解決什麼樣的問題?
爲了解決模塊拆分後,彼此遠程調用的問題。
RPC -> Remote Procedure Call 遠程調用,常見的RPC框架有:
阿里的:dubbo。
噹噹的:dubbox。
谷歌的:grpc。
SpringCloud(一站式開發)等。
2、實現方案
查看官網dubbo結構圖
1、首先通過register
將服務提供者的url註冊到Registry
註冊中心中。
2、客戶端Consumer
從註冊中心獲取被調用服務端註冊信息,如:接口名稱,URL地址等信息。
3、將獲取的url地址返回到Consumer
客戶端,客戶端通過獲取的URL地址支持invoke
反射機制獲取服務的實現。
3、整體項目結構信息
|-- netty-to-dubbo
|-- netty-dubbo-api
|-- cn.org.july.netty.dubbo.api
|-- Iservice : 對外服務暴露接口
|-- RpcRequest :服務請求對象Bean
|-- netty-dubbo-common
|-- cn.org.july.netty.dubbo.annotation
|-- RpcAnnotation : 定義一個接口標識註解
|-- netty-dubbo-server
|-- cn.org.july.netty.dubbo.registry
|-- IRegisterCenter :服務註冊接口
|-- RegisterCenterImpl:服務註冊實現類
|-- ZkConfig:ZK配置文件
|-- cn.org.july.netty.dubbo.rpc
|-- NettyRpcServer:基於netty實現的Rpc通訊服務
|-- RpcServerHandler:Rpc服務處理流程
|-- cn.org.july.netty.dubbo.service
|-- ServiceImpl:對外接口IService接口實現類
|-- netty-dubbo-client
|-- cn.org.july.netty.dubbo.loadbalance
|-- LoadBalance :負載均衡實現接口
|-- RandomLoadBalance:負載均衡實現類隨機獲取服務提供者
|-- cn.org.july.netty.dubbo.proxy
|-- RpcClientProxy:netty客戶端通訊組件
|-- RpcProxyHandler:netty與服務端通訊消息處理組件
|-- cn.org.july.netty.dubbo.registry
|-- IServiceDiscover:從註冊中心獲取註冊的服務接口
|-- ServiceDiscoverImpl:接口IServiceDiscover的實現類
|-- ZkConfig:zk配置文件。
4、服務提供者Provider
端
4.1、實現Iservice
接口
首先我們看下Iservice
接口的內容:
package cn.org.july.netty.dubbo.api;
/**
* @author july_whj
*/
public interface IService {
/**
* 計算加法
*/
int add(int a, int b);
/**
* @param msg
*/
String sayHello(String msg);
}
我們編寫ServiceImpl
實現以上兩個接口類。
package cn.org.july.netty.dubbo.service;
import cn.org.july.netty.dubbo.annotation.RpcAnnotation;
import cn.org.july.netty.dubbo.api.IService;
/**
* @author july_whj
*/
@RpcAnnotation(IService.class)
public class ServiceImpl implements IService {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public String sayHello(String msg) {
System.out.println("rpc say :" + msg);
return "rpc say: " + msg;
}
}
該類實現比較簡單,不做多處理,下面分析服務註冊。
4.2、服務註冊到ZK
首先我們定義一個接口類IRegisterCenter
,裏面定義一個registry
方法,該方法實現服務註冊。服務註冊需要將服務的名稱、服務的地址註冊到註冊中心中,我們定義接口如下:
package cn.org.july.netty.dubbo.registry;
/**
* @author july_whj
*/
public interface IRegisterCenter {
/**
* 服務註冊
* @param serverName 服務名稱(實現方法路徑)
* @param serviceAddress 服務地址
*/
void registry(String serverName,String serviceAddress);
}
第二,我們使用zookeerper作爲服務註冊中心,在netty-dubbo-server
模塊中引入zk的客戶端操作類,pom文件如下:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.5.0</version>
</dependency>
注意:這裏版本使用的2.5.0,我使用的zk版,
第三,實現該接口編寫接口實現類RegisterCenterImpl
。
package cn.org.july.netty.dubbo.registry;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
/**
* @author july_whj
*/
public class RegisterCenterImpl implements IRegisterCenter {
private CuratorFramework curatorFramework;
{
curatorFramework = CuratorFrameworkFactory.builder()
.connectString(ZkConfig.addr).sessionTimeoutMs(4000)
.retryPolicy(new ExponentialBackoffRetry(1000, 10)).build();
curatorFramework.start();
}
@Override
public void registry(String serverName, String serviceAddress) {
String serverPath = ZkConfig.ZK_REGISTER_PATH.concat("/").concat(serverName);
try {
if (curatorFramework.checkExists().forPath(serverPath) == null) {
curatorFramework.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT).forPath(serverPath, "0".getBytes());
}
String addr = serverPath.concat("/").concat(serviceAddress);
String rsNode = curatorFramework.create().withMode(CreateMode.EPHEMERAL)
.forPath(addr, "0".getBytes());
System.out.println("服務註冊成功," + rsNode);
} catch (Exception e) {
e.printStackTrace();
}
}
}
我們分析下以上代碼:
定義一個CuratorFramework
對象,通過代碼塊來實例化該對象,並通過curatorFramework.start();
來連接ZKConfig中配置好的地址連接ZK。
使用zk作爲註冊中心,我們瞭解下ZK的存儲結構。zookeeper的命名空間的結構和文件系統很像。一個名字和文件一樣使用/的路徑表現,zookeeper的每個節點都是被路徑唯一標識的。
分析一下registry
方法,首先從ZkConfig中獲取要註冊數據的根節點信息,並將該信息和服務名稱進行拼接,判斷該路徑是否存在,如果不存在使用PERSISTENT
方式創建該服務名稱路徑信息。PERSISTENT
方式爲持久方式,我們使用這種方式創建因爲服務名稱不是動態變化的,不用每次去監聽它的變化。而我們服務的地址是有可能存在多個,並且有可能發生變化,我們使用EPHEMERAL
方式來創建服務的實現地址。
我們將ServiceImpl
服務註冊到zk上,我們首先獲取這個服務的服務名稱,和服務實現的地址,將該服務的服務名稱和服務地址註冊到zk上,下面看下我們的註冊服務的測試類RegTest
。
import cn.org.july.netty.dubbo.registry.IRegisterCenter;
import cn.org.july.netty.dubbo.registry.RegisterCenterImpl;
import java.io.IOException;
public class RegTest {
public static void main(String[] args) throws IOException {
IRegisterCenter registerCenter = new RegisterCenterImpl();
registerCenter.registry("cn.org.july.test", "127.0.0.1:9090");
System.in.read();
}
}
我們將cn.org.july.test
服務,和服務實現的地址127.0.0.1:9090註冊到zk中。
看下服務執行效果:
服務端顯示註冊成功,我們看以下zk服務中有沒有該數據,
最後,我們可以看到數據註冊成功。
4.3、實現NettyRpcServer
我們要將ServiceImpl
服務發佈到zk上,並通過netty監聽某個端口信息。
我們先看下
package cn.org.july.netty.dubbo.rpc;
import cn.org.july.netty.dubbo.annotation.RpcAnnotation;
import cn.org.july.netty.dubbo.registry.IRegisterCenter;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
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;
import java.util.HashMap;
import java.util.Map;
/**
* @author july_whj
*/
public class NettyRpcServer {
private IRegisterCenter registerCenter;
private String serviceAddress;
private Map<String, Object> handlerMap = new HashMap<>(16);
public NettyRpcServer(IRegisterCenter registerCenter, String serviceAddress) {
this.registerCenter = registerCenter;
this.serviceAddress = serviceAddress;
}
/**
* 發佈服務
*/
public void publisher() {
for (String serviceName : handlerMap.keySet()) {
registerCenter.registry(serviceName, serviceAddress);
}
try {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
//啓動netty服務
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline channelPipeline = channel.pipeline();
channelPipeline.addLast(new ObjectDecoder(1024 * 1024, ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())));
channelPipeline.addLast(new ObjectEncoder());
channelPipeline.addLast(new RpcServerHandler(handlerMap));
}
}).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);
String[] addr = serviceAddress.split(":");
String ip = addr[0];
int port = Integer.valueOf(addr[1]);
ChannelFuture future = bootstrap.bind(ip, port).sync();
System.out.println("服務啓動,成功。");
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 子對象的實現
*
* @param services 對象實現類
*/
public void bind(Object... services) {
//將實現類通過註解獲取實現類的名稱、實現類的實現放入map集合中。
for (Object service : services) {
RpcAnnotation annotation = service.getClass().getAnnotation(RpcAnnotation.class);
String serviceName = annotation.value().getName();
handlerMap.put(serviceName, service);
}
}
}
分析下以上代碼:
通過bind方法,將服務提供者通過RpcAnnotation
註解獲取服務名稱,並將服務名稱,服務實現類放入handlerMap 中。
通過publisher方法,獲取handlerMap 中的服務實現,將這些服務實現通過registerCenter.registry(serviceName, serviceAddress)
將這些服務註冊到zk註冊中心中,完成服務的註冊。下面代碼是netty的基礎代碼,創建兩個工作線程池,啓動netty服務,通過channelPipeline定義序列化對象和RpcServerHandler實現。這裏不做過多解析。
我們看下RpcServerHandler
的代碼實現。
package cn.org.july.netty.dubbo.rpc;
import cn.org.july.netty.dubbo.api.RpcRequest;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.nio.Buffer;
import java.util.HashMap;
import java.util.Map;
public class RpcServerHandler extends ChannelInboundHandlerAdapter {
private Map<String, Object> handlerMap = new HashMap<>();
public RpcServerHandler(Map<String, Object> handlerMap) {
this.handlerMap = handlerMap;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws UnsupportedEncodingException {
System.out.println("channelActive:" + ctx.channel().remoteAddress());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服務端接收到消息:" + msg);
RpcRequest rpcRequest = (RpcRequest) msg;
Object result = new Object();
if (handlerMap.containsKey(rpcRequest.getClassName())) {
Object clazz = handlerMap.get(rpcRequest.getClassName());
Method method = clazz.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getTypes());
result = method.invoke(clazz, rpcRequest.getParams());
}
ctx.write(result);
ctx.flush();
ctx.close();
}
}
這裏複寫了channelRead
方法,接收客戶端傳遞的RpcRequest
對象信息。下面判斷handlerMap中是否存在客戶端調用的實現類,如果存在通過反射機制獲取服務端實現類,通過invoke
方法調用方法實現,並將執行結果result
對象通過ctx.write(result);
將執行結果返回客戶端。
4.4、編寫服務啓動類ServerTest
import cn.org.july.netty.dubbo.api.IService;
import cn.org.july.netty.dubbo.registry.IRegisterCenter;
import cn.org.july.netty.dubbo.registry.RegisterCenterImpl;
import cn.org.july.netty.dubbo.rpc.NettyRpcServer;
import cn.org.july.netty.dubbo.service.ServiceImpl;
/**
* Created with IntelliJ IDEA.
* User: wanghongjie
* Date: 2019/5/3 - 23:03
* <p>
* Description:
*/
public class ServerTest {
public static void main(String[] args) {
IService service = new ServiceImpl();
IRegisterCenter registerCenter = new RegisterCenterImpl();
NettyRpcServer rpcServer = new NettyRpcServer(registerCenter, "127.0.0.1:8080");
rpcServer.bind(service);
rpcServer.publisher();
}
}
啓動netty服務,將服務實現類service通過bind方法綁定到handlerMap中,通過publisher方法,將service、服務實現地址發佈到zk,並啓動netty服務,監聽8080端口。
5、實現服務消費者
做爲服務消費者,我們首先要連接zk註冊中心,獲取服務實現的地址,並實時監聽獲取最新的地址信息。通過遠程調用實現該服務。如果服務實現是多個我們需實現客戶端負載,選取我們的服務地址。
5.1、負載均衡實現
定義loadbalance
接口.
package cn.org.july.netty.dubbo.loadbalance;
import java.util.List;
public interface LoadBalance {
String select(List<String> repos);
}
定義select
選擇方法。
通過RandomLoadBalance
實現loadbalance
接口,從實現名稱可以看到Random隨機獲取。
package cn.org.july.netty.dubbo.loadbalance;
import java.util.List;
import java.util.Random;
public class RandomLoadBalance implements LoadBalance {
@Override
public String select(List<String> repos) {
int len = repos.size();
if (len == 0)
throw new RuntimeException("未發現註冊的服務。");
Random random = new Random();
return repos.get(random.nextInt(len));
}
}
5.2、獲取註冊中心服務註冊信息
定義IServiceDiscover
接口,定義discover
方法,進行服務發現。
package cn.org.july.netty.dubbo.registry;
public interface IServiceDiscover {
String discover(String serviceName);
}
通過ServiceDiscoverImpl
實現IServiceDiscover
接口。
package cn.org.july.netty.dubbo.registry;
import cn.org.july.netty.dubbo.loadbalance.LoadBalance;
import cn.org.july.netty.dubbo.loadbalance.RandomLoadBalance;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.ArrayList;
import java.util.List;
/**
* @author july_whj
*/
public class ServiceDiscoverImpl implements IServiceDiscover {
List<String> repos = new ArrayList<String>();
private CuratorFramework curatorFramework;
public ServiceDiscoverImpl() {
curatorFramework = CuratorFrameworkFactory.builder()
.connectString(ZkConfig.addr).sessionTimeoutMs(4000)
.retryPolicy(new ExponentialBackoffRetry(1000, 10))
.build();
curatorFramework.start();
}
@Override
public String discover(String serviceName) {
String path = ZkConfig.ZK_REGISTER_PATH.concat("/").concat(serviceName);
try {
repos = curatorFramework.getChildren().forPath(path);
} catch (Exception e) {
e.printStackTrace();
}
registerWatch(path);
LoadBalance loadBalance = new RandomLoadBalance();
return loadBalance.select(repos);
}
/**
* 監聽ZK節點內容刷新
*
* @param path 路徑
*/
private void registerWatch(final String path) {
PathChildrenCache childrenCache = new PathChildrenCache(curatorFramework, path, true);
PathChildrenCacheListener childrenCacheListener = new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
repos = curatorFramework.getChildren().forPath(path);
}
};
childrenCache.getListenable().addListener(childrenCacheListener);
try {
childrenCache.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
和服務註冊同樣定義CuratorFramework
對象,並通過curatorFramework.start();
連接ZK。
連接成功後通過zk註冊的根節點加服務名稱,獲取該服務的服務地址。
獲取的服務地址有可能不是最新的服務地址,我們需要監聽zk節點的內容刷新,通過調用registerWatch
方法,監聽該節點的數據變化。
最後,將獲取到的地址集合,通過LoadBalance
隨機選出一個地址,實現該服務。
5.3、客戶端netty實現RPC遠程調用
定義客戶端實現類RpcClientProxy
.
package cn.org.july.netty.dubbo.proxy;
import cn.org.july.netty.dubbo.api.RpcRequest;
import cn.org.july.netty.dubbo.registry.IServiceDiscover;
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;
/**
* Created with IntelliJ IDEA.
* User: wanghongjie
* Date: 2019/5/3 - 23:08
* <p>
* Description:
*/
public class RpcClientProxy {
private IServiceDiscover serviceDiscover;
public RpcClientProxy(IServiceDiscover serviceDiscover) {
this.serviceDiscover = serviceDiscover;
}
public <T> T create(final Class<T> interfaceClass) {
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class<?>[]{interfaceClass}, new InvocationHandler() {
//封裝RpcRequest請求對象,然後通過netty發送給服務等
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setClassName(method.getDeclaringClass().getName());
rpcRequest.setMethodName(method.getName());
rpcRequest.setTypes(method.getParameterTypes());
rpcRequest.setParams(args);
//服務發現,zk進行通訊
String serviceName = interfaceClass.getName();
//獲取服務實現url地址
String serviceAddress = serviceDiscover.discover(serviceName);
//解析ip和port
System.out.println("服務端實現地址:" + serviceAddress);
String[] arrs = serviceAddress.split(":");
String host = arrs[0];
int port = Integer.parseInt(arrs[1]);
System.out.println("服務實現ip:" + host);
System.out.println("服務實現port:" + port);
final RpcProxyHandler rpcProxyHandler = new RpcProxyHandler();
//通過netty方式進行連接發送數據
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline channelPipeline = socketChannel.pipeline();
channelPipeline.addLast(new ObjectDecoder(1024 * 1024, ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())));
channelPipeline.addLast(new ObjectEncoder());
//netty實現代碼
channelPipeline.addLast(rpcProxyHandler);
}
});
ChannelFuture future = bootstrap.connect(host, port).sync();
//將封裝好的對象寫入
future.channel().writeAndFlush(rpcRequest);
future.channel().closeFuture().sync();
} catch (Exception e) {
} finally {
group.shutdownGracefully();
}
return rpcProxyHandler.getResponse();
}
});
}
}
我們看下create
方法,通過動態代理newProxyInstance方法,傳入待調用的接口對象,獲取getClassLoader後,實現invoke方法。定義RpcRequest
對象,封裝請求參數。通過interfaceClass
對象獲取服務實現名稱,調用discover
方法獲取服務提供者的地址信息,netty通過該信息連接服務,並將RpcRequest
對象發送到服務端,服務端解析對象,獲取接口請求參數等信息,執行方法,並將結果返回到客戶端RpcProxyHandler
對象接收返回結果。RpcProxyHandler
代碼實現:
package cn.org.july.netty.dubbo.proxy;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* Created with IntelliJ IDEA.
* User: wanghongjie
* Date: 2019/5/3 - 23:21
* <p>
* Description:
*/
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;
}
}
我們複寫channelRead
方法,獲取服務端返回的結果信息msg
,並將msg
賦值給response
,通過getResponse
獲取返回信息。
5.4、客戶單調用測試
import cn.org.july.netty.dubbo.api.IService;
import cn.org.july.netty.dubbo.proxy.RpcClientProxy;
import cn.org.july.netty.dubbo.registry.IServiceDiscover;
import cn.org.july.netty.dubbo.registry.ServiceDiscoverImpl;
/**
* Created with IntelliJ IDEA.
* User: wanghongjie
* Date: 2019/5/3 - 23:06
* <p>
* Description:
*/
public class ClientTest {
public static void main(String[] args) {
IServiceDiscover serviceDiscover = new ServiceDiscoverImpl();
RpcClientProxy rpcClientProxy = new RpcClientProxy(serviceDiscover);
IService iService = rpcClientProxy.create(IService.class);
System.out.println(iService.sayHello("netty-to-dubbo"));
System.out.println(iService.sayHello("你好"));
System.out.println(iService.sayHello("成功咯,很高興"));
System.out.println(iService.add(10, 4));
}
}
我們看下執行效果。
服務端啓動:
客戶單調用:
遠程調用完成。
源碼地址:傳送門