既然有 HTTP 了,为什么还要有 RPC?

252 阅读5分钟

现在电脑上装的各种联网软件,比如 xx管家,xx卫士,它们都作为客户端(Client)需要跟服务端(Server)建立连接收发消息,此时都会用到应用层协议,在这种 Client/Server (C/S) 架构下,它们可以使用自家造的 RPC 协议,因为它只管连自己公司的服务器就 ok 了。

但有个软件不同,浏览器(Browser) ,不管是 Chrome 还是 IE,它们不仅要能访问自家公司的服务器(Server) ,还需要访问其他公司的网站服务器,因此它们需要有个统一的标准,不然大家没法交流。于是,HTTP 就是那个时代用于统一 Browser/Server (B/S) 的协议。

也就是说在多年以前,HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。很多软件同时支持多端,比如某度云盘,既要支持网页版,还要支持手机端和 PC 端,如果通信协议都用 HTTP 的话,那服务器只用同一套就够了。而 RPC 就开始退居幕后,一般用于公司内部集群里,各个微服务之间的通讯。

那这么说的话,都用 HTTP 得了,还用什么 RPC?

HTTP 和 RPC 有什么区别

RPC和HTTP并不是竞争关系,而是互补关系:

  1. RPC = 内部通信的利器,追求性能和开发效率
  2. HTTP = 对外服务的标准,追求通用性和兼容性

基于 guide-rpc-framework RPC框架的代码分析,从服务发现底层连接方式传输内容三个维度详细对比HTTP和RPC的区别:

1. 🔍 服务发现机制

RPC的服务发现

RPC框架通常有完整的服务注册发现机制:

@Override
public InetSocketAddress lookupService(RpcRequest rpcRequest) {
    String rpcServiceName = rpcRequest.getRpcServiceName();
    CuratorFramework zkClient = CuratorUtils.getZkClient();
    List<String> serviceUrlList = CuratorUtils.getChildrenNodes(zkClient, rpcServiceName);
    if (CollectionUtil.isEmpty(serviceUrlList)) {
        throw new RpcException(RpcErrorMessageEnum.SERVICE_CAN_NOT_BE_FOUND, rpcServiceName);
    }
    // load balancing
    String targetServiceUrl = loadBalance.selectServiceAddress(serviceUrlList, rpcRequest);
    log.info("Successfully found the service address:[{}]", targetServiceUrl);
    String[] socketAddressArray = targetServiceUrl.split(":");
    String host = socketAddressArray[0];
    int port = Integer.parseInt(socketAddressArray[1]);
    return new InetSocketAddress(host, port);
}

RPC服务发现特点

  • 🏗️ 自动注册:服务启动时自动注册到注册中心(如Zookeeper)
  • 🔍 动态发现:客户端从注册中心动态获取服务列表
  • ⚖️ 负载均衡:内置负载均衡算法选择最优服务实例
  • 💓 健康检查:通过临时节点实现服务健康监控
  • 🔄 实时更新:服务列表变化时自动更新本地缓存

HTTP的服务发现

HTTP通常依赖外部机制:

# 传统方式:硬编码或配置文件
http://api.example.com:8080/user/getUserInfo

# 现代方式:通过服务网格或API网关
http://api-gateway:80/user-service/getUserInfo

HTTP服务发现特点

  • 📝 手动配置:通常需要手动配置服务地址
  • 🌐 DNS解析:依赖DNS或负载均衡器进行服务发现
  • 🔧 外部依赖:需要额外的组件(如Nginx、API Gateway)
  • 📍 静态配置:服务地址相对固定,变更需要重新配置

2. 🔌 底层连接方式

RPC的连接管理

RPC框架通常维护长连接池:

public Channel get(InetSocketAddress inetSocketAddress) {
    String key = inetSocketAddress.toString();
    // determine if there is a connection for the corresponding address
    if (channelMap.containsKey(key)) {
        Channel channel = channelMap.get(key);
        // if so, determine if the connection is available, and if so, get it directly
        if (channel != null && channel.isActive()) {
            return channel;
        } else {
            channelMap.remove(key);
        }
    }
    return null;
}

