最近在b站看到一个视频,可以用来入门rpc(remote procedure call),记录一下学习的过程,rpc即是一个计算机通信协议,该协议允许运行于一台计算机的程序调用另一台计算机的子程序,程序员无需额外地为这个交互作用编程,如果设计的软件采用面向对象编程,远程调用亦可作为远程方法调用
大概的流程是消费方以本地方式调用服务,将方法、参数等信息封装成请求体,并且找到服务地址,将消息发送到服务端,服务端对请求信息进行解码,调用本地服务,并将结果返回给消费方法,消费方进行解码并得到最后结果。而透明化远程服务调用在java方式上面就是通过jdk动态代理,或者字节码生成如cglib,asm等等,但字节码生成的代码编写难度可能相对较高以及不易维护。
首先明确两个角色,消费方和服务方,消费方如何拿到服务方的地址?这时候需要引入一个注册中心的角色,用来注册服务方的地址,消费方可以从注册中心里拿到服务方地址,而且也需要确定通信的格式,比如定义什么格式的消息头、消息体,采用什么样的序列化和反序列化(用于编码解码)方式,这里通信方式选择http和nettynio支持的tcp进行远程调用。
下面开始编码环节:
分为五个模块,这里不作子模块划分了,consumer就是服务调用方,framework是这个简单的远程调用框架所需的一些类, protocol是协议的具体实现,provider是服务提供方,register则起到注册中心的作用
简单的框架部分
invocation对象
用来封装所请求的方法信息,而返回结果其实也可以定义一个结果对象,这里只是string类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1/**
2 * @author: lele
3 * @date: 2019/11/15 下午7:01
4 * 封装调用方所想调用的远程方法信息
5 */
6@Data
7@AllArgsConstructor
8public class Invocation implements Serializable {
9
10 private static final long serialVersionUID = -7789662944864163267L;
11 private String interfaceName;
12
13 private String methodName;
14 private Object[] params;
15 //防止重载
16 private Class[] paramsTypes;
17}
18
目标地址类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 1/**
2 * @author: lele
3 * @date: 2019/11/15 下午6:56
4 * 自定义的URL类,方便管理
5 */
6@Data
7@AllArgsConstructor
8public class URL implements Serializable {
9
10 private static final long serialVersionUID = 7947426935048871471L;
11 private String hostname;
12 private Integer port;
13
14 @Override
15 public boolean equals(Object o) {
16 if (this == o) return true;
17 if (o == null || getClass() != o.getClass()) return false;
18 URL url = (URL) o;
19 return Objects.equals(hostname, url.hostname) &&
20 Objects.equals(port, url.port);
21 }
22
23 @Override
24 public int hashCode() {
25
26 return Objects.hash(hostname, port);
27 }
28}
29
协议接口
把不同协议抽象
1
2
3
4
5
6
7
8
9
10 1
2public interface Protocol {
3
4 //服务提供方启动的方法
5 void start(URL url);
6
7 //发送请求
8 String send(URL url, Invocation invocation);
9}
10
协议工厂
1
2
3
4
5
6
7
8
9
10 1public class ProtocolFactory {
2 public static HttpProtocol http() {
3 return new HttpProtocol();
4 }
5 public static NettyProtocol netty(){
6 return new NettyProtocol();
7 }
8}
9
10
动态代理工厂
负责对服务接口下的方法进行代理,这个是核心
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 1/**
2 * @author: lele
3 * @date: 2019/11/15 下午7:48
4 * 代理工厂,对传入的类进行代理对具体执行的方法进行封装然后发送给服务端进行执行
5 */
6public class ProxyFactory {
7
8 public static <T> T getProxy(Class interfaceClass) {
9 return (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
10 @Override
11 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
12 //指定所用协议
13 Protocol protocol=ProtocolFactory.http();
14 //通过注册中心获取可用链接
15 URL url= MapRegister.random(interfaceClass.getName());
16 //封装方法参数
17 Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(), args, method.getParameterTypes());
18 //发送请求
19 String res = protocol.send(url,invocation);
20 return res;
21 }
22 });
23
24 }
25}
26
http
通过提供一个内嵌的tomcat服务器作为服务提供者,并且添加一个servlet,这个servlet对请求进行解码后,通过反射执行相关的逻辑并返回给调用方
协议具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1public class HttpProtocol implements Protocol {
2 @Override
3 public void start(URL url) {
4 HttpServer server=new HttpServer();
5 server.start(url.getHostname(),url.getPort());
6 }
7
8 @Override
9 public String send(URL url, Invocation invocation) {
10 HttpClient client=new HttpClient();
11 String res = client.post(url.getHostname(), url.getPort(), invocation);
12 return res;
13 }
14}
15
服务端
tomcat实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 1
2/**
3 * @author: lele
4 * @date: 2019/11/15 下午6:44
5 * 构造嵌入式tomcat服务器
6 */
7public class HttpServer {
8 public void start(String hostname, Integer port) {
9
10 Tomcat tomcat = new Tomcat();
11 //获取server实例
12 Server server = tomcat.getServer();
13 Service service = server.findService("Tomcat");
14
15 //连接器
16 Connector connector = new Connector();
17 connector.setPort(port);
18
19 Engine engine = new StandardEngine();
20 engine.setDefaultHost(hostname);
21 //host
22 Host host = new StandardHost();
23 host.setName(hostname);
24 String contextPath = "";
25 //上下文
26 Context context = new StandardContext();
27 context.setPath(contextPath);
28 context.addLifecycleListener(new Tomcat.FixContextListener());
29
30 host.addChild(context);
31 engine.addChild(host);
32
33 service.setContainer(engine);
34 service.addConnector(connector);
35 //添加servlet,匹配所有路径
36 tomcat.addServlet(contextPath, "dispathcer", new DispatcherServlet());
37 context.addServletMappingDecoded("/*","dispathcer");
38 try {
39 tomcat.start();
40 tomcat.getServer().await();
41
42 } catch (LifecycleException e) {
43 e.printStackTrace();
44 }
45 }
46}
47
48
处理servlet的逻辑,而自定义servlet添加该逻辑并配置到内嵌tomcat当中即可完成服务端处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 1public class DispatcherServlet extends HttpServlet {
2 @Override
3 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
4 new HttpServerHandler().handle(req,resp);
5 }
6}
7
8
9public class HttpServerHandler {
10 public void handle(HttpServletRequest req, HttpServletResponse resp){
11 try {
12 //获取输入流
13 ServletInputStream inputStream = req.getInputStream();
14 //包装成对象输入流
15 ObjectInputStream ois=new ObjectInputStream(inputStream);
16 //转换成方法调用参数
17 Invocation invocation= (Invocation) ois.readObject();
18 String hostAddress = InetAddress.getLocalHost().getHostName();
19 URL url=new URL(hostAddress,8080);
20 Class implClass=MapRegister.get(invocation.getInterfaceName(),url);
21 Method method = implClass.getMethod(invocation.getMethodName(), invocation.getParamsTypes());
22 String result = (String) method.invoke(implClass.newInstance(), invocation.getParams());
23 //写回结果
24 IOUtils.write(result,resp.getOutputStream());
25 } catch (IOException e) {
26 e.printStackTrace();
27 } catch (ClassNotFoundException e) {
28 e.printStackTrace();
29 } catch (NoSuchMethodException e) {
30 e.printStackTrace();
31 } catch (IllegalAccessException e) {
32 e.printStackTrace();
33 } catch (InstantiationException e) {
34 e.printStackTrace();
35 } catch (InvocationTargetException e) {
36 e.printStackTrace();
37 }
38
39 }
40}
41
客户端
发起请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 1public class HttpClient {
2 public String post(String hostname, Integer port, Invocation invocation) {
3 try {
4 URL url = new URL("http", hostname, port, "/");
5 HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
6 urlConnection.setRequestMethod("POST");
7 urlConnection.setDoOutput(true);
8 OutputStream outputStream = urlConnection.getOutputStream();
9 ObjectOutputStream oos = new ObjectOutputStream(outputStream);
10 oos.writeObject(invocation);
11 oos.flush();
12 oos.close();
13 InputStream inputStream = urlConnection.getInputStream();
14 String result = IOUtils.toString(inputStream);
15 return result;
16 } catch (MalformedURLException e) {
17 e.printStackTrace();
18 } catch (IOException e) {
19 e.printStackTrace();
20 } finally {
21 }
22 return null;
23 }
24}
25
netty
服务端通过继承simpleInboundHandler和复写数据流入的处理方法,同样的经过对请求参数进行处理通过反射执行相关逻辑返回给调用方,调用方编解码后保存结果即可。
协议实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 1public class NettyProtocol implements Protocol {
2 @Override
3 public void start(URL url) {
4 NettyServer nettyServer=new NettyServer();
5 try {
6 nettyServer.start(url.getHostname(),url.getPort());
7 } catch (InterruptedException e) {
8 e.printStackTrace();
9 }
10 }
11
12 @Override
13 public String send(URL url, Invocation invocation) {
14 NettyClient nettyClient=new NettyClient();
15 String res = nettyClient.send(url, invocation);
16 return res;
17 }
18}
19
20
客户端
1
2
3
4
5
6
7
8
9
10 1@Data
2public class NettyClientHandler extends SimpleChannelInboundHandler<String> {
3 private String result;
4
5 @Override
6 protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
7 this.result = s;
8 }
9}
10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 1public class NettyClient {
2
3
4 public String send(URL url, Invocation invocation) {
5 //用来保存调用结果的handler
6 NettyClientHandler res = new NettyClientHandler();
7 NioEventLoopGroup group = new NioEventLoopGroup();
8 Bootstrap bootstrap = new Bootstrap();
9 try {
10 bootstrap.group(group)
11 .channel(NioSocketChannel.class)
12 //true保证实时性,默认为false会累积到一定的数据量才发送
13 .option(ChannelOption.TCP_NODELAY, true)
14 .handler(new ChannelInitializer<SocketChannel>() {
15 @Override
16 protected void initChannel(SocketChannel ch) throws Exception {
17 //编码器
18 ch.pipeline().addLast(new ObjectEncoder());
19 //反序列化(解码)对象时指定类解析器,null表示使用默认的类加载器
20 ch.pipeline().addLast(new ObjectDecoder(1024 * 64, ClassResolvers.cacheDisabled(null)));
21 ch.pipeline().addLast(res);
22
23 }
24 });
25 //connect是异步的,但调用其future的sync则是同步等待连接成功
26 ChannelFuture future = bootstrap.connect(url.getHostname(), url.getPort()).sync();
27 System.out.println("链接成功!" + "host:" + url.getHostname() + " port:" + url.getPort());
28 //同步等待调用信息发送成功
29 future.channel().writeAndFlush(invocation).sync();
30 //同步等待NettyClientHandler的channelRead0被触发后(意味着收到了调用结果)关闭连接
31 future.channel().closeFuture().sync();
32 return res.getResult();
33
34 } catch (InterruptedException e) {
35 e.printStackTrace();
36 } finally {
37 group.shutdownGracefully();
38 }
39 return null;
40 }
41}
42
服务端
启动服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 1public class NettyServer {
2 public void start(String hostName,int port) throws InterruptedException {
3 NioEventLoopGroup boss = new NioEventLoopGroup();
4 NioEventLoopGroup worker = new NioEventLoopGroup();
5 ServerBootstrap bootstrap = new ServerBootstrap();
6 try {
7 bootstrap.group(boss, worker)
8 .channel(NioServerSocketChannel.class)
9 //存放已完成三次握手的请求的队列的最大长度
10 .option(ChannelOption.SO_BACKLOG, 128)
11 //启用心跳保活
12 .childOption(ChannelOption.SO_KEEPALIVE, true)
13 .childHandler(new ChannelInitializer<SocketChannel>() {
14 @Override
15 protected void initChannel(SocketChannel ch) throws Exception {
16 //自带的对象编码器
17 ch.pipeline().addLast(new ObjectEncoder());
18 //解码器
19 ch.pipeline().addLast(new ObjectDecoder(1024 * 64, ClassResolvers.cacheDisabled(null)));
20 ch.pipeline().addLast(new NettyServerHandler());
21 }
22 });
23 //bind初始化端口是异步的,但调用sync则会同步阻塞等待端口绑定成功
24 ChannelFuture future = bootstrap.bind(hostName,port).sync();
25 System.out.println("绑定成功!"+"host:"+hostName+" port:"+port);
26 future.channel().closeFuture().sync();
27 } catch (Exception e) {
28 e.printStackTrace();
29 }finally {
30 boss.shutdownGracefully();
31 worker.shutdownGracefully();
32 }
33 }
34
35}
36
处理入站消息(消息即invocation类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1public class NettyServerHandler extends SimpleChannelInboundHandler<Invocation> {
2
3
4 @Override
5 protected void channelRead0(ChannelHandlerContext ctx, Invocation invocation) throws Exception {
6 String hostAddress = InetAddress.getLocalHost().getHostName();
7 //这里的port按照本地的端口,可以用其他变量指示
8 Class serviceImpl= MapRegister.get(invocation.getInterfaceName(),new URL(hostAddress,8080));
9 Method method=serviceImpl.getMethod(invocation.getMethodName(),invocation.getParamsTypes());
10 Object result=method.invoke(serviceImpl.newInstance(),invocation.getParams());
11 System.out.println("结果-------"+result);
12 //由于操作异步,确保发送消息后才关闭连接
13 ctx.writeAndFlush(result).addListener(ChannelFutureListener.CLOSE);
14// ReferenceCountUtil.release(invocation); 默认实现,如果只是普通的adapter则需要释放对象
15 }
16}
17
注册中心
这里的实现只是把存放服务信息的类存入文本中,因为服务端和客户端是两个jvm进程,所以对象内存地址也不一样,需要持久化再取出,而比较通用流行的方式的是使用redis,zookeeper作为注册中心,这里的格式是{接口名:{URL:实现类}}一个接口对应多个map,每个map只有一个key/value,key为可用的url(服务地址),value(具体的实现类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 1
2public class MapRegister {
3
4
5 //{服务名:{URL:实现类}}
6 private static Map<String, Map<URL, Class>> REGISTER = new HashMap<>();
7
8 //一个接口对应多个map,每个map只有一个key/value,key为可用的url(服务地址),value(具体的实现类)
9 public static void register(String interfaceName, URL url, Class implClass) {
10 Map<URL, Class> map = new HashMap<>();
11 map.put(url,implClass);
12 REGISTER.put(interfaceName,map);
13 saveFile();
14 }
15
16//这里直接拿第一个
17 public static URL random(String interfaceName){
18 REGISTER=getFile();
19 return REGISTER.get(interfaceName).keySet().iterator().next();
20 }
21//通过接口名和url寻找具体的实现类,服务端使用
22 public static Class get(String interfaceName,URL url){
23
24 REGISTER=getFile();
25 return REGISTER.get(interfaceName).get(url);
26 }
27
28public static Map<String,Map<URL,Class>> getFile(){
29 FileInputStream fileInputStream= null;
30 try {
31 fileInputStream = new FileInputStream(System.getProperty("user.dir")+"/"+"temp.txt");
32 ObjectInputStream in=new ObjectInputStream(fileInputStream);
33 Object o = in.readObject();
34 return (Map<String, Map<URL, Class>>) o;
35 } catch (FileNotFoundException e) {
36 e.printStackTrace();
37 } catch (IOException e) {
38 e.printStackTrace();
39 } catch (ClassNotFoundException e) {
40 e.printStackTrace();
41 }
42 return null;
43}
44 private static void saveFile(){
45 FileOutputStream fileOutputStream= null;
46 try {
47 fileOutputStream = new FileOutputStream(System.getProperty("user.dir")+"/"+"temp.txt");
48 ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
49 objectOutputStream.writeObject(REGISTER);
50 } catch (FileNotFoundException e) {
51 e.printStackTrace();
52 } catch (IOException e) {
53 e.printStackTrace();
54 }
55
56 }
57}
58
59
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 1//消费端
2public class Comsumer {
3 public static void main(String[] args) {
4 HelloService helloService = ProxyFactory.getProxy(HelloService.class);
5 String result = helloService.qq();
6 System.out.println(result);
7
8 }
9}
10
11//服务提供方
12public class Provider {
13
14 public static void main(String[] args) throws UnknownHostException {
15 String hostAddress = InetAddress.getLocalHost().getHostName();
16 URL url=new URL(hostAddress,8080);
17 //这里多个接口的话,都要注册上去
18 MapRegister.register(HelloService.class.getName(),url,HelloServiceImpl.class);
19 Protocol server= ProtocolFactory.http();
20 server.start(url);
21
22 }
23
24}
25
先启动provider,再启动comsumer,可以看到访问的结果。
主要核心还是通过动态代理代理具体调用的方法,通过tcp或者http等远程调用其它服务的接口,但这个只能简单的作为入门例子,还有很多改进的地方,如结合多线程处理消息,接入spring,更详细的协议定义,比如返回结果不仅仅是string,注册中心改为zookeeper实现等等,项目具体地址——https://github.com/97lele/rpcstudy/tree/master