rpc学习及netty自定义rpc

什么是RPC

RPC全称为remote procedure call,即远程过程调用。
借助RPC可以做到像本地调用一样调用远程服务,是一种进程间的通信方式
比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的方法,由于两个应用不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。
需要注意的是RPC并不是一个具体的技术,而是指整个网络远程调用过程

RPC架构
一个完整的RPC架构里面包含了四个核心的组件,分别是Client,Client Stub,Server以及Server Stub,这个Stub可以理解为存根。

  • 客户端(Client),服务的调用方。
  • 客户端存根(Client
    Stub),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远 程发送给服务方。
  • 服务端(Server),真正的服务提供者。
  • 服务端存根(Server Stub),接收客户端发送过来的消息,将消息解包,并调用本地的方法。
    在这里插入图片描述
    RPC调用过程
    在这里插入图片描述
    (1) 客户端(client)以本地调用方式(即以接口的方式)调用服务;
    (2) 客户端存根(client stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对
    象序列化为二进制);
    (3) 客户端通过sockets将消息发送到服务端;
    (4) 服务端存根( server stub)收到消息后进行解码(将消息对象反序列化);
    (5) 服务端存根( server stub)根据解码结果调用本地的服务;
    (6) 本地服务执行并将结果返回给服务端存根( server stub);
    (7) 服务端存根( server stub)将返回结果打包成消息(将结果消息对象序列化);
    (8) 服务端(server)通过sockets将消息发送到客户端;
    (9) 客户端存根(client stub)接收到结果消息,并进行解码(将结果消息发序列化);
    (10) 客户端(client)得到最终结果。
    RPC的目标是要把2、3、4、7、8、9这些步骤都封装起来。
    注意:无论是何种类型的数据,最终都需要转换成二进制流在网络上进行传输,数据的发送方需要将对象转换为二
    进制流,而数据的接收方则需要把二进制流再恢复为对象。
    在java中RPC框架比较多,常见的有Hessian、gRPC、Thri、HSF (High Speed Service Framework)、Dubbo 等,其实对于RPC框架而言,核心模块 就是通讯和序列化。

基于Netty自定义RPC

RPC又称远程过程调用,我们所知的远程调用分为两种,现在在服务间通信的方式也基本以这两种为主

1.是基于HTTP的restful形式的广义远程调用,以spring could的feign和restTemplate为代表,采用的协议是HTTP的7层调用协议,并且协议的参数和响应序列化基本以JSON格式和XML格式为主。
2.是基于TCP的狭义的RPC远程调用,以阿里的Dubbo为代表,主要通过netty来实现4层网络协议,NIO来异步传输,序列化也可以是JSON或者hessian2以及java自带的序列化等,可以配置。

接下来我们主要以第二种的RPC远程调用来自己实现

需求

