为什么需要Nacos?

我们现在的RPC框架其实只有一个服务提供者,客户端也是通过固定的一个服务端地址进行访问的,这会存在极大的隐患,如果这个服务提供者挂了或者换了地址,那客户端就没法访问了。
在分布式架构中,有一个重要的组件,就是服务注册中心,它用于保存多个服务提供者的信息,每个服务提供者在启动时都需要向注册中心注册自己所拥有的服务。这样客户端在发起远程调用的时候,就可以直接向注册中心请求服务提供者的信息,如果拿来的这个挂了,还可以重新请求,并且在这种情况下可以很方便地实现负载均衡。
常见的注册中心有Eureka、Zookeeper和Nacos。

安装使用Nacos


引入nacos-client依赖

<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>1.3.0</version>
</dependency>

       首先把整个逻辑先顺一遍,引入Nacos服务注册中心之后,启动服务端,服务端会把服务名(即接口名)和服务端地址注册到Nacos,然后启动客户端,客户端发出调用请求时,会向Nacos请求一个可以提供对应服务的服务端地址,拿到地址后就可以和服务端进行通信了。其次修正一个概念,在前面服务端本地保存的服务类名是叫ServiceRegistry,但现在引入了服务注册中心,为了保证命名与传达的意思保持一致,所以现在将服务端的本地服务类名改为ServiceProvider,而ServiceRegistry则作为Nacos服务注册中心的远程注册表的名字。

dubbo指定naocs注册ip_java

实现Nacos服务注册中心

首先定义ServiceRegistry接口:

public interface ServiceRegistry {

    /**
     * @description 将一个服务注册到注册表
     * @param [ServiceName, inetSocketAddress] 服务名称,提供服务的地址
     * @return [void]
     * @date [2021-03-13 14:44]
     */
    void register(String serviceName, InetSocketAddress inetSocketAddress);

    /**
     * @description 根据服务名查找服务端地址
     * @param [serviceName]
     * @return [java.net.InetSocketAddress] 服务端地址
     * @date [2021-03-13 14:45]
     */
    InetSocketAddress lookupService(String serviceName);

}

实现Nacos服务注册中心的实现类:NacosServiceRegistry,我们也可以使用ZooKeeper作为注册中心,实现接口就可以

public class NacosServiceRegistry implements ServiceRegistry{

    private static final Logger logger = LoggerFactory.getLogger(NacosServiceRegistry.class);

    private static final String SERVER_ADDR = "127.0.0.1:8848";
    private static final NamingService namingService;

    static {
        try {
            //连接Nacos创建命名服务
            namingService = NamingFactory.createNamingService(SERVER_ADDR);
        }catch (NacosException e){
            logger.error("连接Nacos时有错误发生:" + e);
            throw new RpcException(RpcError.FAILED_TO_CONNECT_TO_SERVICE_REGISTRY);
        }
    }

    @Override
    public void register(String serviceName, InetSocketAddress inetSocketAddress) {
        try {
            //向Nacos注册服务
            namingService.registerInstance(serviceName, inetSocketAddress.getHostName(), inetSocketAddress.getPort());
        }catch (NacosException e) {
            logger.error("注册服务时有错误发生" + e);
            throw new RpcException(RpcError.REGISTER_SERVICE_FAILED);
        }
    }

    @Override
    public InetSocketAddress lookupService(String serviceName) {
        try {
            //利用列表获取某个服务的所有提供者
            List<Instance> instances = namingService.getAllInstances(serviceName);
            Instance instance = instances.get(0);
            return new InetSocketAddress(instance.getIp(), instance.getPort());
        }catch (NacosException e) {
            logger.error("获取服务时有错误发生" + e);
        }
        return null;
    }
}

可以发现Nacos的使用还是比较简单,在lookupService()方法中,通过getAllInstance()获取到某个服务的所有提供者,然后需要选择一个,这里就涉及到负载均衡策略,后面再讲,这里我们先选择第0个。

服务端注册服务到Nacos

修改RpcServer接口,新增一个方法publishService(),用于向Nacos注册服务,接着实现这个方法就可以了,以NettyServer的实现为例,首先在构造方法中创建一个ServiceRegistry:

public NettyServer(String host, int port) {
        this.host = host;
        this.port = port;
        serviceRegistry = new NacosServiceRegistry();
        serviceProvider = new ServiceProviderImpl();
}

然后实现publishService()方法,这里的实现是注册完一个服务后直接调用start()方法,这是个不太好的实现……导致一个服务端只能注册一个服务,之后会进行调整。

/**
     * @description 将服务保存到服务端本地注册表,同时注册到Nacos注册中心
     */
    @Override
    public <T> void publishService(Object service, Class<T> serviceClass) {
        if (serializer == null) {
            logger.error("未设置序列化器");
            throw new RpcException(RpcError.SERIALIZER_NOT_FOUND);
        }
        serviceProvider.addServiceProvider(service);
        serviceRegistry.register(serviceClass.getCanonicalName(), new InetSocketAddress(host, port));
        start();
    }

客户端发现服务

以NettyClient为例,之前创建NettyClient时,需要传入host和port,现在这个host和port是从Nacos服务注册中心中获取的,sendRequest修改如下(重点在最后两行代码):

public Object sendRequest(RpcRequest rpcRequest) {
        if (serializer == null) {
            logger.error("未设置序列化器");
            throw new RpcException(RpcError.SERIALIZER_NOT_FOUND);
        }
        //保证自定义实体类变量的原子性和共享性的线程安全,此处应用于rpcResponse
        AtomicReference<Object> result = new AtomicReference<>(null);
        try {
            //从Nacos获取提供对应服务的服务端地址
            InetSocketAddress inetSocketAddress = serviceRegistry.lookupService(rpcRequest.getInterfaceName());
            //创建Netty通道连接
            Channel channel = ChannelProvider.get(inetSocketAddress, serializer);
……

测试

NettyTestClient如下,构造RpcClient时不再需要传入地址和端口:

public class NettyTestClient {
    public static void main(String[] args) {
        RpcClient client = new NettyClient();
        client.setSerializer(new ProtostuffSerializer());
        RpcClientProxy rpcClientProxy = new RpcClientProxy(client);
        HelloService helloService = rpcClientProxy.getProxy(HelloService.class);
        HelloObject object = new HelloObject(12, "this is netty style");
        String res = helloService.hello(object);
        System.out.println(res);
    }
}

NettyTestServer如下,这里把之前的start()写在了publishService中,实际应当分离,否则只能注册一个服务:

public class NettyTestServer {
    public static void main(String[] args) {
        HelloService helloService = new HelloServiceImpl();
        NettyServer server = new NettyServer("127.0.0.1", 9999);
        server.setSerializer(new ProtostuffSerializer());
        server.publishService(helloService, HelloService.class);
    }
}

startup.cmd启动Nacos,然后启动服务端和客户端,会得到和之前一样的结果。
这里如果通过设置不同的端口号,启动两个服务端的话,会看到即使客户端多次调用,也只是由同一个服务端提供服务,这是因为在NacosServiceRegistry中,我们直接选择了服务列表的第0个,后面实现负载均衡的时候会作出修改。

本节over……