RPC简介和框架选择

什么是Restful架构

Rest是Representational State Transfer的缩写词
a) 每一个uri代表一种资源
b) 客户端和服务端之间传递这种资源的表现层
c) 客户端通过四个Http动词,对服务端资源进行操作,实现"表现层状态转换"

GET 用来获取资源,
POST 用来新建资源(也可以用于更新资源),
PUT 用来更新资源,
DELETE 用来删除资源。

通过RestTemplate可以方便的使用Rest资源,内部使用了模板方法的设计模式

  1. getForEntity() 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象
  2. getForObject() 发送一个HTTP GET请求,返回的响应体映射为一个对象
  3. postForEntity() 发送一个HTTP POST请求,返回包含一个对象的ResponseEntity
  4. postForObject() 发送一个HTTP POST请求,返回的响应体将映射为一个对象
  5. execute() 在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象
  6. exchange()在URL上执行特定的HTTP方法,返回包含对象的ResponseEntity

什么是RPC

RPC就是要像调用本地的函数一样去调远程函数。进程通信有两种方式,第一种是LPC,通过共享内存空间进行进程间的通信,多用于在多任务操作系统中,另一种是RPC,通过网络进行进程间的通信,他的目的是让调用远程方法就像调用本地方法一样,调用着并不知道这个方法的服务提供者是谁,通过RPC能够解耦服务。

RPC框架的由四个核心组件构成,他们是Client,Cleint Stub,Server和Server Stub,stub可以理解为存根。
Client是服务调用方,Server是服务提供者
Client Stub存放服务端的地址消息,将客户端请求封装为消息,通过序列化后进行网络传输给服务端。
Server Stub存放客户端发来的消息,通过反序列化解析为客户端请求,然后调用本地方法。

实现RPC需要解决的三个问题

  1. 函数-call id的映射解决了标识函数的问题。在客户端和服务端之间维护函数和call id的对应关系,当调用远程方法时,先从这个表查出call id,然后传给服务端,然后服务端根据id查出所调用的函数
  2. 序列化和反序列化解决了参数传递的问题。本地调用通过内存栈就可以传递参数,但是远程调用需要把参数序列化为字节数组,然后在反序列为参数
  3. 网络传输解决函数id和函数参数在网络种传输的问题。大部分RPC框架都使用TCP协议,但也有使用UDP或者是HTTP的
    在这里插入图片描述

有了Rest为何还要RPC

有一种常见错误就是将Rest视为另一种远程过程调用机制,但是它们却没有任何关系。
RPC是面向服务的,关注行为和动作;而REST是面向资源的,关注资源本身,很多都是做网站的。也就是说Rest是将资源的状态在b/s之间传递,而RPC是调用远程主机应用程序的某个方法,关注减小本地调用和远程调用的差异。RPC更偏向内部调用,REST更偏向外部调用

考虑用RPC还是HTTP构建自己服务时,考虑的因素

  1. 接口是否需要Schema约束 HTTP是弱Schema约束的
  2. 是否需要更高效的传输协议 RPC直接使用TCP进行传输
  3. 是否对数据包大小敏感 HTTP头部有很多冗余
    论复杂度,RPC框架肯定是高于简单的HTTP接口的。但毋庸置疑,HTTP接口由于受限于HTTP协议,需要带HTTP请求头,导致传输起来效率或者说安全性不如RPC

SpringCloud和Dubbo的区别

Dubbo:基于TCP的远程过程调用
Spring Cloud:基于spirngboot,而springboot是基于http协议rest风格的RPC

  1. 服务间通信协议不同,Dubbo基于TCP协议,而Spring Cloud基于http协议
  2. Dubbo比SpringCloud效率高
  3. SpirngCloud基于http协议,服务提供方可用多语言开发
  4. spring cloud是微服务生态,包括完善的微服务工具集;而Dubbo是RPC的实现框架,仅仅是微服务的一部分。
    对于效率要求比较高,开发时使用统一的技术方法栈,则选择RPC。。。。常用的为dubbo(耦合Java语言)
    如果需要更加灵活的,跨语言,跨平台,用HTTP

Dubbo的基本原理

通过netty实现一个RPC框架

RPC框架让调用方像调用本地函数一样调用远程方法
调用方直接调用本地函数,传入相应参数。通过动态代理的方式将通讯细节以及序列化操作交给代理类来处理,这就需要通过网络来完整无误的将参数传递过去,而Netty框架就是一个高性能的网络通信框架,它可以胜任任务。

