RPC 和 HTTP 的区别

RPC 和 HTTP 的区别

概念不同

  • HTTP 是应用层协议。
  • RPC 是远程过程调用,它是调用方式,对应的是本地调用。
    所谓的 RPC 协议,实际上是基于 TCP、UDP、甚至 HTTP2 改造后的自定义协议。

HTTP 和 RPC 的请求流程

gRPC请求流程

在这里插入图片描述

  • stub是桩代码的意思,可以理解为存根
  • client stub:存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • server stub:接收客户端发送过来的消息,将消息解包,并调用本地的方法。

在这里插入图片描述

网络传输层

本质都是基于 Socket 通信

HTTP/1.1

  • 建立一个 TCP 长连接,设置 keep-alive 长时间复用这个连接。
  • 框架中会引入成熟的网络库,给 HTTP 加连接池,保证不只有一个 TCP 连接可用。

RPC

  • 建立 TCP 连接池,框架也会引入成熟网络库来提高传输性能。
  • gRPC 基于 HTTP/2,拥有多路复用、优先级控制、头部压缩等优势。

浅析 RPC 的优势和不足

优势

  • 相较于 HTTP/1.1,数据包更小、序列化更快,所以传输效率很高。
  • 基于 TCP 或 HTTP/2 的自定义 RPC 协议,网络传输性能比 HTTP/1.1 更快。
  • 适用于微服务架构,微服务集群下,每个微服务职责单一,有利于多团队的分工协作。

不足

  • RPC 协议本身无法解决微服务集群的问题,例如:服务发现、服务治理等,需要工具来保障服务的稳定性。
  • 调用方对服务端的 RPC 接口有强依赖关系,需要有自动化工具、版本管理工具来保证代码级别的强依赖关系。例如,stub 桩文件需要频繁更新,否则接口调用方式可能出错。

RPC 框架职责

编解码层

目标

  • 生成代码:代码生成工具将 IDL 文件转换成不同语言可以依赖的 lib 代码(类似于库函数)
  • 序列化 & 反序列化:对象 ↔ 二进制字节流

选型

  • 安全性
  • 通用性:跨语言、跨平台
  • 兼容性:序列化协议升级后,保证原服务的稳定性
  • 性能
    • 时间:序列化反序列化的速度
    • 空间:序列化后的数据体积大小,体积越小,网络传输耗时越短

协议层

目标
支持解析多种协议,包含 HTTP、HTTP2、自定义 RPC 协议、私有协议等。

网络传输层

  • 目标

    IO 多路复用实现高并发,可靠传输

  • 选型指标

    • 易用:封装原生 socket API
    • 性能:零拷贝、建立连接池、减少 GC 等

http框架解析

Gin框架

Router 路由是如何保存的?

请添加图片描述

基数树在Gin中的应用

在这里插入图片描述

基数树,相当于是一种前缀树。对于基数树的每个节点,如果该节点是确定的子树的话,就和父节点合并。基数树可用来构建关联数组。
在上面的图里也可以看到,数据结构会把所有相同前缀都提取 剩余的都作为子节点。

从上面可以看到基数树是一个前缀树,图中也可以看到数据结构。那基数树在Gin中是如何应用的呢?举一个例子其实就能看得出来

router.GET("/support", handler1)
router.GET("/search", handler2)
router.GET("/contact", handler3)
router.GET("/group/user/", handler4)
router.GET("/group/user/test", handler5)
/ (handler = nil, indices = "scg")
    s (handler = nil, indices = "ue")
        upport (handler = handler1, indices = "")
        earch (handler = handler2, indices = "")
    contact (handler = handler3, indices = "")
    group/user/ (handler = handler4, indices = "u")
        test (handler = handler5, indices = "")

https://segmentfault.com/a/1190000019149860

路由注册

通过任意一个路由注册方法,都可以进入到 func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes 这个方法。

// 挂载路由的实际处理函数
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 计算最终的绝对路径 base + relativePath
    // 注意base也是绝对路径
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 合并处理函数,将中间件和逻辑函数结合在一起
    // 一般来说这里传入的handlder是逻辑函数 len(handlers) = 1
    // 只有少数的handler会有自己的中间件处理函数 len(handlers) > 1
    handlers = group.combineHandlers(handlers)
    // 将处理好的 HandlersChain 加载到Radix Tree中去
    // 这也表明,这里的RouterGroup只会载处理路由时发挥作用
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

看到这里回到engine.addRoute中去,通过深入发现如下调用链:
engine.GET -> routergroup.GET -> routergroup.handle -> engine.addRoute -> methodTree.addRoute -> node(radix-tree’s node).insertChild

r.Handle

r.Get, r.Post等方法实质都是通过调用 r.Handle 实现的

func (r *Router) Handle(method, path string, handle Handle) {
    // 路径注册必须从/开始,否则直接报错
	if path[0] != '/' {
		panic("path must begin with '/' in path '" + path + "'")
	}

    // 路由树map不存在需要新建
	if r.trees == nil {
		r.trees = make(map[string]*node)
	}

    // 获取当前方法所对应树的根节点,不存在则新建一个
	root := r.trees[method]
	if root == nil {
		root = new(node)
		r.trees[method] = root
	}

    // 向路由树当中添加一条一条路由
	root.addRoute(path, handle)
}

