最近在b站看到一個視頻,可以用來入門rpc(remote procedure call),記錄一下學習的過程,rpc即是一個計算機通信協議,該協議允許運行於一臺計算機的程序調用另一臺計算機的子程序,程序員無需額外地爲這個交互作用編程,如果設計的軟件採用面向對象編程,遠程調用亦可作爲遠程方法調用
大概的流程是消費方以本地方式調用服務,將方法、參數等信息封裝成請求體,並且找到服務地址,將消息發送到服務端,服務端對請求信息進行解碼,調用本地服務,並將結果返回給消費方法,消費方進行解碼並得到最後結果。而透明化遠程服務調用在java方式上面就是通過jdk動態代理,或者字節碼生成如cglib,asm等等,但字節碼生成的代碼編寫難度可能相對較高以及不易維護。
首先明確兩個角色,消費方和服務方,消費方如何拿到服務方的地址?這時候需要引入一個註冊中心的角色,用來註冊服務方的地址,消費方可以從註冊中心裏拿到服務方地址,而且也需要確定通信的格式,比如定義什麼格式的消息頭、消息體,採用什麼樣的序列化和反序列化(用於編碼解碼)方式,這裏通信方式選擇http和nettynio支持的tcp進行遠程調用。
下面開始編碼環節:
分爲五個模塊,這裏不作子模塊劃分了,consumer就是服務調用方,framework是這個簡單的遠程調用框架所需的一些類, protocol是協議的具體實現,provider是服務提供方,register則起到註冊中心的作用
簡單的框架部分
invocation對象
用來封裝所請求的方法信息,而返回結果其實也可以定義一個結果對象,這裏只是string類型
/**
* @author: lele
* @date: 2019/11/15 下午7:01
* 封裝調用方所想調用的遠程方法信息
*/
@Data
@AllArgsConstructor
public class Invocation implements Serializable {
private static final long serialVersionUID = -7789662944864163267L;
private String interfaceName;
private String methodName;
private Object[] params;
//防止重載
private Class[] paramsTypes;
}
目標地址類
/**
* @author: lele
* @date: 2019/11/15 下午6:56
* 自定義的URL類,方便管理
*/
@Data
@AllArgsConstructor
public class URL implements Serializable {
private static final long serialVersionUID = 7947426935048871471L;
private String hostname;
private Integer port;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
URL url = (URL) o;
return Objects.equals(hostname, url.hostname) &&
Objects.equals(port, url.port);
}
@Override
public int hashCode() {
return Objects.hash(hostname, port);
}
}
協議接口
把不同協議抽象
public interface Protocol {
//服務提供方啓動的方法
void start(URL url);
//發送請求
String send(URL url, Invocation invocation);
}
協議工廠
public class ProtocolFactory {
public static HttpProtocol http() {
return new HttpProtocol();
}
public static NettyProtocol netty(){
return new NettyProtocol();
}
}
動態代理工廠
負責對服務接口下的方法進行代理,這個是核心
/**
* @author: lele
* @date: 2019/11/15 下午7:48
* 代理工廠,對傳入的類進行代理對具體執行的方法進行封裝然後發送給服務端進行執行
*/
public class ProxyFactory {
public static <T> T getProxy(Class interfaceClass) {
return (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//指定所用協議
Protocol protocol=ProtocolFactory.http();
//通過註冊中心獲取可用鏈接
URL url= MapRegister.random(interfaceClass.getName());
//封裝方法參數
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(), args, method.getParameterTypes());
//發送請求
String res = protocol.send(url,invocation);
return res;
}
});
}
}
http
通過提供一個內嵌的tomcat服務器作爲服務提供者,並且添加一個servlet,這個servlet對請求進行解碼後,通過反射執行相關的邏輯並返回給調用方
協議具體實現
public class HttpProtocol implements Protocol {
@Override
public void start(URL url) {
HttpServer server=new HttpServer();
server.start(url.getHostname(),url.getPort());
}
@Override
public String send(URL url, Invocation invocation) {
HttpClient client=new HttpClient();
String res = client.post(url.getHostname(), url.getPort(), invocation);
return res;
}
}
服務端
tomcat實例
/**
* @author: lele
* @date: 2019/11/15 下午6:44
* 構造嵌入式tomcat服務器
*/
public class HttpServer {
public void start(String hostname, Integer port) {
Tomcat tomcat = new Tomcat();
//獲取server實例
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
//連接器
Connector connector = new Connector();
connector.setPort(port);
Engine engine = new StandardEngine();
engine.setDefaultHost(hostname);
//host
Host host = new StandardHost();
host.setName(hostname);
String contextPath = "";
//上下文
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
//添加servlet,匹配所有路徑
tomcat.addServlet(contextPath, "dispathcer", new DispatcherServlet());
context.addServletMappingDecoded("/*","dispathcer");
try {
tomcat.start();
tomcat.getServer().await();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
處理servlet的邏輯,而自定義servlet添加該邏輯並配置到內嵌tomcat當中即可完成服務端處理
public class DispatcherServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
new HttpServerHandler().handle(req,resp);
}
}
public class HttpServerHandler {
public void handle(HttpServletRequest req, HttpServletResponse resp){
try {
//獲取輸入流
ServletInputStream inputStream = req.getInputStream();
//包裝成對象輸入流
ObjectInputStream ois=new ObjectInputStream(inputStream);
//轉換成方法調用參數
Invocation invocation= (Invocation) ois.readObject();
String hostAddress = InetAddress.getLocalHost().getHostName();
URL url=new URL(hostAddress,8080);
Class implClass=MapRegister.get(invocation.getInterfaceName(),url);
Method method = implClass.getMethod(invocation.getMethodName(), invocation.getParamsTypes());
String result = (String) method.invoke(implClass.newInstance(), invocation.getParams());
//寫回結果
IOUtils.write(result,resp.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
客戶端
發起請求
public class HttpClient {
public String post(String hostname, Integer port, Invocation invocation) {
try {
URL url = new URL("http", hostname, port, "/");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
OutputStream outputStream = urlConnection.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(invocation);
oos.flush();
oos.close();
InputStream inputStream = urlConnection.getInputStream();
String result = IOUtils.toString(inputStream);
return result;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
return null;
}
}
netty
服務端通過繼承simpleInboundHandler和複寫數據流入的處理方法,同樣的經過對請求參數進行處理通過反射執行相關邏輯返回給調用方,調用方編解碼後保存結果即可。
協議實現
public class NettyProtocol implements Protocol {
@Override
public void start(URL url) {
NettyServer nettyServer=new NettyServer();
try {
nettyServer.start(url.getHostname(),url.getPort());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String send(URL url, Invocation invocation) {
NettyClient nettyClient=new NettyClient();
String res = nettyClient.send(url, invocation);
return res;
}
}
客戶端
@Data
public class NettyClientHandler extends SimpleChannelInboundHandler<String> {
private String result;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
this.result = s;
}
}
public class NettyClient {
public String send(URL url, Invocation invocation) {
//用來保存調用結果的handler
NettyClientHandler res = new NettyClientHandler();
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.group(group)
.channel(NioSocketChannel.class)
//true保證實時性,默認爲false會累積到一定的數據量才發送
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//編碼器
ch.pipeline().addLast(new ObjectEncoder());
//反序列化(解碼)對象時指定類解析器,null表示使用默認的類加載器
ch.pipeline().addLast(new ObjectDecoder(1024 * 64, ClassResolvers.cacheDisabled(null)));
ch.pipeline().addLast(res);
}
});
//connect是異步的,但調用其future的sync則是同步等待連接成功
ChannelFuture future = bootstrap.connect(url.getHostname(), url.getPort()).sync();
System.out.println("鏈接成功!" + "host:" + url.getHostname() + " port:" + url.getPort());
//同步等待調用信息發送成功
future.channel().writeAndFlush(invocation).sync();
//同步等待NettyClientHandler的channelRead0被觸發後(意味着收到了調用結果)關閉連接
future.channel().closeFuture().sync();
return res.getResult();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
return null;
}
}
服務端
啓動服務:
public class NettyServer {
public void start(String hostName,int port) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
//存放已完成三次握手的請求的隊列的最大長度
.option(ChannelOption.SO_BACKLOG, 128)
//啓用心跳保活
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//自帶的對象編碼器
ch.pipeline().addLast(new ObjectEncoder());
//解碼器
ch.pipeline().addLast(new ObjectDecoder(1024 * 64, ClassResolvers.cacheDisabled(null)));
ch.pipeline().addLast(new NettyServerHandler());
}
});
//bind初始化端口是異步的,但調用sync則會同步阻塞等待端口綁定成功
ChannelFuture future = bootstrap.bind(hostName,port).sync();
System.out.println("綁定成功!"+"host:"+hostName+" port:"+port);
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
處理入站消息(消息即invocation類)
public class NettyServerHandler extends SimpleChannelInboundHandler<Invocation> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Invocation invocation) throws Exception {
String hostAddress = InetAddress.getLocalHost().getHostName();
//這裏的port按照本地的端口,可以用其他變量指示
Class serviceImpl= MapRegister.get(invocation.getInterfaceName(),new URL(hostAddress,8080));
Method method=serviceImpl.getMethod(invocation.getMethodName(),invocation.getParamsTypes());
Object result=method.invoke(serviceImpl.newInstance(),invocation.getParams());
System.out.println("結果-------"+result);
//由於操作異步,確保發送消息後才關閉連接
ctx.writeAndFlush(result).addListener(ChannelFutureListener.CLOSE);
// ReferenceCountUtil.release(invocation); 默認實現,如果只是普通的adapter則需要釋放對象
}
}
註冊中心
這裏的實現只是把存放服務信息的類存入文本中,因爲服務端和客戶端是兩個jvm進程,所以對象內存地址也不一樣,需要持久化再取出,而比較通用流行的方式的是使用redis,zookeeper作爲註冊中心,這裏的格式是{接口名:{URL:實現類}}一個接口對應多個map,每個map只有一個key/value,key爲可用的url(服務地址),value(具體的實現類)
public class MapRegister {
//{服務名:{URL:實現類}}
private static Map<String, Map<URL, Class>> REGISTER = new HashMap<>();
//一個接口對應多個map,每個map只有一個key/value,key爲可用的url(服務地址),value(具體的實現類)
public static void register(String interfaceName, URL url, Class implClass) {
Map<URL, Class> map = new HashMap<>();
map.put(url,implClass);
REGISTER.put(interfaceName,map);
saveFile();
}
//這裏直接拿第一個
public static URL random(String interfaceName){
REGISTER=getFile();
return REGISTER.get(interfaceName).keySet().iterator().next();
}
//通過接口名和url尋找具體的實現類,服務端使用
public static Class get(String interfaceName,URL url){
REGISTER=getFile();
return REGISTER.get(interfaceName).get(url);
}
public static Map<String,Map<URL,Class>> getFile(){
FileInputStream fileInputStream= null;
try {
fileInputStream = new FileInputStream(System.getProperty("user.dir")+"/"+"temp.txt");
ObjectInputStream in=new ObjectInputStream(fileInputStream);
Object o = in.readObject();
return (Map<String, Map<URL, Class>>) o;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
private static void saveFile(){
FileOutputStream fileOutputStream= null;
try {
fileOutputStream = new FileOutputStream(System.getProperty("user.dir")+"/"+"temp.txt");
ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(REGISTER);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//消費端
public class Comsumer {
public static void main(String[] args) {
HelloService helloService = ProxyFactory.getProxy(HelloService.class);
String result = helloService.qq();
System.out.println(result);
}
}
//服務提供方
public class Provider {
public static void main(String[] args) throws UnknownHostException {
String hostAddress = InetAddress.getLocalHost().getHostName();
URL url=new URL(hostAddress,8080);
//這裏多個接口的話,都要註冊上去
MapRegister.register(HelloService.class.getName(),url,HelloServiceImpl.class);
Protocol server= ProtocolFactory.http();
server.start(url);
}
}
先啓動provider,再啓動comsumer,可以看到訪問的結果。
主要核心還是通過動態代理代理具體調用的方法,通過tcp或者http等遠程調用其它服務的接口,但這個只能簡單的作爲入門例子,還有很多改進的地方,如結合多線程處理消息,接入spring,更詳細的協議定義,比如返回結果不僅僅是string,註冊中心改爲zookeeper實現等等,項目具體地址——https://github.com/97lele/rpcstudy/tree/master