BLOG / POST
RPC 远程过程调用
RPC 是一种让程序像调用本地方法一样调用远程服务的协议,LSP 中的 JSON-RPC 即是其典型应用。
- rpc
- protocol
RPC 的全称是 Remote Procedure Call,即「远程过程调用」,它是一种计算机协议。
作为一个 Neovim 玩家,你一定听过 LSP(Language Server Protocol,语言服务器协议)。不知道大家有没有想过,为什么你在编辑器中编写不同语言的代码,都能获取到一些语法提示。这其中就是 LSP 在工作。LSP 的作用是让开发工具通过「JSON-RPC」与语言服务器进行通信。

上面的 JSON-RPC 属于 RPC 的一种,从名称上来看它是一种使用 JSON 作为数据格式来进行信息的传输。
远程过程调用
所谓远程过程调用,它允许一台计算机上的程序可以调用另一台计算机上的函数。这个调用看起来就像在调用本地函数一样。以下是本地调用的情况。
// App A
package main
import "fmt"
// 本地函数 Hello
func hello() {
fmt.Println("hello")
}
// 直接调用
func main() {
Hello()
}
RPC 的作用就是让程序 A 可以调用程序 B 中的 hello 函数,即
// App A
package main
// 直接调用
func main() {
Hello() //来自App B
}
// App B
package main
func Hello(){
fmt.Println("hello")
}
func main() {
}
如果这样直接运行 App A 肯定会报错,因为根本找不到 Hello 函数。
通过 HTTP 调用
我们可以换种方式,通过 HTTP 暴露接口来间接调用
// App A (server)
package main
import (
"net/http"
)
func hello() string {
return "hello"
}
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
h := hello()
w.Write([]byte(h))
})
http.ListenAndServe(":8080", nil)
}
// App B (client)
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("http://localhost:8080/hello")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
通过调用 HTTP 接口 http://localhost:8080/hello 就可以调用 hello 函数了。这种方式常见于前后端分离的项目中,前端通过 HTTP 调用后端服务的接口。
通过 RPC 调用
以下是通过 TCP 协议
// App A (Server)
package main
import (
"log"
"net"
"net/rpc"
)
type ServiceA struct{}
type Args struct {
Message string
}
type Reply struct {
Message string
}
// RPC 方法需满足
// 1. 方法必须导出(首字母大写)
// 2. 有两个参数,
// 第一个参数 args 用于接收客户端传入的参数
// 第二个参数 reply 用于返回结果给客户端
// 3. 返回值类型为error
func (s *ServiceA) Hello(args *Args, reply *Reply) error {
reply.Message = args.Message
return nil
}
func main() {
service := new(ServiceA)
rpc.Register(service)
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
go rpc.ServeConn(conn)
}
}
// App B (Client)
package main
import (
"fmt"
"log"
"net/rpc"
)
type Args struct {
Message string
}
type Reply struct {
Message string
}
func main() {
// 连接到服务器
conn, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("Dialing error:", err)
}
// 关闭连接
defer conn.Close()
args := Args{Message: "world"}
var resp Reply
// 调用服务
err = conn.Call("ServiceA.Hello", args, &resp)
if err != nil {
log.Fatal("Calling error:", err)
}
fmt.Println(resp)
}
RPC 的优势
RPC(远程过程调用)相比于传统的 HTTP API 调用,有以下几个主要优势:
- 更高的性能:RPC 通常使用二进制协议,数据传输更加高效,减少了网络开销。
- 强类型支持:RPC 框架通常提供强类型支持,可以在编译时捕获类型错误,提高代码的可靠性。
- 更好的接口定义:RPC 使用接口定义语言(IDL)来描述服务接口,使得服务契约更加清晰。
- 跨语言支持:许多 RPC 框架支持多种编程语言,便于不同语言编写的服务之间进行通信。
- 双向流支持:一些 RPC 框架(如 gRPC)支持双向流,适用于需要持续数据交换的场景。
- 自动代码生成:RPC 框架通常提供工具来自动生成客户端和服务端代码,减少了手动编写的工作量。
RPC 与微服务
有了 RPC,可以轻松实现微服务架构,为服务之间的通信提供了高效、可靠的方式。
- 服务间通信:RPC 为微服务之间提供了一种标准化的通信方式,使得不同服务可以轻松地相互调用。
- 性能优化:相比于 RESTful API,RPC 通常能提供更高的性能,这对于频繁通信的微服务架构尤为重要。
- 服务契约:RPC 的接口定义语言(IDL)可以作为服务之间的契约,明确定义服务接口,有助于维护服务的一致性。
- 语言无关性:许多 RPC 框架支持多种编程语言,这使得在多语言微服务环境中,不同语言实现的服务可以无缝通信。
- 负载均衡:许多 RPC 框架内置了负载均衡功能,可以自动分配请求到多个服务实例,提高系统的可用性和扩展性。
总的来说,RPC 为微服务架构提供了一种高效、灵活的通信机制,有助于构建可扩展、高性能的分布式系统。