公众号【离心计划】,一起离开地球表面
【RPC系列合集】
| 前言
上一小节我们用Netty实现了RPC的服务端与客户端通信,加上我们之前的序列化部分,已经可以成功进行发送请求到返回响应了。还记得我们在写Netty部分的代码时有一个RpcRequestHandler和RpcResponseHandler,针对每个请求或响应需要做对应的处理。在发请求前,客户端需要知道目标的地址和端口,这是发请求的必要条件;在处理请求时,我们要做的事情是根据请求中携带的函数签名,找到服务端的方法,然后传入携带的参数调用方法,拿到结果后包装成Response给客户端,所以被调用的服务需要注册在Netty服务端,也需要注册在NameServer中提供服务签名与地址的映射关系。
代码仓库:https://github.com/JAYqq/my-sparrow-rpc/tree/v1.3,一定要参考这边的完整代码
| NameServer
我们在【专栏】RPC系列(实战)-摸清RPC骨架中提到过NameServer,在这里我再总结一下NameServer最重要的功能:
-
服务注册与发现。每个远程服务需要在NameServer注册好自己的身份信息与网络地址的对应关系,才能提供生产服务
-
路由策略与负载均衡。一个服务往往对应多台服务器,以集群的方式提供服务,那么以怎样的策略合理分配流量到这些机器是应对高并发的重要条件
-
健康检测。服务器往往会因为各种内外部原因停止服务,此时就不应有流量继续进来,因此通过健康检测机制去“保活”也很重要
而我们在Sparrow-Rpc中没有去实现那么完美的NameServer,而仅仅支持了服务注册与发现,原因是我们可以使用现有的开源框架像Zookeeper为我们做这些事情,当然zk也有一些问题,像RocketMq就自己实现了NameServer机制,本系列教程中弱化了NameServer的开发工作,读者明白它的作用就行,有兴趣可以自行查阅。
ok,首先我们在sparrow-rpc-api模块下创建一个NameServer接口,并定义了三个方法
public interface NameService {
/**
* 服务注册
*
* @param serviceSign
* @param uri
*/
void registerServer(String serviceSign, URI uri);
/**
* 服务下线
*/
void unregisterServer(String serviceSign, URI uri);
/**
* 服务查询
*/
URI seekService(String serviceSign);
}
然后我们实现一个基于Json文件的NameServer,将服务与地址的关系记录持久化到Json中,并编写SPI的配置文件com.sparrow.rpc.api.NameService
com.sparrow.rpc.namesrv.impl.FileNameService
public class FileNameService implements NameService
我们服务注册的方法用到了一个MetaInfo的元信息类,用来存储服务信息,整个方法逻辑很简单就是把服务信息组装成Json写入文件中
@Override
public void registerServer(String serviceSign, URI uri) {
logger.info("Service {} register,uri:{}", serviceSign, uri.toString());
MetaInfo metaInfo = new MetaInfo();
metaInfo.setServiceSign(serviceSign);
metaInfo.setUri(uri);
String jsonMeta = JsonUtil.readJson(metaDataFile);
try {
JsonMetaInfo jsonMetaInfo;
if (jsonMeta.isEmpty()) {
jsonMetaInfo = new JsonMetaInfo();
List<MetaInfo> metaInfos = new ArrayList<>();
metaInfos.add(metaInfo);
jsonMetaInfo.setMetaInfos(metaInfos);
} else {
jsonMetaInfo = JsonUtil.readValue(jsonMeta, JsonMetaInfo.class);
jsonMetaInfo.getMetaInfos().add(metaInfo);
}
JsonUtil.writeJson(metaDataFile, jsonMetaInfo);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
服务发现的方法也很简单,找到第一个与服务签名相同的信息就返回其元信息
@Override
public URI seekService(String serviceSign) {
if (serviceSign.isEmpty()) {
return null;
}
try {
String jsonMeta = JsonUtil.readJson(metaDataFile);
JsonMetaInfo jsonMetaInfo = JsonUtil.readValue(jsonMeta, JsonMetaInfo.class);
for (MetaInfo metaInfo : jsonMetaInfo.getMetaInfos()) {
if (metaInfo.getServiceSign().equals(serviceSign)) {
return metaInfo.getUri();
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
| ServiceHub
在前言中提到,除了在NameServer完成服务注册(UserService对应10.23.4.56),还需要在Netty服务端维护一个服务签名与具体服务类的对应关系(UserService与其对应的Class),它们之间的联系像这样
private Map<String, Object> serviceMp = new ConcurrentHashMap<>();
然后为了将NameServer以及其他Rpc内部功能性方法与具体使用者解耦,我们创建一个RpcAccessor接口,把Rpc服务相关的方法收敛到一起
public interface RpcAccessor extends Closeable {
/**
* 获取NameServer
*
* @return
*/
default NameService getNameService() {
return SpiSupport.load(NameService.class);
}
/**
* 注册RPC服务
*
* @param serviceSign 服务签名
* @param service 服务实例
* @param clazz 服务接口类
* @param <T> 类型
* @return 服务地址
*/
<T> URI addRpcService(String serviceSign, T service, Class<T> clazz);
/**
* 启动RPC服务
*/
Closeable start() throws Exception;
}
然后在sparrow-rpc-core中创建一个实现类NettyRpcAccessor,并维护SPI的配置信息。其中addService就是与服务注册相关的部分
com.sparrow.rpc.core.NettyRpcAccessor
@Override
public <T> URI addRpcService(String serviceSign, T service, Class<T> clazz) {
//服务实例注册
ServiceHub.getInstance().addService(serviceSign, service);
//服务URI注册
NameService nameService = SpiSupport.load(NameService.class);
URI localUri = getLocalUri();
if (Objects.isNull(localUri)) {
throw new IllegalStateException("Get local uri fail");
}
nameService.registerServer(serviceSign, localUri);
return null;
}
| 小结
这一节我们实现了一个低配版的NameServer,如果你想拥有一个工业级的服务注册中心,那么可以采用开源的工具帮助你,你也可以参考开源框架自己实现的注册机制,至少通过这一小节我们知道了NameServer的功能,下一小节我们会讲解动态代理部分的实现,把整个调用链路上最后的代理类去补上。