一 、介紹一下使用到的框架類工具以及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客戶端呢?)