VINO/WANG返回博客 ←

BLOG / POST

RPC 远程过程调用

RPC 是一种让程序像调用本地方法一样调用远程服务的协议,LSP 中的 JSON-RPC 即是其典型应用。

  • rpc
  • protocol

RPC 的全称是 Remote Procedure Call,即「远程过程调用」,它是一种计算机协议。

作为一个 Neovim 玩家,你一定听过 LSP(Language Server Protocol,语言服务器协议)。不知道大家有没有想过,为什么你在编辑器中编写不同语言的代码,都能获取到一些语法提示。这其中就是 LSP 在工作。LSP 的作用是让开发工具通过「JSON-RPC」与语言服务器进行通信。

LSP 通信流程

上面的 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 为微服务架构提供了一种高效、灵活的通信机制,有助于构建可扩展、高性能的分布式系统。