基於Zookeeper+Thrift的RPC動態服務註冊發現和調用(Java)

一 、介紹一下使用到的框架類工具以及pom文件

(1) ZK封裝非常好的框架類: Curator (可以先去學習)

(2)  Thrift文件編譯工具, 本人安裝的老版本 Thrift Compiler (0.9.3) 

(3) ZK UI(可以忽略)

(4) pom文件

            <dependency>
                <groupId>org.apache.curator</groupId>
                <artifactId>curator-framework</artifactId>
                <version>4.0.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.curator</groupId>
                <artifactId>curator-recipes</artifactId>
                <version>4.0.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.curator</groupId>
                <artifactId>curator-x-discovery</artifactId>
                <version>4.0.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.curator</groupId>
                <artifactId>curator-test</artifactId>
                <version>4.0.0</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.thrift</groupId>
                <artifactId>libthrift</artifactId>
                <version>0.9.3</version>
            </dependency>

二 、相關代碼和工程(代碼內註釋詳解)

(1)ZK 客戶端 服務類

package com.play.english.cqx.zk;

import org.apache.curator.RetryPolicy;
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.PathChildrenCacheListener;
import org.apache.curator.retry.RetryForever;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.curator.utils.CloseableUtils;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.ACL;

import java.util.List;

/**
 * @author chaiqx on 2019/12/3
 */
public class CqxZk implements AutoCloseable {

    //zk 服務名稱
    private String name;

    //zk 服務器連接字符串
    private String zkConnectedStr;

    //封裝好的client
    private CuratorFramework client;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getZkConnectedStr() {
        return zkConnectedStr;
    }

    public void setZkConnectedStr(String zkConnectedStr) {
        this.zkConnectedStr = zkConnectedStr;
    }

    public CuratorFramework getClient() {
        return client;
    }

    public void setClient(CuratorFramework client) {
        this.client = client;
    }


    /**
     * 自定義一個異常捕獲處理器,只是打印暫時無別的操作
     */
    private Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println(name + ": " + e);
        }
    };

    /**
     * 構造函數,啓動zk客戶端和連接zk服務器
     *
     * @param name
     * @param zkConnectedStr
     */
    public CqxZk(String name, String zkConnectedStr) {
        try {
            this.name = name;
            this.zkConnectedStr = zkConnectedStr;
            RetryPolicy retryPolicy = new RetryForever(10000);
            this.client = CuratorFrameworkFactory.builder()
                    .connectString(zkConnectedStr)
                    .retryPolicy(retryPolicy)
                    .sessionTimeoutMs(30 * 1000)
                    .connectionTimeoutMs(30 * 1000)
                    .maxCloseWaitMs(60 * 1000)
                    .threadFactory(new ThreadFactoryBuilder().setNameFormat(name + "-%d").setUncaughtExceptionHandler(uncaughtExceptionHandler).build())
                    .build();
            this.client.start();
            this.client.blockUntilConnected();
            System.out.println(String.format("cqx zk :  %s started.", this.zkConnectedStr));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new CqxZkException(e);
        }
    }

    /**
     * 添加節點
     *
     * @param path
     * @param value
     * @param mode
     * @return
     */
    public String add(String path, byte[] value, CreateMode mode) {
        try {
            return this.client.create().creatingParentsIfNeeded().withMode(mode).forPath(path, value);
        } catch (Exception e) {
            throw new CqxZkException(e);
        }
    }

    /**
     * 添加節點
     *
     * @param path
     * @param value
     * @param mode
     * @param aclList
     * @return
     */
    public String add(String path, byte[] value, CreateMode mode, List<ACL> aclList) {
        try {
            return this.client.create().creatingParentsIfNeeded().withMode(mode).withACL(aclList).forPath(path, value);
        } catch (Exception e) {
            throw new CqxZkException(e);
        }
    }

    /**
     * 判斷節點是否存在
     *
     * @param path
     * @return
     */
    public boolean exist(String path) {
        try {
            return this.client.checkExists().forPath(path) != null;
        } catch (Exception e) {
            System.out.println(String.format("zk check exist error, path = %s , %s", path, e));
            return false;
        }
    }

    /**
     * 移除節點
     *
     * @param path
     */
    public void remove(String path) {
        try {
            this.client.delete().forPath(path);
        } catch (Exception e) {
            throw new CqxZkException(e);
        }
    }

    /**
     * 設置節點數據
     *
     * @param path
     * @param value
     */
    public void set(String path, byte[] value) {
        try {
            this.client.setData().forPath(path, value);
        } catch (Exception e) {
            throw new CqxZkException(e);
        }
    }

    /**
     * 獲取節點下的所有子節點
     *
     * @param nodePath
     * @return
     */
    public List<String> getChildren(String nodePath) {
        try {
            return this.client.getChildren().forPath(nodePath);
        } catch (Exception e) {
            System.out.println(String.format("get node children failed, nodePath = %s ", nodePath));
        }
        return null;
    }

    /**
     * 註冊目錄監聽器
     *
     * @param nodePath
     * @param listener
     * @return
     */
    public PathChildrenCache registerPathChildrenListener(String nodePath, PathChildrenCacheListener listener) {
        try {
            //創建一個PathChildrenCache
            PathChildrenCache pathChildrenCache = new PathChildrenCache(this.client, nodePath, true);
            //添加子目錄監視器
            pathChildrenCache.getListenable().addListener(listener);
            //啓動監聽器
            pathChildrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
            //返回PathChildrenCache
            return pathChildrenCache;
        } catch (Exception e) {
            System.out.println(String.format("register path children node cache listener failed, nodePath = %s ", nodePath));
        }
        return null;
    }

    @Override
    public void close() throws Exception {
        System.out.println(String.format("cqx zk %s - %s closed", this.name, this.zkConnectedStr));
        CloseableUtils.closeQuietly(this.client);
    }
}