RPC框架需要关注的点:1. 动态代理 2. 通讯协议 3. 序列化 4. 网络传输
在这里插入图片描述

Protocol协议

首先我们需要确定通信双方的协议格式,请求对象和响应对象。
RpcRequest={requestId,className,methodName,parameterTypes(参数类型),parameters(入参)} //requestId验证请求和响应是否匹配
RpcResponse={requestId,error,result}

序列化

市面有很多序列化协议,为了方便起见,使用JSON作为序列化协议,fastjson作为JSON框架。

public class JSONSerializer implements Serializer{
    public byte[] serialize(){
          return JSON.toJSONBytes(object);
    }
    public <T> T deserialize(Class<T> clazz,byte[] bytes){
          return JSON.parseObject(bytes,clazz);
    }
}

编解码器

编码器实现

public class RpcEncoder extends MessageToByteEncoder {
    private Class<?> clazz;
    private Serializer serializer;

    public RpcEncoder(Class<?> clazz, Serializer serializer) {
        this.clazz = clazz;
        this.serializer = serializer;
    }


    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object msg, ByteBuf byteBuf) throws Exception {
        if (clazz != null && clazz.isInstance(msg)) {
            byte[] bytes = serializer.serialize(msg);
            byteBuf.writeInt(bytes.length);
            byteBuf.writeBytes(bytes);
        }
    }
}

解码器实现

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 {
        //因为之前编码的时候写入一个Int型,4个字节来表示长度
        if (byteBuf.readableBytes() < 4) {
            return;
        }
        //标记当前读的位置
        byteBuf.markReaderIndex();
        int dataLength = byteBuf.readInt();
        if (byteBuf.readableBytes() < dataLength) {
            byteBuf.resetReaderIndex();
            return;
        }
        byte[] data = new byte[dataLength];
        //将byteBuf中的数据读入data字节数组
        byteBuf.readBytes(data);
        Object obj = serializer.deserialize(clazz, data);
        list.add(obj);
    }
}

Netty客户端

a) 编写启动方法,指定传输使用的Channel
b) 指定ChannelHandler,对网络中的数据进行读写处理
c) 添加编解码器
d) 添加失败重试机制
e) 添加发送请求消息的方法

public class NetyClient{
     private String host;
     private Integer port;
     private Channel channel;
     private ClientHandler clientHandler;
     private EventLoopGroup evenetLoopGrouop;
     private static final int MAX_RETRY = 5;  //最大重试次数
     public NettyClient(String host, Integer port) {
        this.host = host;
        this.port = port;
     }
     public void connect(){
         clientHandler = new ClientHandler();
         eventLoopGroup = new NioEventLoopGroup();
         //开始编写启动类
         Bootstrap bootstrap = new Bootstrap();
         bootstrap.group(eventLoopGroup)
                //指定传输使用的Channel
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        //添加编码器
                        pipeline.addLast(new RpcEncoder(RpcRequest.class, new JSONSerializer()));
                        //添加解码器
                        pipeline.addLast(new RpcDecoder(RpcResponse.class, new JSONSerializer()));
                        //请求处理类
                        pipeline.addLast(clientHandler);
                    }
             });
             进行失败重连
             connect(bootstrap, host, port, MAX_RETRY);
     }
     /*
      *  进行失败重连
      */
     private void connect(Bootstrap bootstrap, String host, int port, int retry) {
        ChannelFuture channelFuture = bootstrap.connect(host, port).addListener(future -> {
            if (future.isSuccess()) {
                log.info("连接服务端成功");
            } else if (retry == 0) {
                log.error("重试次数已用完,放弃连接");
            } else {
                //第几次重连:
                int order = (MAX_RETRY - retry) + 1;
                //本次重连的间隔
                int delay = 1 << order;
                log.error("{} : 连接失败,第 {} 重连....", new Date(), order);
                bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit.SECONDS);
            }
        });
        channel = channelFuture.channel();
    }
    /*
     * 发送消息
     */
     public RpcResponse send(){
          try {
               //1. 将请求对象写入通道
               channel.writeAndFlush(request).await();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           //2. 根据请求id获得响应对象
           return clientHandler.getRpcResponse(request.getRequestId())
     }
     /*
      * 关闭通道
      */
      @PreDestroy
      public void close() {
         eventLoopGroup.shutdownGracefully();
         channel.closeFuture().syncUninterruptibly();
      }
}

