> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 认识RPC
- URL: https://huizhou92.com/ren-shi-rpc/
- Published: 2024-08-13T10:24:42.000Z
- Updated: 2026-09-06T11:40:24.000Z
- Author: huizhou92
- Tags: tech, RPC, go, #lang-zh-cn

RPC是远程过程调用的简称，是分布式系统中不同节点间流行的通信方式。是互联网时代的基石技术，Go语言的标准库也提供了一个简单的RPC实现，Go语言的RPC包的路径为`net/rpc`，本篇文章的目的是利用`net/rpc` 实现一个简单的RPC 接口，帮助我们拨开RPC 的迷雾。

对 `net/rpc` 而言，一个函数需要能够被远程调用，需要满足如下[五个条件](https://go.googlesource.com/go/blob/9177e12ccc2115e44de824ae7247ace88617c29a/src/net/rpc/server.go?ref=huizhou92.com#L13)：

> - the method’s type is exported.
> - the method is exported.
> - the method has two arguments, both exported (or builtin) types.
> - the method’s second argument is a pointer.
> - the method has return type error.

也就是说，必须满足。

```go
func (t *T) MethodName(argType T1, replyType *T2) error

```

## 简单的RPC请求

基于这五点要求，我们可以构建一个简单的RPC接口

```go
type HelloService struct{}  
func (p *HelloService) Hello(request string, reply *string) error {  
    log.Println("HelloService Hello")  
    *reply = "hello:" + request  
    return nil  
}

```

然后就可以将HelloService类型的对象注册为一个RPC服务。

```go
func main() {
	_ = rpc.RegisterName("HelloService", new(HelloService))  
	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)  
	}
}

```

客户端的实现如下：

```go
func main() {
	conn, err := net.Dial("tcp", ":1234")
	if err != nil {
		log.Fatal("net.Dial:", err)
	}
	client := rpc.NewClient(conn)
	var reply string
	err = client.Call("HelloService.Hello", "hello", &reply)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reply)
}

```

首先是通过`rpc.Dial` Dail a RPC服务，然后再通过`client.Call()`调用具体的RPC方法。第一个参数是用**点号链接的RPC服务名字和方法名字**，第二个参数是入参，第三个为返回值，是一个指针。  
由这个例子可以看出RPC的使用其实非常简单。  
在 Server 与Client 的代码中，我们都需要去记忆 RPC 服务的名字`HelloService` 以及，接口名字`Hello`。这在开发过程中，很容易出错，我们可以稍微封装一下。将公共的部分抽出来，完整的代码如下：

```go
// server.go
const ServerName = "HelloService"  
  
type HelloServiceInterface = interface {  
    Hello(request string, reply *string) error  
}  
  
func RegisterHelloService(srv HelloServiceInterface) error {  
    return rpc.RegisterName(ServerName, srv)  
}  
  
type HelloService struct{}  
  
func (p *HelloService) Hello(request string, reply *string) error {  
    log.Println("HelloService Hello")  
    *reply = "hello:" + request  
    return nil  
}

func main() {  
    _ = RegisterHelloService(new(HelloService))  
    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)  
    }  
}

```

```go
// client.go

type HelloServiceClient struct {  
    *rpc.Client  
}  
  
var _ HelloServiceInterface = (*HelloServiceClient)(nil)  

const ServerName = "HelloService" 

func DialHelloService(network, address string) (*HelloServiceClient, error) {  
    conn, err := net.Dial(network, address)  
    client := rpc.NewClient(conn)  
    if err != nil {  
       return nil, err  
    }  
    return &HelloServiceClient{Client: client}, nil  
}

func (p *HelloServiceClient) Hello(request string, reply *string) error {  
    return p.Client.Call(ServerName+".Hello", request, reply)  
}
func main() {
	client, err := DialHelloService("tcp", "localhost:1234")  
	if err != nil {  
	    log.Fatal("net.Dial:", err)  
	}  
	var reply string  
	err = client.Hello("hello", &reply)  
	if err != nil {  
	    log.Fatal(err)  
	}  
	fmt.Println(reply)
}

```

是不是已经有点眼熟了？

## Codec

标准库的RPC默认采用Go语言特有的[Gob编码](https://pkg.go.dev/net/rpc/jsonrpc?ref=huizhou92.com#NewClientCodec)，但是我们很容易在上面实现其他编码，比如`Protobuf`，`JSON` 等。标准库中已经支持了`jsonrpc`编码，我们只需要稍微改动一下服务端与客户端代码，就能实现JSON编码

```go
// server.go
func main() {  
    _ = rpc.RegisterName("HelloService", new(HelloService))  
    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.ServeCodec(jsonrpc.NewServerCodec(conn))  
       //go rpc.ServeConn(conn)  
    }  
}

//client.go
func DialHelloService(network, address string) (*HelloServiceClient, error) {  
    conn, err := net.Dial(network, address)  
    //client := rpc.NewClient(conn)  
    client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))  
    if err != nil {  
       return nil, err  
    }  
    return &HelloServiceClient{Client: client}, nil  
}

```

请求的JSON数据对象在内部对应两个结构体：客户端是[clientRequest](https://go.googlesource.com/go/blob/9177e12ccc2115e44de824ae7247ace88617c29a/src/net/rpc/jsonrpc/client.go?ref=huizhou92.com#L46)，服务器端是 [serverRequest](https://go.googlesource.com/go/blob/9177e12ccc2115e44de824ae7247ace88617c29a/src/net/rpc/jsonrpc/server.go?ref=huizhou92.com#L46) 。clientRequest和serverRequest结构体的内容基本是一致的：

```go
type clientRequest struct {  
    Method string `json:"method"`  
    Params [1]any `json:"params"`  
    Id     uint64 `json:"id"`  
}
type serverRequest struct {  
    Method string           `json:"method"`  
    Params *json.RawMessage `json:"params"`  
    Id     *json.RawMessage `json:"id"`  
}

```

其中，Method 表示服务名字，它是 serviceName +Method 组成。 params部分的第一个元素为参数，id是由调用方维护的唯一的调用编号。用于在并发场场景下区分请求。  
我们可以用`nc` 来模拟服务端，然后运行客户端代码，看看使用json编码的客户端会给服务端发送什么信息，

```shell
 nc -l 1234

```

`nc` 收到的数据为：

```shell
 {"method":"HelloService.Hello","params":["hello"],"id":0}

```

跟 [serverRequest](https://go.googlesource.com/go/blob/9177e12ccc2115e44de824ae7247ace88617c29a/src/net/rpc/jsonrpc/server.go?ref=huizhou92.com#L46) 一致。  
我们也可以运行 服务端代码，然后使用 nc 发送请求。

```shell
echo -e '{"method":"HelloService.Hello","params":["Hello"],"Id":1}' | nc localhost 1234 
--- 
{"id":1,"result":"hello:Hello","error":null}

```

## 总结

本文介绍了 Go 标准库中的`rpc`，它使用非常简单，性能异常强大。很多`rpc`的第三方库都是对`rpc`的封装。文章很简单，等于是给RPC研究系列开了一个头。下一篇文章，我们会将`protobuf` 跟RPC结合起来，最终，我们会实现一个自己的RPC框架。