(2) Thrift 源文件Hello.thrift(包名自己自定義)

namespace java com.play.english.cqx.thrift.thrift

service hello{
       string sayHello(1:i32 id)
}

編譯腳本:(注意執行目錄)

#!/usr/bin/env bash

thrift --gen java -out ../thrift Hello.thrift

執行腳本最終生成java代碼類:Hello.java 

(3) Thrift 服務端 Server類

Thrift  RPC Service 具體實現類:

package com.play.english.cqx.thrift.server;

import com.play.english.cqx.thrift.thrift.Hello;
import org.apache.thrift.TException;

/**
 * @author chaiqx on 2019/12/9
 */
public class HelloImpl implements Hello.Iface {

    @Override
    public String sayHello(int id) throws TException {
        if (id == 1) {
            return "hello, I am cqx!";
        } else if (id == 2) {
            return "hello, I am cqh!";
        } else {
            return "hello";
        }
    }
}

Thrift PRC Server 服務類:

package com.play.english.cqx.thrift.server;

import com.play.english.cqx.thrift.thrift.Hello;
import com.play.english.cqx.zk.CqxZk;
import org.apache.commons.lang.StringUtils;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadedSelectorServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;
import org.apache.thrift.transport.TTransportException;
import org.apache.zookeeper.CreateMode;

import java.net.InetAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author chaiqx on 2019/12/9
 */
public class CqxThriftServer {

    //RPC所有相關節點公用的頂級父節點
    private static final String THRIFT_SERVER_PREFIX = "/thrift/";
    //固定的一個單線程池
    private ExecutorService thread = Executors.newSingleThreadExecutor();
    //zk客戶端服務實例
    private CqxZk cqxZk;
    //RPC服務名
    private String name;
    //PRC服務端口號
    private int port;

    /**
     * 構造函數,啓動RPC節點並註冊節點到ZK
     *
     * @param port
     * @param name
     * @param cqxZk
     */
    public CqxThriftServer(int port, String name, CqxZk cqxZk) {
        this.cqxZk = cqxZk;
        this.name = name;
        this.port = port;
        this.startAndRegisterService();
    }