编写数据处理类,使用Map维护请求对象ID与响应结果的映射关系,目的是为了客户端验证服务端响应是否与请求相匹配,因为Netty的channel可能被多个线程使用,当返回结果是不确定是从哪个线程返回的。

public class ClientHandler extends ChannelDuplexHandler {
    /**
     * 使用Map维护请求对象ID与响应结果Future的映射关系
     * 目的是
     */
    private final Map<String, DefaultFuture> futureMap = new ConcurrentHashMap<>();
    
    //1. 将RpcRequest写入通道
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        if (msg instanceof RpcRequest) {
            RpcRequest request = (RpcRequest) msg;
            //发送请求对象之前,先把请求ID保存下来,并构建一个与响应Future的映射关系
            futureMap.putIfAbsent(request.getRequestId(), new DefaultFuture());
        }
        super.write(ctx, msg, promise);
    }
    //2. 从通道读取返回的RpcResponse
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof RpcResponse) {
            //获取响应对象
            RpcResponse response = (RpcResponse) msg;
            DefaultFuture defaultFuture =
            futureMap.get(response.getRequestId());
            //将结果写入DefaultFuture
            defaultFuture.setResponse(response);
        }
        super.channelRead(ctx,msg);
    }

    //根据响应id查询发送请求的请求id
    public RpcResponse getRpcResponse(String requsetId) {
        try {
            DefaultFuture future = futureMap.get(requsetId);
            return future.getRpcResponse(5000);
        } finally {
            //获取成功以后,从map中移除
            futureMap.remove(requsetId);
        }
    }
}

响应结果是封装在DefaultFuture中的,因为Netty是异步框架,所有的返回都是基于Future和Callback机制的
实际上用了wait和notify机制,同时使用一个boolean变量做辅助

public class DefaultFuture {
    private RpcResponse rpcResponse;
    private volatile boolean isSucceed = false;
    private final Object object = new Object();

    public RpcResponse getRpcResponse(int timeout) {
        synchronized (object) {
            while (!isSucceed) {
                try {
                    object.wait(timeout);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return rpcResponse;
        }
    }

    public void setResponse(RpcResponse response) {
        if (isSucceed) {
            return;
        }
        synchronized (object) {
            this.rpcResponse = response;
            this.isSucceed = true;
            object.notify();
        }
    }
}

Netty服务端

与客户端差不多,但注意对请求解码后,需要通过代理的方式调用本地函数

public class NettyServer implements InitializingBean {
    private EventLoopGroup boss = null;
    private EventLoopGroup worker = null;
    @Autowired
    private ServerHandler serverHandler;
    @Override
    public void afterPropertiesSet() throws Exception {
        //此处使用了zookeeper做注册中心,本文不涉及,可忽略
        ServiceRegistry registry = new ZkServiceRegistry("127.0.0.1:2181");
        start(registry);
    }

    public void start(ServiceRegistry registry) throws Exception {
        //负责处理客户端连接的线程池
        boss = new NioEventLoopGroup();
        //负责处理读写操作的线程池
        worker = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        //添加解码器
                        pipeline.addLast(new RpcEncoder(RpcResponse.class, new JSONSerializer()));
                        //添加编码器
                        pipeline.addLast(new RpcDecoder(RpcRequest.class, new JSONSerializer()));
                        //添加请求处理器
                        pipeline.addLast(serverHandler);

                    }
                });
        bind(serverBootstrap, 8888);
    }

    /**
     * 如果端口绑定失败,端口数+1,重新绑定
     *
     * @param serverBootstrap
     * @param port
     */
    public void bind(final ServerBootstrap serverBootstrap,int port) {
        serverBootstrap.bind(port).addListener(future -> {
            if (future.isSuccess()) {
                log.info("端口[ {} ] 绑定成功",port);
            } else {
                log.error("端口[ {} ] 绑定失败", port);
                bind(serverBootstrap, port + 1);
            }
        });
    }

    @PreDestroy
    public void destory() throws InterruptedException {
        boss.shutdownGracefully().sync();
        worker.shutdownGracefully().sync();
        log.info("关闭Netty");
    }
}

处理读写操作的Handler类