案例版本:
server端与client端定义共用的接口,server端需要实现该接口。客户端通过jdk的proxy动态代理生成对象,在方法调用时执行invoke中的代码。invoke中使用netty与server端通信,我们在这里传输(类名#方法名)的字符串。server端收到数据后,根据类名#方法名去调用实现类并通过netty返回数据。

要求完成改造版本:
序列化协议修改为JSON,使用fastjson作为JSON框架,并将RpcRequest实体作为通信载体,服务端需根据客户端传递过来的RpcRequest对象通过反射,动态代理等技术,最终能够执行目标方法,返回字符串"success"。

要点提示:

(1)客户端代理的invoke方法中需封装RpcRequest对象,将其当做参数进行传递。

(2)服务端的UserServiceImpl类上添加@Service注解,在启动项目时,添加到容器中。

(3)服务端要添加@SpringBootApplication注解,main方法中添加。SpringApplication.run(ServerBootstrap.class, args);,进行启动扫描(注意项目启动类位置:扫描路径)。

(4)服务端在收到参数,可以借助反射及动态代理(如需用到ApplicationContext对象,可以借助实现ApplicationContextAware接口获取),来调用UserServiceImpl方法,最终向客户端返回”success“即可。

(5)既然传递的是RpcRequest对象了,那么客户端的编码器与服务端的解码器需重新设置。

实现

1.分析需求

  • 原始版本使用serviceName#methodName#组合作为待调用的类名#方法名,我们需要使用RpcRequest类来封装请求,封装待调用的类、方法、参数。
  • 使用fastjson作序列化

2.修改客户端,封装RpcRequest

 //2)封装RpcRequest,解析providerParam
     RpcRequest rpcRequest= new RpcRequest();
     rpcRequest.setClassName(providerParam.split("#")[0]);
     rpcRequest.setMethodName(providerParam.split("#")[1]);
     rpcRequest.setParameters(objects);
     rpcRequest.setParameterTypes(method.getParameterTypes());

3.修改客户端,修改UserClientHandler的 param参数 由String类型改为RpcRequest,并修改客户端的编码规则

  //4)配置启动引导对象
        bootstrap.group(group)
                //设置通道为NIO
                .channel(NioSocketChannel.class)
                //设置请求协议为TCP
                .option(ChannelOption.TCP_NODELAY,true)
                //监听channel 并初始化
                .handler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //获取ChannelPipeline
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //设置编码
                        //pipeline.addLast(new StringEncoder());
                        pipeline.addLast( new RpcEncoder(RpcRequest.class, new JSONSerializer()));
                        pipeline.addLast(new StringDecoder());
                        //添加自定义事件处理器
                        pipeline.addLast(userClientHandler);
                    }
                });

3.修改服务端,我们需要添加一个解码器

package com.lagou.entity;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.MessageToMessageDecoder;

import java.nio.charset.Charset;
import java.util.List;

public class RpcDecoder extends ByteToMessageDecoder {

    private Class<?> clazz;

    private Serializer serializer;



    public RpcDecoder(Class<?> clazz, Serializer serializer) {

        this.clazz = clazz;

        this.serializer = serializer;

    }




    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        if (clazz != null ) {
            int lenght=byteBuf.readableBytes();
            if (lenght < 1) return;
            //读取长度
            int len = byteBuf.readInt();

            //读取请求bytes
            byte[] bytes = new byte[len];
            byteBuf.getBytes(4, bytes);

            System.out.println(new String(bytes, Charset.forName("UTF-8")));
            //使用fastjson转换
            Object o=serializer.deserialize(clazz,bytes);
            list.add(o);
            byteBuf.clear();
        }
    }
}



4.修改服务端,添加springboot容器,将 UserServiceHandler、UserServiceImpl都加入到容器中进行管理,UserServiceHandler如果为单例的话会报错,所有为其设置@Scope(“prototype”)。其次使用ApplicationContext获取Request中的待调用的对象,进行调用

@Scope("prototype")
@Component
public class UserServiceHandler extends ChannelInboundHandlerAdapter implements ApplicationContextAware {


    private ApplicationContext applicationContext;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    //当客户端读取数据时,该方法会被调用
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        //注意:  客户端将来发送请求的时候会传递一个参数:  UserService#sayHello#are you ok
         //1.判断当前的请求是否符合规则
        RpcRequest rpcRequest = (com.lagou.entity.RpcRequest) msg;
        //获取对象;
        Object e=applicationContext.getBean(rpcRequest.getClassName());
        //requestClass.
        Class requestClass=e.getClass();
        //requestMethod.
        Method method = requestClass.getMethod(rpcRequest.getMethodName(),rpcRequest.getParameterTypes());

        //调用方法
        Object result= method.invoke(e,rpcRequest.getParameters());
        System.out.println("直接反射:"+result);
        //返回success
        ctx.writeAndFlush("success");
    }
}

5.启动Consumer与provider即可看到结果,其次Netty有必要仔细学习一下,参考文章ByteBuf详解

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值