计算机网络相关 讲一下rpc与传统http的区别

这是一个非常硬核且经典的问题。要真正理解 RPC(Remote Procedure Call,远程过程调用)和 HTTP 的区别,以及如何手写一个 RPC 框架,我们需要深入操作系统的网络层、IO 模型以及序列化协议。

第一部分:RPC 与 传统 HTTP 的区别

1. 概念上的误区

首先要纠正一个常见的误区:RPC 和 HTTP 不是对立的

  • RPC 是一种设计思想(我要像调用本地函数一样调用远程函数)。
  • HTTP 是一个传输协议

事实上,很多 RPC 框架(如 gRPC)底层用的就是 HTTP/2。我们通常所说的 "RPC vs HTTP",实际上是在比较 "RPC 风格(如 gRPC, Dubbo)""RESTful 风格(基于 HTTP/1.1 + JSON)"

2. 核心区别对比

|----------|-----------------------------------------------------------|----------------------------------------------------|
| 特性 | RPC (以 gRPC/Dubbo 为例) | RESTful HTTP (传统 Web API) |
| 设计初衷 | 动作导向 。隐藏网络细节,让远程调用像本地调用一样(service.getUser(id))。 | 资源导向 。操作资源的状态(GET /users/1)。 |
| 传输协议 | 通常基于 TCPHTTP/2。 | 通常基于 HTTP/1.1。 |
| 报文体积 | 极小。通常使用二进制序列化(Protobuf, Thrift, Hessian),没有冗余 Header。 | 较大。使用文本(JSON/XML),且 HTTP 1.1 Header 通常包含大量元数据。 |
| 性能 | 。基于二进制、TCP 长连接、多路复用。 | 中等。JSON 解析慢,HTTP/1.1 存在队头阻塞问题。 |
| 开发体验 | 强类型 。通常需要 IDL 文件(如 .proto)生成代码,客户端服务端强契约。 | 弱类型。接口灵活,通常看文档对接,容易出现字段拼写错误。 |
| 适用场景 | 微服务内部通信(追求低延迟、高吞吐)。 | 对外接口(如移动端、Web 前端),因为 HTTP 通用性最好。 |

总结:

  • HTTP (REST) 像寄信:格式通用,谁都能读,但信封(Header)很厚,且一来一回慢。
  • RPC (TCP/Binary) 像专线电话:语言精简(二进制),双方约定暗号(IDL),连接建立后说话极快。

第二部分:涉及的计算机网络硬核知识

要手搓 RPC,你必须解决网络通信中的三大核心问题。这也是计算机网络的精髓。

1. 寻址与传输(Layer 4 - Transport Layer)
  • Socket 编程 :RPC 的本质是网络通信。你需要使用 Socket API(在 Java 中是 Socket/ServerSocket,在 Go 中是 net.Dial/net.Listen)。
  • TCP 连接复用 :建立 TCP 连接(三次握手)很慢。成熟的 RPC 框架都会使用连接池长连接,避免每次调用都握手。
2. 序列化与反序列化(Layer 6 - Presentation Layer)

网络只能传输 0 和 1(字节流),不能传输内存中的对象(如 Java 的 Object 或 Go 的 Struct)。

  • 序列化(Marshaling):把内存对象变成二进制串。
  • 反序列化(Unmarshaling):把二进制串变回内存对象。
  • 手搓选择:为了简单,我们可以用 JSON。为了性能,通常用 Protobuf 或 Hessian。
3. 拆包与粘包(TCP 字节流特性)

这是很多初学者最容易忽略的点。

TCP 是面向字节流的协议,它没有"消息"的概念。

  • 粘包:你发送了两个请求 "ABC" 和 "DEF",TCP 为了效率可能会把它们合并成 "ABCDEF" 发送过去。
  • 拆包:你发送了一个很大的包,TCP 可能会把它拆成两段发送。

解决方案(自定义应用层协议)

我们需要定义一个协议格式 。最常用的方式是 Length-Prefix(长度前缀法)

格式:[消息长度 (4字节)] + [消息体 (N字节)]

读取时,先读4个字节拿到长度 N,再读取 N 个字节,这样才能保证读到的是一个完整的 RPC 请求。


第三部分:如何手搓一个简单的 RPC

我们以 Java 为例(因为 Java 的动态代理最适合演示 RPC 原理),逻辑通用于所有语言。

架构图
复制代码
[Client App] --调用--> [Client Stub (Proxy)] --序列化--> [Network]
                                                          |
[Server Impl] <--反射调用-- [Server Skeleton] <--反序列化--|
步骤 1: 定义公共接口

客户端和服务端都需要这个接口。

复制代码
public interface UserService {
    User getUser(Integer id);
}
步骤 2: 客户端代理 (The Magic)

客户端并没有 UserService 的实现类,怎么调用?用动态代理

代理的作用是:拦截方法调用,把方法名、参数打包发给服务端。

复制代码
// 这是一个极简的动态代理逻辑
public class RpcClientProxy implements InvocationHandler {
    private String host;
    private int port;

    public RpcClientProxy(String host, int port) {
        this.host = host;
        this.port = port;
    }

    // 当你调用 userService.getUser(1) 时,会被这个 invoke 方法拦截
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1. 封装请求对象 (包含接口名、方法名、参数类型、参数值)
        RpcRequest request = new RpcRequest();
        request.setClassName(method.getDeclaringClass().getName());
        request.setMethodName(method.getName());
        request.setParamTypes(method.getParameterTypes());
        request.setArgs(args);