@Component
@Slf4j
@ChannelHandler.Sharable
public class ServerHandler extends SimpleChannelInboundHandler<RpcRequest> implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) {
        RpcResponse rpcResponse = new RpcResponse();
        rpcResponse.setRequestId(msg.getRequestId());
        try {
            Object handler = handler(msg);
            log.info("获取返回结果: {} ", handler);
            rpcResponse.setResult(handler);
        } catch (Throwable throwable) {
            rpcResponse.setError(throwable.toString());
            throwable.printStackTrace();
        }
        ctx.writeAndFlush(rpcResponse);
    }

    /**
     * 服务端使用代理处理请求
     *
     * @param request
     * @return
     */
    private Object handler(RpcRequest request) throws ClassNotFoundException, InvocationTargetException {
        //使用Class.forName进行加载Class文件
        Class<?> clazz = Class.forName(request.getClassName());
        Object serviceBean = applicationContext.getBean(clazz);
        log.info("serviceBean: {}",serviceBean);
        Class<?> serviceClass = serviceBean.getClass();
        log.info("serverClass:{}",serviceClass);
        String methodName = request.getMethodName();

        Class<?>[] parameterTypes = request.getParameterTypes();
        Object[] parameters = request.getParameters();

        //使用CGLIB Reflect
        FastClass fastClass = FastClass.create(serviceClass);
        FastMethod fastMethod = fastClass.getMethod(methodName, parameterTypes);
        log.info("开始调用CGLIB动态代理执行服务端方法...");
        return fastMethod.invoke(serviceBean, parameters);
    }

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

客户端使用Java动态代理,在代理类实现通信细节,Java动态代理类需要实现InvocationHandler接口
在invoke方法中封装请求对象,构建NettyClient对象开启客户端,然后发送请求返回调用结果
单独的T 代表一个类型 ,而 Class代表这个类型所对应的类, Class<?>表示类型不确定的类

@Slf4j
public class RpcClientDynamicProxy<T> implements InvocationHandler {
    private Class<T> clazz;
    public RpcClientDynamicProxy(Class<T> clazz) throws Exception {
        this.clazz = clazz;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RpcRequest request = new RpcRequest();
        String requestId = UUID.randomUUID().toString();

        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();

        Class<?>[] parameterTypes = method.getParameterTypes();

        request.setRequestId(requestId);
        request.setClassName(className);
        request.setMethodName(methodName);
        request.setParameterTypes(parameterTypes);
        request.setParameters(args);
        log.info("请求内容: {}",request);
        
        //开启Netty 客户端,直连
        NettyClient nettyClient = new NettyClient("127.0.0.1", 8888);
        log.info("开始连接服务端:{}",new Date());
        nettyClient.connect();
        RpcResponse send = nettyClient.send(request);
        log.info("请求调用返回结果:{}", send.getResult());
        return send.getResult();
    }
}

代理工厂类通过Proxy.newProxyInstance创建接口的代理类

public class ProxyFactory {
    public static <T> T create(Class<T> interfaceClass) throws Exception {
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class<?>[] {interfaceClass}, new RpcClientDynamicProxy<T>(interfaceClass));
    }
}

测试RPC框架

客户端使用Java动态代理,在代理类中实现通信细节,
服务端也是用动态代理,通过CGLIB动态代理类执行目标方法

  1. 公共接口
public interface HelloService {
    String hello(String name);
}
  1. 服务端实现接口方法
//服务端实现
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public String hello(String name) {
        return "hello, " + name;
    }
}
  1. 客户端应用
@SpringBootApplication
@Slf4j
public class ClientApplication {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(ClientApplication.class, args);
        HelloService helloService = ProxyFactory.create(HelloService.class);
        log.info("响应结果“: {}",helloService.hello("pjmike"));
    }
}

以上我们基于Netty实现了一个非非非常简陋的RPC框架,比起成熟的RPC框架来说相差甚远,甚至说基本的注册中心都没有实现,但是通过本次实践,可以说我对于RPC的理解更深了,了解了一个RPC框架到底需要关注哪些方面,未来当我们使用成熟的RPC框架时,比如Dubbo,能够做到心中有数,能明白其底层不过也是使用Netty作为基础通讯框架。往后,如果更深入翻看开源RPC框架源码时,也相对比较容易

补充

CGLIB和Java动态代理的区别

  1. Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类;
  2. Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效

参考优秀博客:https://segmentfault.com/a/1190000020599687?utm_source=tag-newest

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值