目錄
一. RPC是什麼
RPC全稱remote procedure call,翻譯過來就是遠程過程調用。在分佈式系統中,一個模塊像調用本地方法一樣調用遠程方法的過程,就叫RPC。
我們耳熟能詳的webservice、restful接口調用都是RPC,只是消息的組織方式和消息協議不同。
爲了加深對RPC的理解,我手寫了一個簡單的RPC框架,完整的代碼已上傳至https://github.com/RingWu2020/ym-rpc
二.RPC流程
RPC的流程大致如上圖所示:
- 客戶端調用client stub(client stub位於本地,就和調用本地方法一樣),傳遞參數
- clientstub將參數編組爲消息,然後通過系統調用向服務的發送消息;
- 客戶端本地操作系統將消息從客戶端機器發送到服務端機器;
- 服務的操作系統將接收到的數據包傳遞給Server stub;
- Server stub解組消息爲參數;
- Server stub再調用服務端的過程,過程執行結果以相同的方式回傳給客戶端。
三. RPC協議
涉及到通信,自然需要協議,比如tcp、udp、http、ftp、sftp等等;相信有很多小夥伴也馬上聯想到了netty,事實上,很多RPC框架都用到了netty。
常用的RPC協議有SOAP、XML-RPC、JSON-RPC、JSON-WSP,傳統的webservice框架apache cxf、apache axis2等大多基於標準的SOAP協議。而很多新興的框架,譬如dubbo支持多種協議。
四. 手寫RPC框架
在瞭解了RPC的基本原理後,嘗試來寫一個RPC框架。一方面可以加深對RPC的理解,另外一方面,也可以發現RPC過程中一些細節上的問題,對於閱讀開源RPC框架的源碼很有幫助。
在開發前,需要說明的是,用戶在使用RPC框架的時候,過程接口的定義,接口的調用,接口的實現都是用戶去完成的。
客戶端代碼開發
結合前面的RPC流程,rpc客戶端需要完成的功能的有:
- 生成接口的代理對象
- 從註冊中心發現服務
- 將請求編組爲消息發送到服務端
1.生成接口的代理對象
這裏使用jdk的動態代理生成接口的代理對象,生成代理對象的工廠類,代碼如下
package cn.wym.rpc.client;
import cn.wym.rpc.discovery.ServiceInfoDiscoverer;
import cn.wym.rpc.discovery.ZookeeperServiceInfoDiscoverer;
import lombok.Getter;
import lombok.Setter;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
@Getter@Setter
public class ClientStubProxyFactory {
//服務發現
private ServiceInfoDiscoverer serviceInfoDiscoverer= new ZookeeperServiceInfoDiscoverer();
//代理對象緩存,避免每次都新建
private Map<Class<?>, Object> objectCache = new HashMap<Class<?>, Object>();
//通信客戶端,用於發送請求
private NetClient netClient = new NettyClient();
public <T> T getProxy(Class<T> interf) {
T obj = (T) this.objectCache.get(interf);
if (obj == null) {
obj = (T) Proxy.newProxyInstance(interf.getClassLoader(), new Class<?>[] { interf },
new ClientStubInvocationHandler(interf, serviceInfoDiscoverer, netClient));
this.objectCache.put(interf, obj);
}
return obj;
}
}
package cn.wym.rpc.client;
import cn.wym.rpc.common.Request;
import cn.wym.rpc.common.Response;
import cn.wym.rpc.discovery.ServiceInfo;
import cn.wym.rpc.discovery.ServiceInfoDiscoverer;
import cn.wym.rpc.protocol.JSONRpcPRotocol;
import cn.wym.rpc.protocol.RpcProtocol;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Random;
public class ClientStubInvocationHandler implements InvocationHandler {
private Class<?> interf;
private ServiceInfoDiscoverer serviceInfoDiscoverer;
private NetClient netClient;
private Random random = new Random();
public <T> ClientStubInvocationHandler(Class<T> interf, ServiceInfoDiscoverer serviceInfoDiscoverer, NetClient netClient) {
this.interf = interf;
this.serviceInfoDiscoverer = serviceInfoDiscoverer;
this.netClient = netClient;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("toString")) {
return proxy.getClass().toString();
}
if (method.getName().equals("hashCode")) {
return 0;
}
//根據名稱去註冊中心找到對應的服務
String serviceName = method.getName();
List<ServiceInfo> serviceInfos = serviceInfoDiscoverer.getServiceInfo(serviceName);
//緣分負載均衡
ServiceInfo serviceInfo = serviceInfos.get(random.nextInt(serviceInfos.size()));
//TODO: 將請求編組爲消息發送到服務端,並讀取服務端返回的結果
}
}
2.從註冊中心發現服務
這裏使用zookeeper作爲註冊中心,從zookeeper上讀取服務端的信息,包括 服務名稱、協議、服務端地址;在上面的工廠類中,ServiceInfoDiscoverer就是服務發現類,相關代碼如下:
@Getter@Setter
public class ServiceInfo {
private String name;
private String protocol;
private String address;
}
package cn.wym.rpc.discovery;
import java.util.List;
public interface ServiceInfoDiscoverer {
List<ServiceInfo> getServiceInfo(String name);
}
package cn.wym.rpc.discovery;
import com.alibaba.fastjson.JSON;
import org.I0Itec.zkclient.ZkClient;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
public class ZookeeperServiceInfoDiscoverer implements ServiceInfoDiscoverer{
ZkClient client;
private String centerRootPath = "/ym-rpc";
public ZookeeperServiceInfoDiscoverer() {
//示例代碼,配置信息直接寫代碼裏了,實際應該寫在配置文件裏
String addr = "10.18.51.105:2181";
client = new ZkClient(addr);
client.setZkSerializer(new DefaultZkSerializer());
}
public List<ServiceInfo> getServiceInfo(String name) {
String servicePath = centerRootPath + "/" + name + "/service";
List<String> children = client.getChildren(servicePath);
List<ServiceInfo> resources = new ArrayList<ServiceInfo>();
for (String ch : children) {
try {
String deCh = URLDecoder.decode(ch, "UTF-8");
ServiceInfo r = JSON.parseObject(deCh, ServiceInfo.class);
resources.add(r);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return resources;
}
}
package cn.wym.rpc.discovery;
import org.I0Itec.zkclient.exception.ZkMarshallingError;
import org.I0Itec.zkclient.serialize.ZkSerializer;
public class DefaultZkSerializer implements ZkSerializer {
public byte[] serialize(Object o) throws ZkMarshallingError {
return String.valueOf(o).getBytes();
}
public Object deserialize(byte[] bytes) throws ZkMarshallingError {
return new String(bytes);
}
}
3.將請求編組爲消息發送到服務端
涉及到服務器之間通信,毫無疑問,直接上netty;協議的話,簡單地來個json;
package cn.wym.rpc.protocol;
import cn.wym.rpc.common.Request;
import cn.wym.rpc.common.Response;
public interface RpcProtocol {
//編碼請求
byte[] marshallingRequest(Request req) throws Exception;
//解碼請求
Request unmarshallingRequest(byte[] data) throws Exception;
//編碼響應
byte[] marshallingResponse(Response rsp) throws Exception;
//解碼響應
Response unmarshallingResponse(byte[] data) throws Exception;
}
package cn.wym.rpc.protocol;
import cn.wym.rpc.common.Request;
import cn.wym.rpc.common.Response;
import com.alibaba.fastjson.JSONObject;
public class JSONRpcPRotocol implements RpcProtocol {
public byte[] marshallingRequest(Request req) throws Exception {
return JSONObject.toJSONBytes(req);
}
public Request unmarshallingRequest(byte[] data) throws Exception {
return JSONObject.parseObject(data, Request.class);
}
public byte[] marshallingResponse(Response rsp) throws Exception {
return JSONObject.toJSONBytes(rsp);
}
public Response unmarshallingResponse(byte[] data) throws Exception {
return JSONObject.parseObject(data, Response.class);
}
}
發送請求的客戶端的代碼如下:
package cn.wym.rpc.client;
import cn.wym.rpc.discovery.ServiceInfo;
public interface NetClient {
byte[] sendRequest(byte[] data, ServiceInfo sinfo) throws Throwable;
}
package cn.wym.rpc.client;
import cn.wym.rpc.discovery.ServiceInfo;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient implements NetClient {
public byte[] sendRequest(byte[] data, ServiceInfo sinfo) throws Throwable {
String[] addInfoArray = sinfo.getAddress().split(":");
final NettySendHandler sendHandler = new NettySendHandler(data);
byte[] respData = null;
// 配置客戶端
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(sendHandler);
}
});
// 啓動客戶端連接
b.connect(addInfoArray[0], Integer.valueOf(addInfoArray[1])).sync();
respData = (byte[]) sendHandler.rspData();
} finally {
// 釋放線程組資源
group.shutdownGracefully();
}
return respData;
}
}
package cn.wym.rpc.client;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
@Slf4j
public class NettySendHandler extends ChannelInboundHandlerAdapter {
private CountDownLatch cdl = null;
private Object readMsg = null;
private byte[] data;
public NettySendHandler(byte[] data) {
cdl = new CountDownLatch(1);
this.data = data;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("連接服務端成功:" + ctx);
ByteBuf reqBuf = Unpooled.buffer(data.length);
reqBuf.writeBytes(data);
log.info("客戶端發送消息:" + reqBuf);
ctx.writeAndFlush(reqBuf);
}
public Object rspData() {
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return readMsg;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("客戶端讀取到的數據: " + msg);
ByteBuf msgBuf = (ByteBuf) msg;
byte[] resp = new byte[msgBuf.readableBytes()];
msgBuf.readBytes(resp);
readMsg = resp;
cdl.countDown();
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
log.error("發生異常:" + cause.getMessage());
ctx.close();
}
}
服務端代碼開發
服務端需要完成的功能有:
- 向註冊中心註冊服務
- 監聽端口,接收來自客戶端的請求
- 調用用戶的實現類處理請求,結果回傳給客戶端
1. 向註冊中心註冊服務
將服務的信息(地址、端口、接口名稱、支持的協議)寫入到zookeeper
package cn.wym.rpc.server.registry;
public interface ServiceRegister {
void register(ServiceObject so, String protocol, int port) throws Exception;
ServiceObject getServiceObject(String name) throws Exception;
}
package cn.wym.rpc.server.registry;
import cn.wym.rpc.discovery.DefaultZkSerializer;
import cn.wym.rpc.discovery.ServiceInfo;
import cn.wym.rpc.util.PropertiesUtils;
import com.alibaba.fastjson.JSON;
import org.I0Itec.zkclient.ZkClient;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
/**
* Zookeeper方式獲取遠程服務信息類。
*
* ZookeeperServiceInfoDiscoverer
*/
public class ZookeeperExportServiceRegister implements ServiceRegister {
private ZkClient client;
private String centerRootPath = "/ym-rpc";
private Map<String, ServiceObject> serviceMap = new HashMap<String, ServiceObject>();
public ServiceObject getServiceObject(String name) {
return this.serviceMap.get(name);
}
public ZookeeperExportServiceRegister() {
String addr = PropertiesUtils.getProperties("zk.address");
client = new ZkClient(addr);
client.setZkSerializer(new DefaultZkSerializer());
}
public void register(ServiceObject so, String protocolName, int port) throws Exception {
if (so == null) {
throw new IllegalArgumentException("參數不能爲空");
}
this.serviceMap.put(so.getName(), so);
ServiceInfo soInf = new ServiceInfo();
String host = InetAddress.getLocalHost().getHostAddress();
String address = host + ":" + port;
soInf.setAddress(address);
soInf.setName(so.getInterf().getName());
soInf.setProtocol(protocolName);
this.exportService(soInf);
}
private void exportService(ServiceInfo serviceResource) {
String serviceName = serviceResource.getName();
String uri = JSON.toJSONString(serviceResource);
try {
uri = URLEncoder.encode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String servicePath = centerRootPath + "/" + serviceName + "/service";
if (!client.exists(servicePath)) {
client.createPersistent(servicePath, true);
}
String uriPath = servicePath + "/" + uri;
if (client.exists(uriPath)) {
client.delete(uriPath);
}
client.createEphemeral(uriPath);
}
}
package cn.wym.rpc.server.registry;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter@Setter@AllArgsConstructor
public class ServiceObject {
private String name;
private Class<?> interf;
private Object obj;
}
2.監聽端口,接收來自客戶端的請求
通過netty監聽固定端口來接收客戶端的請求,並使用ChannelHandler來處理請求
package cn.wym.rpc.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Getter@Setter@Slf4j
public class NettyRpcServer {
protected int port;
protected String protocol;
protected RequestHandler handler;
private Channel channel;
public NettyRpcServer(int port, String protocol, RequestHandler handler) {
this.port = port;
this.protocol = protocol;
this.handler = handler;
}
public void start() {
// 配置服務器
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new ChannelRequestHandler(handler));
}
});
// 啓動服務
ChannelFuture f = b.bind(port).sync();
log.info("完成服務端端口綁定與啓動");
channel = f.channel();
// 等待服務通道關閉
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放線程組資源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public void stop() {
this.channel.close();
}
}
package cn.wym.rpc.server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
@Slf4j
public class ChannelRequestHandler extends ChannelInboundHandlerAdapter {
private RequestHandler handler;
public ChannelRequestHandler(RequestHandler handler) {
this.handler = handler;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("激活");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("服務端收到消息:" + msg);
ByteBuf msgBuf = (ByteBuf) msg;
byte[] req = new byte[msgBuf.readableBytes()];
msgBuf.readBytes(req);
byte[] res = handler.handleRequest(req);
log.info("發送響應:" + msg);
ByteBuf respBuf = Unpooled.buffer(res.length);
respBuf.writeBytes(res);
ctx.write(respBuf);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error(ExceptionUtils.getStackTrace(cause));
ctx.close();
}
}
3.調用用戶的實現類處理請求,結果回傳給客戶端
package cn.wym.rpc.server;
import cn.wym.rpc.common.Request;
import cn.wym.rpc.common.Response;
import cn.wym.rpc.common.Status;
import cn.wym.rpc.protocol.RpcProtocol;
import cn.wym.rpc.server.registry.ServiceObject;
import cn.wym.rpc.server.registry.ServiceRegister;
import lombok.Getter;
import lombok.Setter;
import java.lang.reflect.Method;
@Getter@Setter
public class RequestHandler {
private RpcProtocol protocol;
private ServiceRegister serviceRegister;
public RequestHandler(RpcProtocol protocol, ServiceRegister serviceRegister) {
super();
this.protocol = protocol;
this.serviceRegister = serviceRegister;
}
public byte[] handleRequest(byte[] data) throws Exception {
// 1、解組消息
Request req = this.protocol.unmarshallingRequest(data);
// 2、查找服務對象
ServiceObject so = this.serviceRegister.getServiceObject(req.getServiceName());
Response rsp = null;
if (so == null) {
rsp = new Response(Status.NOT_FOUND);
} else {
// 3、反射調用對應的過程方法
try {
Method m = so.getInterf().getMethod(req.getMethod(), req.getParameterTypes());
Object returnValue = m.invoke(so.getObj(), req.getParameters());
rsp = new Response(Status.SUCCESS);
rsp.setReturnValue(returnValue);
} catch (Exception e) {
rsp = new Response(Status.ERROR);
rsp.setException(e);
}
}
// 4、編組響應消息
return this.protocol.marshallingResponse(rsp);
}
}
五. 驗證
寫一個Provider,Consumer,還有一個DemoService的接口,分別啓動兩個provider實例,監聽端口8080,8079,然後運行consumer,查看服務調用情況