前言
在微服務大行其道的今天,分散式系統越來越重要,實現服務化首先就要考慮服務之間的通訊問題。這裡面涉及序列化、反序列化、定址、連線等等問題。。不過,有了RPC框架,我們就無需苦惱。
一、什麼是RPC?
RPC(Remote Procedure Call)— 遠端過程呼叫,是一個計算機通訊協議。該協議允許執行於一臺計算機的程式呼叫另一臺計算機的子程式,而程式設計師無需額外地為這個互動作用程式設計。
值得注意是,兩個或多個應用程式都分佈在不同的伺服器上,它們之間的呼叫都像是本地方法呼叫一樣。
RPC框架有很多,比較知名的如阿里的Dubbo、google的gRPC、Go語言的rpcx、Apache的thrift。當然了,還有Spring Cloud,不過對於Spring Cloud來說,RPC只是它的一個功能模組。
複雜的先不講,如果要實現一個基本功能、簡單的RPC,要涉及哪些東西呢?
- 動態代理
- 反射
- 序列化、反序列化
- 網路通訊
- 編解碼
- 服務發現和註冊
- 心跳與鏈路檢測
- ......
下面我們一起通過程式碼來分析,怎麼把這些技術點串到一起,實現我們自己的RPC。
二、環境準備
在開始之前,筆者先介紹一下所用到的軟體環境。
SpringBoot、Netty、zookeeper、zkclient、fastjson
- SpringBoot 專案的基礎框架,方便打成JAR包,便於測試。
- Netty 通訊伺服器
- zookeeper 服務的發現與註冊
- zkclient zookeeper客戶端
- fastjson 序列化、反序列化
三、RPC生產者
1、服務介面API
整個RPC,我們分為生產者和消費者。首先它們有一個共同的服務介面API。在這裡,我們搞一個操作使用者資訊的service介面。
public interface InfoUserService {
List<InfoUser> insertInfoUser(InfoUser infoUser);
InfoUser getInfoUserById(String id);
void deleteInfoUserById(String id);
String getNameById(String id);
Map<String,InfoUser> getAllUser();
}
複製程式碼
2、服務類實現
作為生產者,它當然要有實現類,我們建立InfoUserServiceImpl實現類,並用註解把它標註為RPC的服務,然後註冊到Spring的Bean容器中。在這裡,我們把infoUserMap當做資料庫,儲存使用者資訊。
package com.viewscenes.netsupervisor.service.impl;
@RpcService
public class InfoUserServiceImpl implements InfoUserService {
Logger logger = LoggerFactory.getLogger(this.getClass());
//當做資料庫,儲存使用者資訊
Map<String,InfoUser> infoUserMap = new HashMap<>();
public List<InfoUser> insertInfoUser(InfoUser infoUser) {
logger.info("新增使用者資訊:{}", JSONObject.toJSONString(infoUser));
infoUserMap.put(infoUser.getId(),infoUser);
return getInfoUserList();
}
public InfoUser getInfoUserById(String id) {
InfoUser infoUser = infoUserMap.get(id);
logger.info("查詢使用者ID:{}",id);
return infoUser;
}
public List<InfoUser> getInfoUserList() {
List<InfoUser> userList = new ArrayList<>();
Iterator<Map.Entry<String, InfoUser>> iterator = infoUserMap.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, InfoUser> next = iterator.next();
userList.add(next.getValue());
}
logger.info("返回使用者資訊記錄數:{}",userList.size());
return userList;
}
public void deleteInfoUserById(String id) {
logger.info("刪除使用者資訊:{}",JSONObject.toJSONString(infoUserMap.remove(id)));
}
public String getNameById(String id){
logger.info("根據ID查詢使用者名稱稱:{}",id);
return infoUserMap.get(id).getName();
}
public Map<String,InfoUser> getAllUser(){
logger.info("查詢所有使用者資訊{}",infoUserMap.keySet().size());
return infoUserMap;
}
}
複製程式碼
元註解定義如下:
package com.viewscenes.netsupervisor.annotation;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RpcService {}
複製程式碼
3、請求資訊和返回資訊
所有的請求資訊和返回資訊,我們用兩個JavaBean來表示。其中的重點是,返回資訊要帶有請求資訊的ID。
package com.viewscenes.netsupervisor.entity;
public class Request {
private String id;
private String className;// 類名
private String methodName;// 函式名稱
private Class<?>[] parameterTypes;// 引數型別
private Object[] parameters;// 引數列表
get/set ...
}
複製程式碼
package com.viewscenes.netsupervisor.entity;
public class Response {
private String requestId;
private int code;
private String error_msg;
private Object data;
get/set ...
}
複製程式碼
4、Netty服務端
Netty作為高效能的NIO通訊框架,在很多RPC框架中都有它的身影。我們也採用它當做通訊伺服器。說到這,我們先看個配置檔案,重點有兩個,zookeeper的註冊地址和Netty通訊伺服器的地址。
TOMCAT埠
server.port=8001
#zookeeper註冊地址
registry.address=192.168.245.131:2181,192.168.245.131:2182,192.168.245.131:2183
#RPC服務提供者地址
rpc.server.address=192.168.197.1:18868
複製程式碼
為了方便管理,我們把它也註冊成Bean,同時實現ApplicationContextAware介面,把上面@RpcService註解的服務類撈出來,快取起來,供消費者呼叫。同時,作為伺服器,還要對客戶端的鏈路進行心跳檢測,超過60秒未讀寫資料,關閉此連線。
package com.viewscenes.netsupervisor.netty.server;
@Component
public class NettyServer implements ApplicationContextAware,InitializingBean{
private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);
private static final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private static final EventLoopGroup workerGroup = new NioEventLoopGroup(4);
private Map<String, Object> serviceMap = new HashMap<>();
@Value("${rpc.server.address}")
private String serverAddress;
@Autowired
ServiceRegistry registry;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(RpcService.class);
for(Object serviceBean:beans.values()){
Class<?> clazz = serviceBean.getClass();
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> inter : interfaces){
String interfaceName = inter.getName();
logger.info("載入服務類: {}", interfaceName);
serviceMap.put(interfaceName, serviceBean);
}
}
logger.info("已載入全部服務介面:{}", serviceMap);
}
public void afterPropertiesSet() throws Exception {
start();
}
public void start(){
final NettyServerHandler handler = new NettyServerHandler(serviceMap);
new Thread(() -> {
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup).
channel(NioServerSocketChannel.class).
option(ChannelOption.SO_BACKLOG,1024).
childOption(ChannelOption.SO_KEEPALIVE,true).
childOption(ChannelOption.TCP_NODELAY,true).
childHandler(new ChannelInitializer<SocketChannel>() {
//建立NIOSocketChannel成功後,在進行初始化時,將它的ChannelHandler設定到ChannelPipeline中,用於處理網路IO事件
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new IdleStateHandler(0, 0, 60));
pipeline.addLast(new JSONEncoder());
pipeline.addLast(new JSONDecoder());
pipeline.addLast(handler);
}
});
String[] array = serverAddress.split(":");
String host = array[0];
int port = Integer.parseInt(array[1]);
ChannelFuture cf = bootstrap.bind(host,port).sync();
logger.info("RPC 伺服器啟動.監聽埠:"+port);
registry.register(serverAddress);
//等待服務端監聽埠關閉
cf.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}).start();
}
}
複製程式碼
上面的程式碼就把Netty伺服器啟動了,在處理器中的建構函式中,我們先把服務Bean的Map傳進來,所有的處理要基於這個Map才能找到對應的實現類。在channelRead中,獲取請求方法的資訊,然後通過反射呼叫方法獲取返回值。
package com.viewscenes.netsupervisor.netty.server;
@ChannelHandler.Sharable
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
private final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);
private final Map<String, Object> serviceMap;
public NettyServerHandler(Map<String, Object> serviceMap) {
this.serviceMap = serviceMap;
}
public void channelActive(ChannelHandlerContext ctx) {
logger.info("客戶端連線成功!"+ctx.channel().remoteAddress());
}
public void channelInactive(ChannelHandlerContext ctx) {
logger.info("客戶端斷開連線!{}",ctx.channel().remoteAddress());
ctx.channel().close();
}
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Request request = JSON.parseObject(msg.toString(),Request.class);
if ("heartBeat".equals(request.getMethodName())) {
logger.info("客戶端心跳資訊..."+ctx.channel().remoteAddress());
}else{
logger.info("RPC客戶端請求介面:"+request.getClassName()+" 方法名:"+request.getMethodName());
Response response = new Response();
response.setRequestId(request.getId());
try {
Object result = this.handler(request);
response.setData(result);
} catch (Throwable e) {
e.printStackTrace();
response.setCode(1);
response.setError_msg(e.toString());
logger.error("RPC Server handle request error",e);
}
ctx.writeAndFlush(response);
}
}
/**
* 通過反射,執行本地方法
* @param request
* @return
* @throws Throwable
*/
private Object handler(Request request) throws Throwable{
String className = request.getClassName();
Object serviceBean = serviceMap.get(className);
if (serviceBean!=null){
Class<?> serviceClass = serviceBean.getClass();
String methodName = request.getMethodName();
Class<?>[] parameterTypes = request.getParameterTypes();
Object[] parameters = request.getParameters();
Method method = serviceClass.getMethod(methodName, parameterTypes);
method.setAccessible(true);
return method.invoke(serviceBean, getParameters(parameterTypes,parameters));
}else{
throw new Exception("未找到服務介面,請檢查配置!:"+className+"#"+request.getMethodName());
}
}
/**
* 獲取引數列表
* @param parameterTypes
* @param parameters
* @return
*/
private Object[] getParameters(Class<?>[] parameterTypes,Object[] parameters){
if (parameters==null || parameters.length==0){
return parameters;
}else{
Object[] new_parameters = new Object[parameters.length];
for(int i=0;i<parameters.length;i++){
new_parameters[i] = JSON.parseObject(parameters[i].toString(),parameterTypes[i]);
}
return new_parameters;
}
}
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)throws Exception {
if (evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent)evt;
if (event.state()== IdleState.ALL_IDLE){
logger.info("客戶端已超過60秒未讀寫資料,關閉連線.{}",ctx.channel().remoteAddress());
ctx.channel().close();
}
}else{
super.userEventTriggered(ctx,evt);
}
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.info(cause.getMessage());
ctx.close();
}
}
複製程式碼
4、服務註冊
我們啟動了Netty通訊伺服器,並且把服務實現類載入到快取,等待請求時呼叫。這一步,我們要進行服務註冊。為了簡單化處理,我們只註冊通訊伺服器的監聽地址即可。
在上面程式碼中,bind之後我們執行了registry.register(serverAddress);
它的作用就是,將Netty監聽的IP埠註冊到zookeeper。
package com.viewscenes.netsupervisor.registry;
@Component
public class ServiceRegistry {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Value("${registry.address}")
private String registryAddress;
private static final String ZK_REGISTRY_PATH = "/rpc";
public void register(String data) {
if (data != null) {
ZkClient client = connectServer();
if (client != null) {
AddRootNode(client);
createNode(client, data);
}
}
}
//連線zookeeper
private ZkClient connectServer() {
ZkClient client = new ZkClient(registryAddress,20000,20000);
return client;
}
//建立根目錄/rpc
private void AddRootNode(ZkClient client){
boolean exists = client.exists(ZK_REGISTRY_PATH);
if (!exists){
client.createPersistent(ZK_REGISTRY_PATH);
logger.info("建立zookeeper主節點 {}",ZK_REGISTRY_PATH);
}
}
//在/rpc根目錄下,建立臨時順序子節點
private void createNode(ZkClient client, String data) {
String path = client.create(ZK_REGISTRY_PATH + "/provider", data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
logger.info("建立zookeeper資料節點 ({} => {})", path, data);
}
}
複製程式碼
有一點需要注意,子節點必須是臨時節點。這樣,生產者端停掉之後,才能通知到消費者,把此服務從服務列表中剔除。到此為止,生產者端已經完成。我們看一下它的啟動日誌:
載入服務類: com.viewscenes.netsupervisor.service.InfoUserService
已載入全部服務介面:{com.viewscenes.netsupervisor.service.InfoUserService=com.viewscenes.netsupervisor.service.impl.InfoUserServiceImpl@46cc127b}
Initializing ExecutorService 'applicationTaskExecutor'
Tomcat started on port(s): 8001 (http) with context path ''
Started RpcProviderApplication in 2.003 seconds (JVM running for 3.1)
RPC 伺服器啟動.監聽埠:18868
Starting ZkClient event thread.
Socket connection established to 192.168.245.131/192.168.245.131:2183, initiating session
Session establishment complete on server 192.168.245.131/192.168.245.131:2183, sessionid = 0x367835b48970010, negotiated timeout = 4000
zookeeper state changed (SyncConnected)
建立zookeeper主節點 /rpc
建立zookeeper資料節點 (/rpc/provider0000000000 => 192.168.197.1:28868)
複製程式碼
四、RPC消費者
首先,我們需要把生產者端的服務介面API,即InfoUserService。以相同的目錄放到消費者端。路徑不同,呼叫會找不到的哦。
1、代理
RPC的目標其中有一條,《程式設計師無需額外地為這個互動作用程式設計。》所以,我們在呼叫的時候,就像呼叫本地方法一樣。就像下面這樣:
@Controller
public class IndexController {
@Autowired
InfoUserService userService;
@RequestMapping("getById")
@ResponseBody
public InfoUser getById(String id){
logger.info("根據ID查詢使用者資訊:{}",id);
return userService.getInfoUserById(id);
}
}
複製程式碼
那麼,問題來了。消費者端並沒有此介面的實現,怎麼呼叫到的呢?這裡,首先就是代理。筆者這裡用的是Spring的工廠Bean機制建立的代理物件,涉及的程式碼較多,就不在文章中體現了,如果有不懂的同學,請想象一下,MyBatis中的Mapper介面怎麼被呼叫的。可以參考筆者文章:Mybatis原始碼分析(四)mapper介面方法是怎樣被呼叫到的
總之,在呼叫userService方法的時候,會呼叫到代理物件的invoke方法。在這裡,封裝請求資訊,然後呼叫Netty的客戶端方法傳送訊息。然後根據方法返回值型別,轉成相應的物件返回。
package com.viewscenes.netsupervisor.configurer.rpc;
@Component
public class RpcFactory<T> implements InvocationHandler {
@Autowired
NettyClient client;
Logger logger = LoggerFactory.getLogger(this.getClass());
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Request request = new Request();
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameters(args);
request.setParameterTypes(method.getParameterTypes());
request.setId(IdUtil.getId());
Object result = client.send(request);
Class<?> returnType = method.getReturnType();
Response response = JSON.parseObject(result.toString(), Response.class);
if (response.getCode()==1){
throw new Exception(response.getError_msg());
}
if (returnType.isPrimitive() || String.class.isAssignableFrom(returnType)){
return response.getData();
}else if (Collection.class.isAssignableFrom(returnType)){
return JSONArray.parseArray(response.getData().toString(),Object.class);
}else if(Map.class.isAssignableFrom(returnType)){
return JSON.parseObject(response.getData().toString(),Map.class);
}else{
Object data = response.getData();
return JSONObject.parseObject(data.toString(), returnType);
}
}
}
複製程式碼
2、服務發現
在生產者端,我們把服務IP埠都註冊到zookeeper中,所以這裡,我們要去拿到服務地址,然後通過Netty連線。重要的是,還要對根目錄進行監聽子節點變化,這樣隨著生產者的上線和下線,消費者端可以及時感知。
package com.viewscenes.netsupervisor.connection;
@Component
public class ServiceDiscovery {
@Value("${registry.address}")
private String registryAddress;
@Autowired
ConnectManage connectManage;
// 服務地址列表
private volatile List<String> addressList = new ArrayList<>();
private static final String ZK_REGISTRY_PATH = "/rpc";
private ZkClient client;
Logger logger = LoggerFactory.getLogger(this.getClass());
@PostConstruct
public void init(){
client = connectServer();
if (client != null) {
watchNode(client);
}
}
//連線zookeeper
private ZkClient connectServer() {
ZkClient client = new ZkClient(registryAddress,30000,30000);
return client;
}
//監聽子節點資料變化
private void watchNode(final ZkClient client) {
List<String> nodeList = client.subscribeChildChanges(ZK_REGISTRY_PATH, (s, nodes) -> {
logger.info("監聽到子節點資料變化{}",JSONObject.toJSONString(nodes));
addressList.clear();
getNodeData(nodes);
updateConnectedServer();
});
getNodeData(nodeList);
logger.info("已發現服務列表...{}", JSONObject.toJSONString(addressList));
updateConnectedServer();
}
//連線生產者端服務
private void updateConnectedServer(){
connectManage.updateConnectServer(addressList);
}
private void getNodeData(List<String> nodes){
logger.info("/rpc子節點資料為:{}", JSONObject.toJSONString(nodes));
for(String node:nodes){
String address = client.readData(ZK_REGISTRY_PATH+"/"+node);
addressList.add(address);
}
}
}
複製程式碼
其中,connectManage.updateConnectServer(addressList);
就是根據服務地址,去連線生產者端的Netty服務。然後建立一個Channel列表,在傳送訊息的時候,從中選取一個Channel和生產者端進行通訊。
3、Netty客戶端
Netty客戶端有兩個方法比較重要,一個是根據IP埠連線伺服器,返回Channel,加入到連線管理器;一個是用Channel傳送請求資料。同時,作為客戶端,空閒的時候還要往服務端傳送心跳資訊。
package com.viewscenes.netsupervisor.netty.client;
@Component
public class NettyClient {
Logger logger = LoggerFactory.getLogger(this.getClass());
private EventLoopGroup group = new NioEventLoopGroup(1);
private Bootstrap bootstrap = new Bootstrap();
@Autowired
NettyClientHandler clientHandler;
@Autowired
ConnectManage connectManage;
public Object send(Request request) throws InterruptedException{
Channel channel = connectManage.chooseChannel();
if (channel!=null && channel.isActive()) {
SynchronousQueue<Object> queue = clientHandler.sendRequest(request,channel);
Object result = queue.take();
return JSONArray.toJSONString(result);
}else{
Response res = new Response();
res.setCode(1);
res.setError_msg("未正確連線到伺服器.請檢查相關配置資訊!");
return JSONArray.toJSONString(res);
}
}
public Channel doConnect(SocketAddress address) throws InterruptedException {
ChannelFuture future = bootstrap.connect(address);
Channel channel = future.sync().channel();
return channel;
}
....其他方法略
}
複製程式碼
我們必須重點關注send方法,它是在代理物件invoke方法呼叫到的。首先從聯結器中輪詢選擇一個Channel,然後傳送資料。但是,Netty是非同步操作,我們還要轉為同步,就是說要等待生產者端返回資料才往下執行。筆者在這裡用的是同步佇列SynchronousQueue,它的take方法會阻塞在這裡,直到裡面有資料可讀。然後在處理器中,拿到返回資訊寫到佇列中,take方法返回。
package com.viewscenes.netsupervisor.netty.client;
@Component
@ChannelHandler.Sharable
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Autowired
NettyClient client;
@Autowired
ConnectManage connectManage;
Logger logger = LoggerFactory.getLogger(this.getClass());
private ConcurrentHashMap<String,SynchronousQueue<Object>> queueMap = new ConcurrentHashMap<>();
public void channelActive(ChannelHandlerContext ctx) {
logger.info("已連線到RPC伺服器.{}",ctx.channel().remoteAddress());
}
public void channelInactive(ChannelHandlerContext ctx) {
InetSocketAddress address =(InetSocketAddress) ctx.channel().remoteAddress();
logger.info("與RPC伺服器斷開連線."+address);
ctx.channel().close();
connectManage.removeChannel(ctx.channel());
}
public void channelRead(ChannelHandlerContext ctx, Object msg)throws Exception {
Response response = JSON.parseObject(msg.toString(),Response.class);
String requestId = response.getRequestId();
SynchronousQueue<Object> queue = queueMap.get(requestId);
queue.put(response);
queueMap.remove(requestId);
}
public SynchronousQueue<Object> sendRequest(Request request,Channel channel) {
SynchronousQueue<Object> queue = new SynchronousQueue<>();
queueMap.put(request.getId(), queue);
channel.writeAndFlush(request);
return queue;
}
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)throws Exception {
logger.info("已超過30秒未與RPC伺服器進行讀寫操作!將傳送心跳訊息...");
if (evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent)evt;
if (event.state()== IdleState.ALL_IDLE){
Request request = new Request();
request.setMethodName("heartBeat");
ctx.channel().writeAndFlush(request);
}
}else{
super.userEventTriggered(ctx,evt);
}
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
logger.info("RPC通訊伺服器發生異常.{}",cause);
ctx.channel().close();
}
}
複製程式碼
至此,消費者端也基本完成。同樣的,我們先看一下啟動日誌:
Waiting for keeper state SyncConnected
Opening socket connection to server 192.168.139.129/192.168.139.129:2181. Will not attempt to authenticate using SASL (unknown error)
Socket connection established to 192.168.139.129/192.168.139.129:2181, initiating session
Session establishment complete on server 192.168.139.129/192.168.139.129:2181, sessionid = 0x100000273ba002c, negotiated timeout = 20000
zookeeper state changed (SyncConnected)
/rpc子節點資料為:["provider0000000015"]
已發現服務列表...["192.168.100.74:18868"]
加入Channel到連線管理器./192.168.100.74:18868
已連線到RPC伺服器./192.168.100.74:18868
Initializing ExecutorService 'applicationTaskExecutor'
Tomcat started on port(s): 7002 (http) with context path ''
Started RpcConsumerApplication in 4.218 seconds (JVM running for 5.569)
複製程式碼
五、測試
我們以Controller裡面的兩個方法為例,先開啟100個執行緒呼叫insertInfoUser方法,然後開啟1000個執行緒呼叫查詢方法getAllUser。
public class IndexController {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
InfoUserService userService;
@RequestMapping("insert")
@ResponseBody
public List<InfoUser> getUserList() throws InterruptedException {
long start = System.currentTimeMillis();
int thread_count = 100;
CountDownLatch countDownLatch = new CountDownLatch(thread_count);
for (int i=0;i<thread_count;i++){
new Thread(() -> {
InfoUser infoUser = new InfoUser(IdUtil.getId(),"Jeen","BeiJing");
List<InfoUser> users = userService.insertInfoUser(infoUser);
logger.info("返回使用者資訊記錄:{}", JSON.toJSONString(users));
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
logger.info("執行緒數:{},執行時間:{}",thread_count,(end-start));
return null;
}
@RequestMapping("getAllUser")
@ResponseBody
public Map<String,InfoUser> getAllUser() throws InterruptedException {
long start = System.currentTimeMillis();
int thread_count = 1000;
CountDownLatch countDownLatch = new CountDownLatch(thread_count);
for (int i=0;i<thread_count;i++){
new Thread(() -> {
Map<String, InfoUser> allUser = userService.getAllUser();
logger.info("查詢所有使用者資訊:{}",JSONObject.toJSONString(allUser));
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
logger.info("執行緒數:{},執行時間:{}",thread_count,(end-start));
return null;
}
}
複製程式碼
結果如下:
六、總結
本文簡單介紹了RPC的整個流程,如果你正在學習RPC的相關知識,可以根據文中的例子,自己實現一遍。相信寫完之後,你會對RPC會有更深一些的認識。
生產者端流程:
- 載入服務,並快取
- 啟動通訊伺服器(Netty)
- 服務註冊(把通訊地址放入zookeeper,也可以把載入到的服務也放進去)
- 反射,本地呼叫
消費者端流程:
- 代理服務介面
- 服務發現(連線zookeeper,拿到服務地址列表)
- 遠端呼叫(輪詢生產者服務列表,傳送訊息)
限於篇幅,本文程式碼並不完整,如有需要,訪問:https://github.com/taoxun/simple_rpc 或者新增筆者微信公眾號:<清幽之地的部落格>),獲取完整專案。