什么是RPC?
RPC就是远程过程调用,在工作中很多需求背景都会有多个服务器,有服务器A、服务器B、服务器C。有时候就会需要服务器B调用服务器A上的某个方法,这时候就需要用到RPC了。
市面上也有很多成熟的RPC框架,在我国常用的就是Dubbo。因为它有比较好的社区动态,也有比较成熟的案例项目。下面对一些常见的框架进行对比:
Dubbo、Motan、gRPC、Thrift,这四种是比较常见的RPC框架
RPC | 开发商 | 生态/社区活跃度 | 适配语言 |
---|---|---|---|
Dubbo | 阿里 | √ | Java/Golang |
Motan | 新浪 | × | Java |
gRPC | √ | C++/Java/Python/Objective-C/C#/Ruby/Go/PHP/Dart | |
Thrift | √ | 比gRPC更多 |
上面简单了解了一下什么是RPC,接下来就开始撸代码吧。
想要完成一个简单的RPC框架需要的组成部分:注册中心、数据传输以及负载均衡。
需要使用的技术:
注册中心(zookeeper)
数据传输(netty)
序列化/反序列化(kryo)
负载均衡算法
Java反射
我们再来看看RPC框架的整体逻辑:
- 服务端(生产者)先向注册中心中注册信息(name:ip:port)。
- 消费者(客户端)就根据配置信息去注册中心中拉取对应的IP地址。
- 客户端通过Netty进行传输,先对数据进行序列化。
- 服务端接收到信息后进行反序列化获取到传输数据,对数据进行解析访问到指定的方法。
- 访问方法返回后,再通过Netty的WriteAndFlush传输到客户端。
- 客户端解析后获得数据,此时同步锁解除,进行接下来的逻辑。
注册中心
在这里我使用的是Zookeeper作为注册中心(你想要使用其他的当然也没问题,使用mysql都可以),然后使用Curator Framework就可以简单的操作ZK了。
zk的简单命令
查看常用命令
通过命令help
查看ZK常用的命令
创建节点
##创建根节点,如果根节点没创建,则无法创建子节点
[zk: localhost:2181(CONNECTED) 2] create /shenweiqu
Created /shenweiqu
##创建子节点test,对应的数据为123
[zk: localhost:2181(CONNECTED) 3] create /shenweiqu/test '123'
Created /shenweiqu/test
获取节点数据
[zk: localhost:2181(CONNECTED) 4] get /shenweiqu/test
123
更新节点数据
[zk: localhost:2181(CONNECTED) 5] set /shenweiqu/test '123123'
[zk: localhost:2181(CONNECTED) 6] get /shenweiqu/test
123123
查看某个目录下的子节点
[zk: localhost:2181(CONNECTED) 9] ls /shenweiqu
[test]
##没有加目录名,则查询根目录所有节点
[zk: localhost:2181(CONNECTED) 10] ls /
[Lionfish, admin, brokers, cluster, config, consumers, controller_epoch, dolphinscheduler, feature, isr_change_notification, latest_producer_id_block, lionfish, log_dir_event_notification, shenweiqu, zookeeper]
查看节点状态
[zk: localhost:2181(CONNECTED) 12] stat /shenweiqu
cZxid = 0x63aae7
ctime = Tue Feb 07 09:54:28 CST 2023
mZxid = 0x63aae7
mtime = Tue Feb 07 09:54:28 CST 2023
pZxid = 0x63aae8
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
删除节点
[zk: localhost:2181(CONNECTED) 15] delete /shenweiqu/test
[zk: localhost:2181(CONNECTED) 16] ls /shenweiqu
[]
Curator的简单用法
创建Curator连接
public static CuratorFramework zkClient() {
ExponentialBackoffRetry retry = new ExponentialBackoffRetry(BASE_SLEEP_TIME_MS, MAX_RETRIES);
String hostAddress = "";
try {
InetAddress localHost = Inet4Address.getLocalHost();
hostAddress = localHost.getHostAddress();
} catch (Exception e) {
e.printStackTrace();
hostAddress = "127.0.0.1";
}
CuratorFramework client = CuratorFrameworkFactory.builder().
connectString(hostAddress + ":2181").
retryPolicy(retry).
build();
client.start();
return client;
}
创建节点
通常我们将zk上的节点分为4个部分:
持久节点(PERSISTENT):
只要创建就一直存在,即使集群宕机,直到手动删除。
临时节点(EPHEMERAL):
临时节点的生命周期与客户端绑定,客户端的会话消失,临时节点也就消失。而且,临时节点不能作为子节点,只能够做为叶子节点。
持久顺序节点(PERSISTENT_SEQUENTIAL):
除了具有持久节点的特性外,子节点还具有顺序性。
临时顺序节点(EPHEMERAL_SEQUENTIAL):
除了具有临时节点的特性外,子节点还具有顺序性。
client.create().withMode(CreateMode.PERSISTENT).forPath(path);
这种方法可以创建节点,但是如果没有创建节点就创建子节点就会报错,加上creatingParentsIfNeeded即可。
client.create().creatingParentsIfNeeded().
withMode(CreateMode.PERSISTENT).forPath(path);
想要创建其他类型的节点,修改withMode即可。
删除节点
client.delete().forPath(path);//如果path下有子节点则会报错提示:Node not empty: path
client.delete().deletingChildrenIfNeeded().forPath(path);
获取/更新节点下的数据
byte[] data = client.getData().forPath(path);
client.setData().forPath(path,"123".getBytes());
获取节点下的子节点
client.getChildren().forPath(path);
监听器
可以给某个节点添加监听器,当该节点的子节点发生变化时,就会调用回调函数。
PathChildrenCache pathChildrenCache = new PathChildrenCache(zkClient(), path, true);
PathChildrenCacheListener pathChildrenCacheListener = (client, cache) -> {
do something
};
pathChildrenCache.getListenable().addListener(pathChildrenCacheListener);
pathChildrenCache.start();
代码如下
public class CuratorUtils {
private final static Map<String, List<String>> SERVICE_ADDRESS_MAP = new ConcurrentHashMap<>();
private final static int BASE_SLEEP_TIME_MS = 3000;
private final static int MAX_RETRIES = 3;
public static CuratorFramework zkClient() {
//重试策略,3秒重试3次
ExponentialBackoffRetry retry = new ExponentialBackoffRetry(BASE_SLEEP_TIME_MS, MAX_RETRIES);
String hostAddress = "";
try {
InetAddress localHost = Inet4Address.getLocalHost();
hostAddress = localHost.getHostAddress();
} catch (Exception e) {
e.printStackTrace();
hostAddress = "127.0.0.1";
}
CuratorFramework client = CuratorFrameworkFactory.builder().
connectString(hostAddress + ":2181").
retryPolicy(retry).
build();
client.start();
return client;
}
public static boolean createPersistentNode(String path) {
CuratorFramework client = zkClient();
try {
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.forPath(path);
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public void deleteNode(String path){
CuratorFramework client = zkClient();
try{
client.delete().deletingChildrenIfNeeded().forPath(path);
}catch(Exception e){
e.printStackTrace();
}
}
public String getNode(String path){
try{
CuratorFramework client = zkClient();
if (client.checkExists().forPath(path) != null) {
byte[] bytes = client.getData().forPath(path);
return new String(bytes);
}
}catch(Exception e){
e.printStackTrace();
}
return null;
}
public void setNode(String path){
try{
CuratorFramework client = zkClient();
if (client.checkExists().forPath(path) != null) {
client.setData().forPath(path,"123".getBytes());
}
}catch(Exception e){
e.printStackTrace();
}
}
public static List<String> getNodeChildrens(String path) {
if (SERVICE_ADDRESS_MAP.containsKey(path)) {
return SERVICE_ADDRESS_MAP.get(path);
}
try {
CuratorFramework client = zkClient();
List<String> urls = client.getChildren().forPath(path);
SERVICE_ADDRESS_MAP.put(path, urls);
registerWatcher(path);
return urls;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
static void registerWatcher(String path) {
try {
PathChildrenCache pathChildrenCache = new PathChildrenCache(zkClient(), path, true);
PathChildrenCacheListener pathChildrenCacheListener = (client, cache) -> {
List<String> urls = client.getChildren().forPath(path);
SERVICE_ADDRESS_MAP.put(path, urls);
};
pathChildrenCache.getListenable().addListener(pathChildrenCacheListener);
pathChildrenCache.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
数据传输
数据传输在这里我使用的是Netty。也可以使用Java自带的Socket,不过Socket是阻塞IO,性能低功能也单一。还可以使用NIO,不过直接使用NIO很麻烦,那还不如使用基于NIO开发出来的Netty。
在网络上传输的中所能够支持的数据类型就是二进制, 对象是没有办法直接传输的,所以我们需要先将对象进行序列化,然后再进行传输。但是不提倡直接使用Java自带的序列化接口,因为没有足够的安全性,我这里使用的是Kryo序列化框架。
服务端启动,等待连接
服务端启动前,会去注册中心(zk)中注册一个服务
ServerBootstrap b = new ServerBootstrap();
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.SO_BACKLOG, 128)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new RpcDecoder());
pipeline.addLast(new RpcEncoder());
pipeline.addLast(new NettyRpcServerHandler());
}
});
ChannelFuture channelFuture = b.bind(PORT).sync();//等待端口绑定
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}
服务端处理客户端传递的数据
客户端数据传输时,就会将对象转为二进制,所以在服务端就需要将二进制数据再次转为对象。创建一个解码类,继承ByteToMessageDecoder
类。
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
if (byteBuf.readableBytes() > 4) {
byteBuf.markReaderIndex();//记录阅读索引
int r = byteBuf.readInt();
if (r < 1) {
log.error("byte length is valid [{}]", r);
return;
}
byte[] b = new byte[r];
byteBuf.readBytes(b);
Object d = kryoSerializer.deserialize(b, genericClass);
list.add(d);
}
}
这步是在Handler之前就进行了,将数据处理好后,再去进行最终目的:方法的调用。创建一个类,继承ChannelInboundHandlerAdapter
类,重写方channelRead
方法。
RpcMessage rpcMessage = (RpcMessage) msg;
RpcRequest rpcRequest = (RpcRequest) rpcMessage.getData();
Object data = serviceHandler.handler(rpcRequest);//服务处理
RpcResponse response = RpcResponse.builder().data(data).requestId(rpcRequest.getRequestId()).build();
rpcMessage.setData(response);
ctx.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE);//将返回的数据封装后,返回给客户端
客户端启动,连接服务端
1、先去注册中心中拉取服务lookupService
2、获取到SocketInetAddress
连接服务端获得通信频道Channel
3、再使用Channel
将数据传输到服务端writeAndFlush
String path = "/lionfish/server/RPC_SERVER";
InetSocketAddress inetSocketAddress = serviceHandler.lookupService(path);
Channel channel = getChannel(inetSocketAddress);
RpcMessage rpcMessage = RpcMessage.builder().requestId(rpcRequest.getRequestId()).data(rpcRequest).build();
channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
log.info("client send message:[{}]", rpcRequest.toString());
} else {
future.cause().printStackTrace();
}
});
channel.closeFuture().sync();
AttributeKey<RpcResponse> key = AttributeKey.valueOf("rpcResponse");//获取数据源中的数据
return channel.attr(key).get();
服务端返回数据后,客户端需要对数据进行解码,然后处理数据后获取数据,整体流程结束。
RpcMessage message = (RpcMessage) msg;
RpcResponse response = (RpcResponse) message.getData();
AttributeKey<Object> key = AttributeKey.valueOf("rpcResponse");
ctx.channel().attr(key).set(response);//将返回的数据放到一个数据源中
ctx.channel().close();
负载均衡
负载均衡的算法有很多:随机算法、绝对公平算法、轮询算法、轮询权重算法、随机权重算法等。我这里选择的是随机权重算法,根据自定义的服务器权重,进行轮询,并随机。
List<String> ips = new ArrayList<>();
for (String url : urls) {
String[] paths = url.split(":");
int weight = 1;
if (paths.length >= 3) {
weight = Integer.parseInt(paths[2]);
}
for (int i = 0; i < weight; i++) {
ips.add(paths[0] + ":" + paths[1]);
}
}
Collections.shuffle(ips);//先将ips数组的顺序打乱,再获取随机数随机获得对应的ip地址
return ips;
方法调用
RPC框架最重要的就是远程方法的调用,我这里使用的是注解调用。启动项目后,先将当前项目所有包含注解RequestMapping
的类全部都找出来,然后和传输过来的数据进行对比。如果类的注解名与方法的注解名一致,则通过Java反射对方法进行invoke。
获取注解类代码
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
ClassUtils.convertClassNameToResourcePath("github.rpcserver") + "/**/*Controller.class";
try {
Resource[] resources = resolver.getResources(pattern);
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(resolver);
for (Resource resource : resources) {
MetadataReader metadataReader = factory.getMetadataReader(resource);
ClassMetadata classMetadata = metadataReader.getClassMetadata();
String className = classMetadata.getClassName();
Class<?> aClass = Class.forName(className);
RequestMapping annotation = aClass.getAnnotation(RequestMapping.class);
if (annotation != null) {
classes.add(aClass);
}
}
} catch (Exception e) {
e.printStackTrace();
}
反射代码
for (Class<?> v : classes) {
RequestMapping annotation = v.getAnnotation(RequestMapping.class);
String value = annotation.value()[0];
if (interfaceName.equals(value)) {
Method[] methods = v.getMethods();
for (Method method : methods) {
RequestMapping anno = method.getAnnotation(RequestMapping.class);
String s = anno.value()[0];
if (s.equals(o.getMethod())) {
return method.invoke(v.newInstance(), o.getParameters());
}
}
}
}
以上关于简单的RPC框架就完成了,代码还是比较乱,后面也会尝试进行优化。