        // 2. 建立网络连接 (Socket)
        Socket socket = new Socket(host, port);
        
        // 3. 发送请求 (序列化) - 这里偷懒用了 Java 自带的序列化,实际应用应换成 JSON/Protobuf
        ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
        output.writeObject(request); // 发送数据

        // 4. 接收响应 (阻塞等待)
        ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
        Object result = input.readObject(); // 获取结果

        // 5. 清理资源
        socket.close();
        
        // 6. 返回结果给调用者
        return result;
    }
    
    // 获取代理对象的工具方法
    public <T> T getProxy(Class<T> interfaceClass) {
        return (T) Proxy.newProxyInstance(
            interfaceClass.getClassLoader(),
            new Class<?>[]{interfaceClass},
            this
        );
    }
}
步骤 3: 服务端监听 (The Server)

服务端需要在一个端口一直监听,接收请求,找到对应的实现类,执行代码。

复制代码
public class RpcServer {
    // 存储接口和服务实现类的映射关系
    private Map<String, Object> serviceRegistry = new HashMap<>();

    public void register(Class interfaceClass, Object impl) {
        serviceRegistry.put(interfaceClass.getName(), impl);
    }

    public void start(int port) throws Exception {
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("RPC Server started on port " + port);

        while (true) {
            // 1. 监听连接 (BIO 模型,阻塞)
            Socket socket = serverSocket.accept();
            
            // 2. 开启线程处理 (为了支持并发,实际应该用线程池)
            new Thread(() -> {
                try {
                    ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                    // 3. 读取请求 (反序列化)
                    RpcRequest request = (RpcRequest) input.readObject();
                    
                    // 4. 从注册表中找到具体的实现类
                    Object serviceImpl = serviceRegistry.get(request.getClassName());
                    
                    // 5. 通过反射调用真实方法
                    Method method = serviceImpl.getClass().getMethod(
                        request.getMethodName(), 
                        request.getParamTypes()
                    );
                    Object result = method.invoke(serviceImpl, request.getArgs());
                    
                    // 6. 写回结果
                    ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                    output.writeObject(result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
步骤 4: 跑起来

服务端代码:

复制代码
UserService service = new UserServiceImpl(); // 真实的业务逻辑
RpcServer server = new RpcServer();
server.register(UserService.class, service);
server.start(8080);

客户端代码:

复制代码
RpcClientProxy proxy = new RpcClientProxy("localhost", 8080);
UserService userService = proxy.getProxy(UserService.class);

// 这一步看起来像本地调用,实际上走了网络!
User user = userService.getUser(1001); 
System.out.println(user.getName());

第四部分:进阶思考(从玩具到生产级框架)

上面的代码只是一个玩具(Toy RPC),要变成一个生产级框架(如 Dubbo/gRPC),还需要解决以下问题,这也体现了计算机网络的高级应用:

  1. IO 模型(BIO vs NIO)
    • 上面的代码使用了 ServerSocket.accept()Stream.read(),这是同步阻塞 IO (BIO)。如果不开启多线程,一个客户端卡住,整个服务端就挂了。
    • 优化 :使用 Java NIO (Netty) 或 Go 的 Goroutine。使用 IO 多路复用 (epoll) 技术,让一个线程管理成千上万个连接。
  1. 序列化性能
    • Java 原生序列化(ObjectOutputStream)非常慢且生成的数据包很大。
    • 优化 :使用 Protobuf, Kryo, Hessian。它们基于二进制位操作,体积可能只有 Java 原生的 1/10。
  1. 服务发现(Service Discovery)
    • 上面代码写死了 localhost:8080。如果是集群呢?
    • 优化 :引入 Zookeeper/Nacos/Etcd 。服务端启动时把 IP 注册上去,客户端调用时先去注册中心拉取 IP 列表,然后进行负载均衡
  1. 自定义协议(解决粘包)
    • 上面的代码依赖 Java 对象的边界。如果跨语言(Java 调 Go),必须定义字节级协议。
    • 优化 :定义 Header (包含 Magic Number, Version, Length) + Body

总结

RPC 的本质就是 "透明化网络通信"

  • 网络层:利用 TCP/Socket 传输数据。
  • 表示层:利用序列化解决对象传输问题。
  • 应用层:利用动态代理(Stub)让调用者无感知。
相关推荐
xwz小王子2 小时前
Nature Electronics 新加坡国立大学研发了基于柔性拓扑结构服装的体感传感器网络
网络·体感传感器
源码获取_wx:Fegn08952 小时前
计算机毕业设计|基于springboot + vue网上超市系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring·课程设计
码农水水2 小时前
阿里Java面试被问:Online DDL的INSTANT、INPLACE、COPY算法差异
java·服务器·前端·数据库·mysql·算法·面试
小旭95272 小时前
【Java 基础】IO 流 全面详解
java·开发语言
Coder_Boy_2 小时前
基于SpringAI的在线考试系统-阅卷评分与错题管理模块回归测试逻辑梳理文档
java·spring boot·系统架构·ddd·tdd·全栈开发
那起舞的日子2 小时前
Java线程池-执行顺序
java
吃吃喝喝小朋友2 小时前
JavaScript事件
开发语言·前端·javascript
先做个垃圾出来………2 小时前
Linux/Unix系统下的基础文本处理命令
java·linux·unix
风若飞2 小时前
Linux 环境下解决 Tomcat8 与 JDK8 配置问题
java·linux·运维·服务器·tomcat