RPC(remote procedure call)遠端過程呼叫
RPC是為了在分散式應用中,兩臺主機的Java程式進行通訊,當A主機呼叫B主機的方法時,過程簡潔,就像是呼叫自己程式裡的方法一樣。
RPC框架的職責就是,封裝好底層呼叫的細節,客戶端只要呼叫方法,就能夠獲取服務提供者的響應,方便開發者編寫程式碼。
RPC底層使用的是TCP協議,服務端和客戶端和點對點通訊。
作用
在RPC的應用場景中,客戶端呼叫服務端的程式碼
客戶端需要有相應的api介面,將方法名、方法引數型別、具體引數等等都傳送給服務端
服務端需要有方法的具體實現,在接收到客戶端的請求後,根據資訊呼叫對應的方法,並返回響應給客戶端
流程圖演示
程式碼實現
首先客戶端要知道服務端的介面,然後封裝一個請求物件,傳送給服務端
要呼叫一個方法需要有:方法名、方法引數型別、具體引數、執行方法的類名
@Data public class RpcRequest { private String methodName; private String className; private Class[] paramType; private Object[] args; }
由服務端返回給客戶端的響應(方法呼叫結果)也使用一個物件進行封裝
@Data public class RpcResponse { private int code; private Object result; }
- 如果是在多執行緒呼叫中,需要具體把每個響應返回給對應的請求,可以加一個ID進行標識
將物件通過網路傳輸,需要先進行序列化操作,這裡使用的是jackson工具
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.11.4</version> </dependency>
public class JsonSerialization { private static ObjectMapper objectMapper = new ObjectMapper(); static { objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS); objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } public static byte[] serialize(Object output) throws JsonProcessingException { byte[] bytes = objectMapper.writeValueAsBytes(output); return bytes; } public static Object deserialize(byte[] input,Class clazz) throws IOException { Object parse = objectMapper.readValue(input,clazz); return parse; } }
- 在反序列化過程中,需要指定要轉化的型別,而服務端接收request,客戶端接收response,二者型別是不一樣的,所以在後續傳輸時指定型別
有了需要傳輸的資料後,使用Netty開啟網路服務進行傳輸
服務端
繫結埠號,開啟連線
public class ServerNetty { public static void connect(int port) throws InterruptedException { EventLoopGroup workGroup = new NioEventLoopGroup(); EventLoopGroup bossGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.channel(NioServerSocketChannel.class) .group(bossGroup,workGroup) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { /** * 加入自定義協議的資料處理器,指定接收到的資料型別 * 加入服務端處理器 */ ch.pipeline().addLast(new NettyProtocolHandler(RpcRequest.class)); ch.pipeline().addLast(new ServerHandler()); } }); bootstrap.bind(port).sync(); } }
Netty中繫結了兩個資料處理器
一個是資料處理器,服務端接收到請求->呼叫方法->返回響應,這些過程都在資料處理器中執行
public class ServerHandler extends SimpleChannelInboundHandler { @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { RpcRequest rpcRequest = (RpcRequest)msg; // 獲取使用反射需要的各個引數 String methodName = rpcRequest.getMethodName(); Class[] paramTypes = rpcRequest.getParamType(); Object[] args = rpcRequest.getArgs(); String className = rpcRequest.getClassName(); //從註冊中心容器中獲取物件 Object object = Server.hashMap.get(className); Method method = object.getClass().getMethod(methodName,paramTypes); //反射呼叫方法 String result = (String) method.invoke(object,args); // 將響應結果封裝好後傳送回去 RpcResponse rpcResponse = new RpcResponse(); rpcResponse.setCode(200); rpcResponse.setResult(result); ctx.writeAndFlush(rpcResponse); } }
- 這裡從hash表中獲取物件,有一個預先進行的操作:將有可能被遠端呼叫的物件放入容器中,等待使用
一個是自定義的TCP協議處理器,為了解決TCP的常見問題:因為客戶端傳送的資料包和服務端接收資料緩衝區之間,大小不匹配導致的粘包、拆包問題。
/** * 網路傳輸的自定義TCP協議 * 傳送時:為傳輸的位元組流新增兩個魔數作為頭部,再計算資料的長度,將資料長度也新增到頭部,最後才是資料 * 接收時:識別出兩個魔數後,下一個就是首部,最後使用長度對應的位元組陣列接收資料 */ public class NettyProtocolHandler extends ChannelDuplexHandler { private static final byte[] MAGIC = new byte[]{0x15,0x66}; private Class decodeType; public NettyProtocolHandler() { } public NettyProtocolHandler(Class decodeType){ this.decodeType = decodeType; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf in = (ByteBuf) msg; //接收響應物件 Object dstObject; byte[] header = new byte[2]; in.readBytes(header); byte[] lenByte = new byte[4]; in.readBytes(lenByte); int len = ByteUtils.Bytes2Int_BE(lenByte); byte[] object = new byte[len]; in.readBytes(object); dstObject = JsonSerialization.deserialize(object, decodeType); //交給下一個資料處理器 ctx.fireChannelRead(dstObject); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { ByteBuf byteBuf = Unpooled.buffer(); //寫入魔數 byteBuf.writeBytes(MAGIC); byte[] object = JsonSerialization.serialize(msg); //資料長度轉化為位元組陣列並寫入 int len = object.length; byte[] bodyLen = ByteUtils.int2bytes(len); byteBuf.writeBytes(bodyLen); //寫入物件 byteBuf.writeBytes(object); ctx.writeAndFlush(byteBuf); } }
- 這個資料處理器是服務端和客戶端都要使用的,就相當於是一個雙方定好傳輸資料要遵守的協議
- 在這裡進行了物件的序列化和反序列化,所以反序列化型別在這個處理器中指定
- 這裡面要將資料的長度傳送,需一個將整數型別轉化為位元組型別的工具
轉化資料工具類
public class ByteUtils { /** short2\u5B57\u8282\u6570\u7EC4 */ public static byte[] short2bytes(short v) { byte[] b = new byte[4]; b[1] = (byte) v; b[0] = (byte) (v >>> 8); return b; } /** int4\u5B57\u8282\u6570\u7EC4 */ public static byte[] int2bytes(int v) { byte[] b = new byte[4]; b[3] = (byte) v; b[2] = (byte) (v >>> 8); b[1] = (byte) (v >>> 16); b[0] = (byte) (v >>> 24); return b; } /** long8\u5B57\u8282\u6570\u7EC4 */ public static byte[] long2bytes(long v) { byte[] b = new byte[8]; b[7] = (byte) v; b[6] = (byte) (v >>> 8); b[5] = (byte) (v >>> 16); b[4] = (byte) (v >>> 24); b[3] = (byte) (v >>> 32); b[2] = (byte) (v >>> 40); b[1] = (byte) (v >>> 48); b[0] = (byte) (v >>> 56); return b; } /** \u5B57\u8282\u6570\u7EC4\u8F6C\u5B57\u7B26\u4E32 */ public static String bytesToHexString(byte[] bs) { if (bs == null || bs.length == 0) { return null; } StringBuffer sb = new StringBuffer(); String tmp = null; for (byte b : bs) { tmp = Integer.toHexString(Byte.toUnsignedInt(b)); if (tmp.length() < 2) { sb.append(0); } sb.append(tmp); } return sb.toString(); } /** * @return */ public static int Bytes2Int_BE(byte[] bytes) { if(bytes.length < 4){ return -1; } int iRst = (bytes[0] << 24) & 0xFF; iRst |= (bytes[1] << 16) & 0xFF; iRst |= (bytes[2] << 8) & 0xFF; iRst |= bytes[3] & 0xFF; return iRst; } /** * long\u8F6C8\u5B57\u8282\u6570\u7EC4 */ public static long bytes2long(byte[] b) { ByteBuffer buffer = ByteBuffer.allocate(8); buffer.put(b, 0, b.length); buffer.flip();// need flip return buffer.getLong(); } }
客戶端
將Netty的操作封裝了起來,最後返回一個Channle型別,由它進行傳送資料的操作
public class ClientNetty { public static Channel connect(String host,int port) throws InterruptedException { InetSocketAddress address = new InetSocketAddress(host,port); EventLoopGroup workGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap(); bootstrap.channel(NioSocketChannel.class) .group(workGroup) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //自定義協議handler(客戶端接收的是response) ch.pipeline().addLast(new NettyProtocolHandler(RpcResponse.class)); //處理資料handler ch.pipeline().addLast(new ClientHandler()); } }); Channel channel = bootstrap.connect(address).sync().channel(); return channel; } }
資料處理器負責接收response,並將響應結果放入在future中,future的使用在後續的動態代理中
public class ClientHandler extends SimpleChannelInboundHandler { @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { RpcResponse rpcResponse = (RpcResponse) msg; //服務端正常情況返回碼為200 if(rpcResponse.getCode() != 200){ throw new Exception(); } //將結果放到future裡 RPCInvocationHandler.future.complete(rpcResponse.getResult()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); } }
要讓客戶端在呼叫遠端方法時像呼叫本地方法一樣,就需要一個代理物件,供客戶端呼叫,讓代理物件去呼叫服務端的實現。
代理物件構造
public class ProxyFactory { public static Object getProxy(Class<?>[] interfaces){ return Proxy.newProxyInstance(ProxyFactory.class.getClassLoader(), interfaces, new RPCInvocationHandler()); } }
客戶端代理物件的方法執行
將request傳送給服務端後,一直阻塞,等到future裡面有了結果為止。
public class RPCInvocationHandler implements InvocationHandler { static public CompletableFuture future; static Channel channel; static { future = new CompletableFuture(); //開啟netty網路服務 try { channel = ClientNetty.connect("127.0.0.1",8989); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { RpcRequest rpcRequest = new RpcRequest(); rpcRequest.setArgs(args); rpcRequest.setMethodName(method.getName()); rpcRequest.setParamType(method.getParameterTypes()); rpcRequest.setClassName(method.getDeclaringClass().getSimpleName()); channel.writeAndFlush(rpcRequest); //一個阻塞操作,等待網路傳輸的結果 String result = (String) future.get(); return result; } }
- 這裡用static修飾future和channle,沒有考慮到客戶端去連線多個服務端和多次遠端呼叫
- 可以使用一個hash表,儲存與不同服務端對應的channle,每次呼叫時從hash表中獲取即可
- 用hash表儲存與不同request對應的future,每個響應的結果與之對應
客戶端
要進行遠端呼叫需要擁有的介面
public interface OrderService { public String buy(); }
預先的操作和測試程式碼
public class Client { static OrderService orderService; public static void main(String[] args) throws InterruptedException { //建立一個代理物件給進行遠端呼叫的類 orderService = (OrderService) ProxyFactory.getProxy(new Class[]{OrderService.class}); String result = orderService.buy(); System.out.println(result); } }
服務端
要接受遠端呼叫需要擁有的具體實現類
public class OrderImpl implements OrderService { public OrderImpl() { } @Override public String buy() { System.out.println("呼叫buy方法"); return "呼叫buy方法成功"; } }
預先操作和測試程式碼
public class Server { public static HashMap<String ,Object> hashMap = new HashMap<>(); public static void main(String[] args) throws InterruptedException { //開啟netty網路服務 ServerNetty.connect(8989); //提前將需要開放的服務註冊到hash表中 hashMap.put("OrderService",new OrderImpl()); } }
執行結果