    /**
     * 獲取本服務即將註冊到ZK上的節點路徑
     * 根據本機器IP和定義的端口號生成唯一路徑
     *
     * @return
     */
    private String getServiceNodePath() {
        try {
            InetAddress inetAddress = InetAddress.getLocalHost();
            String servicePath = inetAddress.getHostAddress();
            return "/".concat(servicePath).concat(":").concat(String.valueOf(port));
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 註冊RPC服務父節點
     * 比如我們此RPC服務爲hello-server
     * 註冊完之後就是/thrift/hello-server 永久節點
     */
    private void register() {
        if (!cqxZk.exist(THRIFT_SERVER_PREFIX.concat(name))) {
            cqxZk.add(THRIFT_SERVER_PREFIX.concat(name), name.getBytes(), CreateMode.PERSISTENT);
        }
    }

    /**
     * 註冊此RPC服務節點
     * <p>
     * 註冊完之後就是/thrift/hello-server/10.1.38.226:7778 臨時節點
     */
    private void registerService() {
        if (!cqxZk.exist(THRIFT_SERVER_PREFIX.concat(name))) {
            register();
        }
        String serviceNodePath = getServiceNodePath();
        if (StringUtils.isBlank(serviceNodePath)) {
            return;
        }
        cqxZk.add(THRIFT_SERVER_PREFIX.concat(name).concat(serviceNodePath), String.valueOf(port).getBytes(), CreateMode.EPHEMERAL);
    }

    /**
     * 啓動RPC服務
     *
     * @return
     */
    private boolean start() {
        try {
            //構造thrift-server
            TServer server = new TThreadedSelectorServer(new TThreadedSelectorServer.Args(new TNonblockingServerSocket(port))
                    .protocolFactory(new TBinaryProtocol.Factory())
                    .processor(new Hello.Processor(new HelloImpl()))
                    .workerThreads(5)
                    .transportFactory(new TFramedTransport.Factory()));
            //異步線程提交,防止主線程阻塞
            thread.submit(server::serve);
            //一直輪訓等待RPC服務啓動成功,服務正常
            while (!server.isServing()) {
                System.out.println("wait for thrift server start!");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
        } catch (TTransportException e) {
            System.out.println(e);
            return false;
        }
        return true;
    }

    /**
     * 啓動RPC服務並且註冊ZK節點
     */
    private void startAndRegisterService() {
        if (!start()) {
            System.out.println(name.concat("start failed!"));
        }
        registerService();
    }


    public static void main(String[] args) {
        //創建ZK客戶端實例
        CqxZk cqxZk = new CqxZk("test-thrift", "127.0.0.1:2181");
        //異步啓動第一個RPC服務
        new Thread("hello-server-1") {
            @Override
            public void run() {
                new CqxThriftServer(7777, "hello-server", cqxZk);
            }
        }.start();
        //異步啓動第二個RPC服務
        new Thread("hello-server-2") {
            @Override
            public void run() {
                new CqxThriftServer(7778, "hello-server", cqxZk);
            }
        }.start();
    }
}

(4) Thrift 客戶端 Client類

package com.play.english.cqx.thrift.client;

import com.play.english.cqx.thrift.thrift.Hello;
import com.play.english.cqx.zk.CqxZk;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * @author chaiqx on 2019/12/9
 */
public class CqxThriftClient {

    //自己知道自己的PRC服務節點路徑所以寫死
    private static final String NODE_PATH = "/thrift/hello-server";

    //IP和端口號分隔符
    private static final String SPLIT_STR = ":";

    //ZK客戶端實例
    private CqxZk cqxZk;

    //RPC服務連接池,Map本地簡單存儲
    private Map<String, TProtocol> protocolMap = new ConcurrentHashMap<>();

    /**
     * 構造函數,RPC服務發現和RPC服務動態監聽
     *
     * @param cqxZk
     */
    public CqxThriftClient(CqxZk cqxZk) {
        this.cqxZk = cqxZk;
        this.serverDetect();
        this.serverListening();
    }

    /**
     * 獲取一個RPC服務連接
     *
     * @return
     */
    private TProtocol getProtocol() {
        //如果連接池爲空,則返回空
        if (MapUtils.isEmpty(protocolMap)) {
            return null;
        }
        //隨機取出一個RPC服務連接
        List<TProtocol> tmp = new ArrayList<>(new ArrayList<>(protocolMap.values()));
        Collections.shuffle(tmp);
        //打印一下使用的那個連接
        System.out.println(tmp.get(0).toString());
        return tmp.get(0);
    }

    /**
     * 對應zk節點路徑的RPC服務進連接池
     *
     * @param path
     */
    private void inProtocolPool(String path) {
        if (StringUtils.isBlank(path)) {
            return;
        }
        //解析該節點的服務地址
        String[] address = path.split(SPLIT_STR);
        if (ArrayUtils.isEmpty(address) || address.length != 2) {
            return;
        }
        if (protocolMap.containsKey(path)) {
            return;
        }
        try {
            //創建與PRC服務端的連接
            TTransport tTransport = new TSocket(address[0], Integer.parseInt(address[1]));
            TFramedTransport framedTransport = new TFramedTransport(tTransport);
            TProtocol tProtocol = new TBinaryProtocol(framedTransport);
            tTransport.open();
            //進連接池
            protocolMap.put(path, tProtocol);
        } catch (TTransportException e) {
            System.out.println(e);
        }
    }

    /**
     * 對應zk節點路徑的RPC服務出連接池
     *
     * @param path
     */
    private void outProtocolPool(String path) {
        if (!protocolMap.containsKey(path)) {
            return;
        }
        //出連接池
        TProtocol protocol = protocolMap.remove(path);
        //關閉連接
        protocol.getTransport().close();
    }

    /**
     * 發現RPC服務
     */
    private void serverDetect() {
        List<String> childrenNodePaths = cqxZk.getChildren(NODE_PATH);
        if (childrenNodePaths != null) {
            childrenNodePaths.forEach(this::inProtocolPool);
        }
    }

    /**
     * 動態監聽RPC服務
     */
    private void serverListening() {
        //註冊ZK目錄監聽器
        cqxZk.registerPathChildrenListener(NODE_PATH, (curatorClient, event) -> {
            //獲取變化的節點數據
            ChildData childData = event.getData();
            if (childData == null) {
                return;
            }
            switch (event.getType()) {
                case CHILD_ADDED://新增RPC節點
                    System.out.println(String.format("path children add children node %s now", childData.getPath()));
                    //新節點進RPC服務連接池
                    inProtocolPool(childData.getPath().substring(childData.getPath().lastIndexOf("/") + 1));
                    break;
                case CHILD_REMOVED://減少RPC節點
                    System.out.println(String.format("path children delete children node %s now", childData.getPath()));
                    //失去的節點出RPC服務連接池
                    outProtocolPool(childData.getPath().substring(childData.getPath().lastIndexOf("/") + 1));
                    break;
                case CONNECTION_LOST://RPC節點連接丟失
                    System.out.println(String.format("path children connection lost %s now", childData.getPath()));
                    //斷開連接節點出RPC服務連接池
                    outProtocolPool(childData.getPath().substring(childData.getPath().lastIndexOf("/") + 1));
                    break;
                case CONNECTION_RECONNECTED://RPC節點重連
                    System.out.println(String.format("path children connection reconnected %s now", childData.getPath()));
                    //重新連接的節點出RPC服務連接池
                    inProtocolPool(childData.getPath().substring(childData.getPath().lastIndexOf("/") + 1));
                    break;
                default://無操作
                    break;
            }
        });
    }

    /**
     * 客戶端say hello
     *
     * @param id
     * @return
     */
    private String sayHello(int id) {
        //獲取一個RPC服務連接
        TProtocol protocol = this.getProtocol();
        if (protocol == null) {
            return null;
        }
        //創建一個RPC實例
        Hello.Client client = new Hello.Client(protocol);
        try {
            //RPC實際say hello
            return client.sayHello(id);
        } catch (TException e) {
            System.out.println(e);
        }
        return null;
    }

    public static void main(String[] args) throws Exception {
        //ZK客戶端實例
        CqxZk cqxZk = new CqxZk("test-thrift", "127.0.0.1:2181");
        //Thrift 客戶端
        CqxThriftClient cqxThriftClient = new CqxThriftClient(cqxZk);
        //每五秒就打印三次say hello,結果可想而知,使用的rpc服務不是同一個,會隨機選取調用
        while (true) {
            for (int i = 0; i < 3; i++) {
                System.out.println(cqxThriftClient.sayHello(i));
            }
            TimeUnit.SECONDS.sleep(5);
        }
    }
}

(5)結果以及結論

如果先啓動RPC客戶端,再啓動RPC服務端:

 剛開始的時候沒有RPC服務,所以一直say hello 提示是null

然後RPC服務端啓動之後,RPC客戶端監聽到ZK節點變化,然後獲取RPC並連接上RPC服務,然後調用say hello 就有數據了,

而且是兩個RPC節點,所以會不斷的隨機選取一個服務。(如果先啓動RPC服務端然後再啓動RPC客戶端呢?)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章