基于socket和序列化的简单rpc框架实现

1、什么是RPC框架?

RPC:Remote Procedure Call,即远程过程调用,通俗来讲就是调用远程应用(服务器)上的方法。而rpc框架就是使得我们调用远程服务器上的方法就像调用本地方法一样简单方便。


2、http也能实现远程过程调用,为什么还要使用rpc框架?

关于这个问题,是我在学习和使用rpc框架中产生的疑惑,明明我们通过http协议get和post等接口就能实现远程调用,还要rpc“多此一举”干嘛呢?
直到某天我在知乎上看见了相同的问题,疑惑顿解。简单说来有以下几点原因:

  • http协议报文含有大量冗余,rpc框架如dubbo中自定义的通信协议更加精简,效率更高
  • rpc框架搭配注册中心具有更丰富的功能,如服务发现,负载均衡,熔断降级等

想了解更多可参见既然有 HTTP 请求,为什么还要用 RPC 调用?


3、Springboot使用rpc的样例

框架这里选择的是dubbo,服务注册中心选用zookeeper。

  • 首先导入pom相关依赖
  • 编写client和provider的配置文件
    client如下:
    在这里插入图片描述
    provider的配置除了服务名称不一样其他都一致
  • 对provider服务类添加@Service注解,注意这个注解不是之前用的@Service注解,它是在dubbo下的:
import com.alibaba.dubbo.config.annotation.Service;

这个注解的意思是,将我们的服务类注册到注册中心,注册中心地址在配置文件中。
服务类其他的代码写法和平时一样,比如这里有一个获得所有用户的方法:

@Override
    public List<UmsMember> getAllUser() {
        List<UmsMember> list= 		 userMapper.selectAll();//userMapper.selectAllUser();
        return list;
    }
  • 在client端如果想调用上面这个方法应该怎么做呢?
    在以前我们使用@Autowired注解注入服务类对象就可,而现在使用的是@Reference注解,该注解也在dubbo下面:
import com.alibaba.dubbo.config.annotation.Reference;
...
...
  @Reference
    UserService userService;

调用provider方法也是一样:

    @RequestMapping("getAllUser")
    @ResponseBody
    public List<UmsMember>  getAllUser(){
        List<UmsMember > allUser = userService.getAllUser();
        return allUser;
    }

可以看出,使用rpc框架确实很简单。


4、一个简单的rpc框架实现

其实rpc的过程并没有那么复杂,我们要调用远程的方法,应该怎么做呢?

  • 首先得要告诉服务方,我们是要调用什么方法,方法里是什么参数,希望拿到什么结果?
    因此就用到了序列化机制,关于序列化,我在各大厂面经那篇文章中有介绍,这里不多讲。
    简单说,序列化为我们将提供了网络传输对象的方法,这里我们的对象就封装了方法名称,参数等等。
  • 然后我们怎么讲对象进行网络传输呢?于是我们这里选择socket进行通信。
  • 在服务方拿到我们序列化后的结果,需要进行反序列化,反序列化以后,它去调用它本地的方法,拿到结果,再将结果序列化发送给客户端,客户端拿到再反序列化。
    总的过程如下:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Tt1UjNQ-1588669665481)(https://s1.ax1x.com/2020/05/05/YFFWnO.png)]

下面通过具体代码,进行一个简单rpc编写:
1、客户端调用一个简单的add(a,b)方法,实现a+b

public class Client {
    public static void main(String[] args) {
        Calculator calculator = new CalculatorImpl();
        int result = calculator.add(100, 200);
        System.out.println("rpc远程调用的结果是:"+result);
    }
}

2、我们将序列化以及通信的逻辑封装在calculatorImpl中:
注意这里ip地址直接选择的本机,端口号自定义,开启socket,然后将socket包装成输入输出流放入对象输入输出流中。

public class CalculatorImpl implements Calculator {
    private static final int PORT = 5566;
    @Override
    public int add(int a, int b) {
        try {
            Socket socket = new Socket("127.0.0.1", PORT); //实际情况会有多个provider,因此会有多个ip地址
            RequestSerializable requestSerializable = generateRequestSerializableObject(a, b);
            //包装一个socket输出流,在不同的场景有不同的输出流,比如文件输出流
            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
            outputStream.writeObject(requestSerializable);

            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object response = objectInputStream.readObject();//拿到反序列化结果
            if (response instanceof Integer) {
                return (Integer) response;
            }else {
                throw new InternalError();
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new InternalError();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new InternalError();
        }
    }
// RequestSerializable为方法以及参数的封装类,需要实现Serialzable接口
    public RequestSerializable generateRequestSerializableObject(int a, int b) {
        RequestSerializable requestSerializable = new RequestSerializable();
        requestSerializable.setA(a);
        requestSerializable.setB(b);
        requestSerializable.setMethod("add");
        return requestSerializable;
    }

3、看看服务方是怎么实现:
服务方只需要指定相同的端口号,开启监听,直到监听到客户端的请求,拿到方法和参数,进行反序列化解析,然后调用本地的add方法,这里的add方法就是一个简单的return a+b;

public class Service {
    private static final int PORT = 5566;
    Calculator calculator = new CalculatorImpl();
    public static void main(String[] args) throws IOException {
        new Service().run();
    }
    private  void run() throws IOException {
        ServerSocket listener = new ServerSocket(PORT);
        try {
            while(true){
                Socket socket = listener.accept();
                System.out.println("链接到客户端");
                try {
                    //反序列化
                    ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                    Object request = objectInputStream.readObject();
                    if(request instanceof RequestSerializable){
                        if("add".equals(((RequestSerializable) request).getMethod())){
                            int result = calculator.add(((RequestSerializable) request).getA(), ((RequestSerializable) request).getB());
                            //再将结果序列化发送回去
                            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
                            outputStream.writeObject((Integer)result);
                        }
                    }else{
                        throw new InternalError();
                    }

                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }finally {
                    socket.close();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            listener.close();
        }
    }

4、客户端调用add(100,200),最后控制台打印结果如下:
服务端:在这里插入图片描述
客户端:
在这里插入图片描述

5、至此一个最简单的rpc就实现完了,实际情况肯定是更复杂的,需要考虑负载均衡、服务注册、长连接、连接池等功能。
参考:
如何实现一个简单的RPC

另外:关于我的个人博客网站.,欢迎访问

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值