https://zhuanlan.zhihu.com/p/110976568

gin 与 net/http 的关系

这基本是整个过程的代码了.

  • ln, err := net.Listen(“tcp”, addr)做了初试化了socket, bind, listen的操作.
  • rw, e := l.Accept()进行accept, 等待客户端进行连接
  • go c.serve(ctx) 启动新的goroutine来处理本次请求. 同时主goroutine继续等待客户端连接, 进行高并发操作
  • h, _ := mux.Handler® 获取注册的路由, 然后拿到这个路由的handler, 然后将处理结果返回给客户端

RPC框架解析

二、gRPC

2.1 gRPC 优点

基于HTTP/2

HTTP/2 提供了二进制、长连接、连接多路复用、双向流、服务器推送、请求优先级、首部压缩等机制。可以节省带宽、降低TCP链接次数、节省CPU,帮助移动设备延长电池寿命等。

gRPC 的协议设计上使用了HTTP2 现有的语义,请求和响应的数据使用HTTP Body 发送,其他的控制信息则用Header 表示。

HTTP/2 对比 HTTP 1.1

浏览器请求 url -> 解析域名 -> 建立 HTTP 连接 -> 服务器处理文件 -> 返回数据 -> 浏览器解析、渲染文件

这个流程最大的问题是,每次请求都需要建立一次 HTTP 连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,而且逻辑上是非必需的,

为了解决这个问题, HTTP 1.1 中提供了 Keep-Alive,允许我们建立一次 HTTP 连接,来返回多次请求数据。

HTTP 1.1有两个问题:

  • HTTP 1.1 基于串行文件传输数据,因此这些请求必须是有序的,所以实际上我们只是节省了建立连接的时间,而获取数据的时间并没有减少

  • 最大并发数问题,假设我们在 Apache 中设置了最大并发数 300,而因为浏览器本身的限制,最大请求数为 6,那么服务器能承载的最高并发数是 50

HTTP 2 解决方案:

  • HTTP/2 引入二进制数据帧和流的概念,其中帧对数据进行顺序标识,这样浏览器收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。因为有了序列,服务器就可以并行的传输数据。

  • HTTP/2 对同一域名下所有请求都是基于流,也就是说同一域名不管访问多少文件,也只建立一路连接。同样Apache的最大连接数为300,因为有了这个新特性,最大的并发就可以提升到300,比原来提升了6倍。

  • 多路复用

    多路复用GRPC使用HTTP/2作为应用层的传输协议,HTTP/2会复用底层的TCP连接
    每一次RPC调用会产生一个新的Stream,每个Stream包含多个Frame,Frame是HTTP/2里面最小的数据传输单位。同时每个Stream有唯一的ID标识,如果是客户端创建的则ID是奇数,服务端创建的ID则是偶数。如果一条连接上的ID使用完了,Client会新建一条连接,Server也会给Client发送一个GOAWAY Frame强制让Client新建一条连接。一条GRPC连接允许并发的发送和接收多个Stream,而控制的参数便是MaxConcurrentStreams,Golang的服务端默认是100。

  • 超时重连
    我们在通过调用Dial或者DialContext函数创建连接时,默认只是返回ClientConn结构体指针,同时会启动一个Goroutine异步的去建立连接。如果想要等连接建立完再返回,可以指定grpc.WithBlock()传入Options来实现。超时机制很简单,在调用的时候传入一个timeout的context就可以了。重连机制通过启动一个Goroutine异步的去建立连接实现的,可以避免服务器因为连接空闲时间过长关闭连接、服务器重启等造成的客户端连接失效问题。也就是说通过GRPC的重连机制可以完美的解决连接池设计原则中的空闲连接的超时与保活问题。

IDL使用ProtoBuf

gRPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议(类似于XML、JSON、hessian)。ProtoBuf能够将数据进行序列化,并广泛应用在数据存储、通信协议等方面。压缩和传输效率高,语法简单,表达力强。

  • proto文件生成目标代码,简单易用
  • protobuf二进制消息,性能好/效率高(空间和时间效率都很不错)
  • 多语言支持

2.2 gRPC 缺点:

  • GRPC尚未提供连接池,需要自行实现
  • 尚未提供“服务发现”、“负载均衡”机制
  • 因为基于HTTP2,绝大部多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx1.9版本已支持)
  • Protobuf二进制可读性差(貌似提供了Text_Fromat功能)
  • 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)

2.3 grpc meatdata

在http请求当中我们可以设置header用来传递数据,grpc底层采用http2协议也是支持传递数据的,采用的是metadata。
Metadata 对于 gRPC 本身来说透明, 它使得 client 和 server 能为对方提供本次调用的信息。
就像一次 http 请求的 RequestHeader 和 ResponseHeader,http header 的生命周期是一次 http 请求, Metadata 的生命周期则是一次 RPC 调用。

  • 客户端请求中携带 metadata