RPC连接特点

  • 🔗 长连接:维护持久化的TCP连接
  • 🏊‍♂️ 连接池:复用连接,避免频繁建连开销
  • 💓 心跳保活:定期发送心跳包保持连接活跃
  • 高性能:基于NIO的异步非阻塞通信
// If no data is sent to the server within 15 seconds, a heartbeat request is sent
p.addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));
p.addLast(new RpcMessageEncoder());
p.addLast(new RpcMessageDecoder());
p.addLast(new NettyRpcClientHandler());

HTTP的连接管理

HTTP传统上是短连接模式:

// HTTP 1.0: 每次请求建立新连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
// 请求完成后连接关闭

// HTTP 1.1: 支持Keep-Alive
connection.setRequestProperty("Connection", "keep-alive");

// HTTP/2: 多路复用
// 一个连接可以并发处理多个请求

HTTP连接特点

  • 🔄 短连接:HTTP/1.0默认每次请求后关闭连接
  • 🔗 Keep-Alive:HTTP/1.1支持连接复用
  • 🚀 多路复用:HTTP/2支持单连接并发请求
  • 🌐 无状态:每个请求都是独立的

3. 📦 传输内容格式

RPC的传输内容

RPC使用自定义的二进制协议:

@Builder
@ToString
public class RpcMessage {
    /**
     * rpc message type
     */
    private byte messageType;
    /**
     * serialization type
     */
    private byte codec;
    /**
     * compress type
     */
    private byte compress;
    /**
     * request id
     */
    private int requestId;
    /**
     * request data
     */
    private Object data;
}

RPC协议格式

+---------------------------------------------------------------+
| 魔数 4byte | 版本 1byte | 总长度 4byte | 消息类型 1byte | ...  |
+---------------------------------------------------------------+
| 序列化类型 1byte | 压缩类型 1byte | 请求ID 4byte | 数据长度 4byte |
+---------------------------------------------------------------+
|                        数据内容                                |
+---------------------------------------------------------------+
/**
 * Magic number. Verify RpcMessage
 */
public static final byte[] MAGIC_NUMBER = {(byte) 'g', (byte) 'r', (byte) 'p', (byte) 'c'};
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
//version information
public static final byte VERSION = 1;
public static final byte TOTAL_LENGTH = 16;
public static final byte REQUEST_TYPE = 1;
public static final byte RESPONSE_TYPE = 2;

RPC传输特点

  • 🔢 二进制协议:紧凑的二进制格式,传输效率高
  • 🗜️ 数据压缩:支持GZIP等压缩算法
  • 高效序列化:使用Kryo、Protobuf等高性能序列化
  • 🎯 强类型:编译时类型检查,类型安全

HTTP的传输内容

HTTP使用文本协议:

POST /api/user/create HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 45

{
  "name": "张三",
  "age": 25,
  "email": "zhangsan@example.com"
}

HTTP传输特点

  • 📝 文本协议:可读性好,便于调试
  • 🏷️ 丰富头部:支持各种元数据(缓存、认证等)
  • 🔄 标准化:统一的状态码和方法
  • 🌍 跨语言:任何语言都可以轻松实现

📊 对比总结表

维度RPCHTTP
服务发现🔍 自动注册发现
⚖️ 内置负载均衡
💓 健康检查
📝 手动配置
🌐 DNS解析
🔧 外部组件
连接方式🔗 长连接池
💓 心跳保活
⚡ NIO异步
🔄 短连接
🔗 Keep-Alive
🚀 HTTP/2多路复用
传输内容🔢 二进制协议
🗜️ 数据压缩
⚡ 高效序列化
📝 文本协议
🏷️ 丰富头部
🌍 标准化

🎯 选择建议

选择RPC的场景

  • 🏢 内部微服务通信
  • ⚡ 对性能要求极高
  • 🔒 强类型约束需求
  • 🎯 服务治理完整性要求

选择HTTP的场景

  • 🌐 对外API接口
  • 🔄 跨语言系统集成
  • 📱 前后端分离架构
  • 🛠️ 需要标准化协议

在实际项目中,两者往往结合使用:内部用RPC保证性能,对外用HTTP保证兼容性! 🚀