IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 手写一个RPC框架 -> 正文阅读

[网络协议]手写一个RPC框架

什么是RPC,为什么需要RPC?

? ? ? ? RPC(Remote Procedure Call)远程过程调用。它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

????????在单体应用的情况下,方法调用只需要知道函数指针就可以进行方法的调用,这种情况是不需要rpc的。而如果是分布式微服务的结构,模块A的某个对象要调用模块B的某个方法,由于模块A与模块B不在同一个机器(服务器)上,所以A是不可能知道B的函数指针的,如果想进行方法调用,必然需要先进行 通信 ,而通信的手段就是 网络编程(Socket 编程),然后A找到B的服务地址,并发送信息,B收到信息后调用本地服务,并将结果返回给A。整个过程如果都需要编程人员来写,将会是一个很复杂的工程。而RPC的作用就是,封装这一系列操作,让客户端调用远程服务时就像调用本地服务一样简便。?

RPC需要实现的一些功能和设计思路

? ? ? ? 1.公共接口:服务端与客户端都能访问到的方法接口,但只有服务端有接口的实现。

? ? ? ? 2.服务注册与发现:服务端启动时把服务名称及地址注册到注册中心,客户端通过服务名称从注册中心中找到服务地址,之后才能进行网络请求。

? ? ? ? 3.传输协议:服务端与客户端之间必须确定好通信的消息结构。

? ? ? ? 4.序列化协议:远程通信必须要将对象转化为二进制传输,也就是序列化。

? ? ? ? 5.负载均衡:服务请求量过大时,不能把请求落在单个服务器上。

公共接口:

? ? ? ? 创建一个公共接口,但是只在服务端实现接口。

public interface HelloService {

    String sayHello(HelloObject helloObject);
}
@Service
public class HelloServerImp implements HelloService {

    @Override
    public String sayHello(HelloObject helloObject) {
        return "这是Impl1方法";   }
}

? ? ? ? 那么客户端如何调用这个服务呢??因为客户端没有接口的具体实现类,也就没办法生成实例对象。所以我们需要在客户端中把参数,接口,方法等信息序列化,通过网络传输给服务端,服务端解析后执行方法得到结果返回客户端,如何简化这一过程:动态代理

动态代理:

? ? ? ??为什么要动态代理?假设客户端不用动态代理,而使用硬编码,那对于服务端只有一个服务时还行,只需要写一套封装的代码,如果服务端有多个服务,我们不可能对每个服务写一套硬编码,所以就可以考虑到动态代理。

? ? ? ? 然后就是动态代理的选型思考。常见的动态代理有两种:JDK反射提供的动态代理,CGLIB动态代理。前者代理的类需要实现接口,后者不需要。一般来说,实现接口的类都直接用JDK动态代理,CGLIB是一个基于ASM的字节码生成库,通过类继承的方式实现动态代理。此处选用JDK动态代理。

? ? ? ? 动态代理需要知道哪些信息?host,port。需要传递这两个值来指明服务端的位置;同时我们需要发送一个request请求对象,也就是说服务端通过接收到这个对象,来确定调用哪个接口的哪个方法。首先需要interfaceName(接口名称)methodName(方法名),考虑到重载,我们还需要知道,methodParam(方法参数)methodParamType(参数类型)。?最后考虑到健康检测,还需要一个flag表示是否为heartBeat(心跳包)。?同样的,客户端收到消息后需要返回一个response响应对象,需要包含如下信息:statusCode(状态码),message(响应补充信息),data(响应数据)。 request 与 response 通过 requestId来确保对应关系。

客户端发送请求:

? ? ? ??对象创建完成之后,我们就需要通过网络传输的方式,将请求对象发送到服务器端。这时候就需要用到网络通信(网络编程)。我这里使用Netty来进行网络通信。首先创建NettyClient

    private static final EventLoopGroup group;
    private static final Bootstrap bootstrap;

    static {
        group = new NioEventLoopGroup();
        bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class);
    }

?????????同时考虑到对象传输过程中的序列化,client端从注册中心中查找服务这两方面问题。还需要构造两个字段。具体实现放在后边说。

    private final CommonSerializer serializer;
    private final ServiceDiscovery serviceDiscovery;
    private final UnprocessedRequests unprocessedRequests;

    
    public NettyClient(Integer serializer, LoadBalance loadBalancer) {
        this.serviceDiscovery = new NacosServiceDiscovery(loadBalancer);
        this.serializer = CommonSerializer.getByCode(serializer);
        this.unprocessedRequests = SingletonFactory.getInstance(UnprocessedRequests.class);
    }