有两种发送 metadata 到 server 的方法,推荐的方法是使用 AppendToOutgoingContext,如果 metadata 已存在则会合并,不存在则添加

func AppendToOutgoingContext(ctx context.Context, kv ...string) context.Context

// create a new context with some metadata
ctx := metadata.AppendToOutgoingContext(ctx, "k1", "v1", "k1", "v2", "k2", "v3")

// later, add some more metadata to the context (e.g. in an interceptor)
ctx := metadata.AppendToOutgoingContext(ctx, "k3", "v4")

  • 接收

服务端调用 FromIncomingContext 即可从 context 中接收 client 发送的 metadata

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    // 解析metada中的信息并验证
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, grpc.Errorf(codes.Unauthenticated, "无Token认证信息")
	}
	
    var (
        appid  string
        appkey string
    )

    if val, ok := md["appid"]; ok {
        appid = val[0]
    }

    if val, ok := md["appkey"]; ok {
        appkey = val[0]
    }

    if appid != "101010" || appkey != "i am key" {
        return nil, grpc.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
    }
	log.Printf("Received: %v.\nToken info: appid=%s,appkey=%s", in.GetName(), appid, appkey)
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

gRPC 默认两种认证方式

  • 基于SSL/TLS认证方式

  • 远程调用认证方式

两种方式能够混合使用

2.4 grpc 负载均衡

https://blog.csdn.net/weixin_43746433/article/details/114290213

HTTP和RPC的优缺点

  • 传输层
    实际应用过程中,五层协议结构里面是没有表示层和会话层的。应该说它们和应用层合并了。我们应该将重点放在应用层和传输层这两个层面。因为HTTP是应用层协议,而TCP是传输层协议。好,知道了网络的分层模型以后我们可以更好地理解为什么RPC服务相比HTTP服务要Nice一些!

  • 传输协议

RPC:可以基于TCP协议,也可以基于HTTP协议

HTTP:基于HTTP协议

  • 传输效率

RPC:使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率

HTTP:如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理

  • 性能消耗

RPC:可以基于thrift实现高效的二进制传输

HTTP:大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能

  • 负载均衡

RPC:基本都自带了负载均衡策略

HTTP:需要配置Nginx,HAProxy来实现

  • 服务治理

RPC:能做到自动通知,不影响上游

HTTP:需要事先通知,修改Nginx/HAProxy配置

  • 总结

RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。

但是对于大型企业来说,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。

在这里插入图片描述

Go语言中的RPC支持与处理

在Go中,标准库提供的net/rpc包实现了RPC协议需要的相关细节,开发者可以很方便的使用该包编写RPC的服务端和客户端程序。这使得用Go语言开发的多个进程之间的通信变得非常简单
net/rpc包允许PRC客户端程序通过网络或者其他IO连接调用一个远程对象的公开方法(该方法必须是外部可访问即首字母大写)。
在PRC服务端,可将一个对象注册为可访问的服务,之后该对象的公开方法就能够以远程的方式提供访问。

https://grpc.io/docs/languages/go/quickstart/

grpc

gRPC是一个开源的远程过程调用框架,用于服务之间的高性能通信。服务之间的通信可以使用各种语言,通过可插拔的负载均衡、追踪、健康检查和身份验证,这让它被认为是一种非常高效的方法。在默认情况下,gRPC使用协议缓冲(Protocol Buffer)来序列化结构化数据。通常,在微服务架构中,gRPC被认为是REST更好的替代方案。gRPC中的“g”取名于最初开发该技术的Google。

在gRPC里,客户端可以直接调用不同机器上的服务应用的方法,就像是本地对象一样,所以创建分布式应用和服务就很简单了。
在很多RPC(Remote Procedure Call Protocol)系统里,gRPC是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。
在服务端,服务实现这个接口并且运行gRPC服务处理客户端调用。
在客户端,有一个stub提供和服务端相同的方法。

http和rpc的区别?

http的实现技术:HttpClient 缺点是消息封装臃肿。

相同点:

底层通讯都是基于socket,都可以实现远程调用,都可以实现服务调用服务

不同点:
  • RPC:框架有:dubbo、cxf、(RMI远程方法调用)Hessian
    当使用RPC框架实现服务间调用的时候,要求服务提供方和服务消费方 都必须使用统一的RPC框架,要么都dubbo,要么都cxf

    跨操作系统在同一编程语言内使用
    优势:调用快、处理快

  • http:框架有:httpClient
    当使用http进行服务间调用的时候,无需关注服务提供方使用的编程语言,也无需关注服务消费方使用的编程语言,服务提供方只需要提供restful风格的接口,服务消费方,按照restful的原则,请求服务,即可

    跨系统跨编程语言的远程调用框架
    优势:通用性强

总结:对比RPC和http的区别

1 RPC要求服务提供方和服务调用方都需要使用相同的技术,要么都grpc,要么都dubbo
而http无需关注语言的实现,只需要遵循rest规范
2 RPC的开发要求较多,像Hessian框架还需要服务器提供完整的接口代码(包名.类名.方法名必须完全一致),否则客户端无法运行

protobuf

https://blog.csdn.net/weixin_43746433/article/details/127849767

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