? ? ? ? 之后声明sendRequest方法,为了提高并发量,我们使用CompletableFuture来封装该方法。首先,想要发送信息,必须知道接收方(服务端)的地址。这里就牵扯到了服务的注册与发现。

服务的注册与发现:

? ? ? ??显然,这个功能的实现需要借助注册中心,常见的注册中心(我见过的)有:zookeeper,nacos。这里使用nacos,为啥不用zookeeper?当然是因为我没学过啊。参考官网的nacos Java sdk,创建一个NacosUtil类,由于只需要nacos实现服务中心功能,所以配置中心相关的方法不做实现。

? ? ? ? 第一步:创建nacos实例(首先要启动nacos客户端这个没啥好说的)

private static final NamingService namingService = NamingFactory.createNamingService("127.0.0.1:8848");

? ? ? ? 第二步:编写注册服务进nacos的方法

public static void registerServer(String serviceName, InetSocketAddress address){                   
     // 第一个参数是nacos实例名称,第一步已经得到了,第二个参数是服务端在注册服务时的address
     namingService.registerInstance(serviceName,address.getHostName(),address.getPort());
            NacosUtil.address = address;
            serviceNames.add(serviceName);
        
    }

? ? ? ? 第三步:编写获取所有服务实例的方法

public static List<Instance> getAllInstance(String serviceName) throws NacosException {
        return namingService.getAllInstances(serviceName);
    }

? ? ? ? 第四步:编写注销服务的方法

 public static void clearRegistry(){
        if (!serviceNames.isEmpty() && address!=null){
            String host = address.getHostName();
            int port = address.getPort();
            Iterator<String> iterator = serviceNames.iterator();
            while (iterator.hasNext()){
                String next = iterator.next();
                namingService.deregisterInstance(next,host,port);
                
            }
        }
    }

? ? ? ? 至此,NacosUtil编写完毕,本项目所需注册中心功能已全部实现。

服务端注册服务:

? ? ? ? 客户端想要找到这个服务,首先这个服务必须已经被注册进服务中心,我们必须在服务端启动时执行这个操作。与客户端一样,服务端的实现也使用netty。由于是个人实现,所以需要在启动时先对nacos做一个初始化清理,防止之前的服务没有注销。

? ? ? ? 服务端在初始化过程中,会扫描所有服务,那么服务的定义是什么呢? 我们这里使用自定义注解@Service和@ServiceScan,凡是被这个注解修饰的Class,将被看作是一个服务,我们通过反射机制,获取到所有服务Class,然后将这些服务注册进nacos中,同时我们在HashMap中存储一份,方便我们快速获得服务实例。初始化结束后,执行start方法,对NioEventLoopGroup进行各种参数设置,配置等。为了实现心跳检测机制,我们在ChildHandler中加入IdleStateHandler。然后绑定端口。至此,服务端启动成功,服务被注册到nacos成功。

客户端sendRequest的实现:

? ? ? ? 该方法接受一个request参数,前文已经说到,该对象包含interfaceName字段,因此我们可以通过该字段从nacos中查询是否有该服务,并返回该服务的地址的ip,port等信息,封装成一个InetSocketAddress。接着,我们对这个地址创建通道。参考之前服务实例的存储方式,我们对于通道也使用一个HashMap进行存储,方便快速获取。创建完成后,调用writeAndFlush方法,开始进行数据发送。

服务端接受消息:

? ? ? ? channelRead0方法,在收到消息时,先通过heartBeat字段判断是否为心跳包,心跳包则不做处理。如果是有效信息,首先对信息进行处理:从存服务实例的HashMap中找到这个服务,然后通过反射调用目标方法。具体代码如下:

    public Object handler(RpcRequest request){
        Object o = RequestHandler.serviceProvider.getServiceProvider(request.getInterfaceName());
        return invokeTargetMethod(request,o);

    }

    public Object invokeTargetMethod(RpcRequest request,Object service){
        Object res = null;
        try {
            Method method = service.getClass().getMethod(request.getMethodName(), request.getParamType());
            res = method.invoke(service,request.getParameters());
            logger.info("服务:{} 成功调用方法:{}", request.getInterfaceName(), request.getMethodName());

        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            return RpcResponse.fail(ResponseCode.METHOD_NOT_FOUND, request.getRequestId());
        }
        return res;
    }

? ? ? ? 将调用成功后的数据通过writeAndFlush发送回客户端。

客户端接受调用成功的数据并检查:

? ? ? ? 客户端成功接收到数据response,将response与request进行检查,判断是否调用成功。检查逻辑如下:response == null ? requestId == responseId? response.statusCode == 200?,如果检查无误,返回response响应体中的data字段。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-11-17 13:06:20  更:2021-11-17 13:07:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年7日历 -2024/7/3 21:31:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码