Go RPC 远程过程调用

Go RPC 远程过程调用

今天来学习 Go 语言的远程过程调用 RPC( Remote Procedure Call)。

在分布式计算,远程过程调用是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程。RPC是一种服务器-客户端模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

—— From WikiPedia

RPC 可以让客户端相对直接地访问服务端的函数,这里说的「相对直接」表示我们不需要在服务端自己写一些比如 web 服务的东西来提供接口,并且在两端手动做各种数据的编码、解码。

本文包括两部分,第一部分介绍 Golang 标准库的 net/rpc,第二部分动手实现一个玩具版 PRC 框架来加深理解。

Part0. net/rpc

这一部分参考 《Go语言高级编程》4.1 RPC入门。未尽之处可移步阅读原文。

Go 标准库的 net/rpc 实现了基本的 RPC,它使用一种 Go 语言特有的 Gob 编码方式,所以服务端、客户端都必须使用 Golang,不能跨语言调用。

对于服务端, net/rpc 要求用一个导出的结构体来表示 RPC 服务,这个结构体中所有符合特定要求的方法就是提供给客户端访问的:

type T struct {
   }

func (t *T) MethodName(argType T1, replyType *T2) error
  • 结构体是导出的。
  • 方法是导出的。
  • 方法有两个参数,都是导出的类型(或者内置类型)。
  • 方法的第二个参数是指针。
  • 方法的返回值是 error。

服务端通过 rpc.Dial(对 TCP 服务)连接服务端,然后用使用 Call 调用 RPC 服务中的方法:

rpc.Call("T.MethodName", argType T1, replyType *T2)

例如,用 net/rpc 实现一个 Hello World。

Hello World

服务端

首先构建一个 HelloService 来表示提供的服务:

// server.go

// HelloService is a RPC service for helloWorld
type HelloService struct {
   }

// Hello say hello to request
func (p *HelloService) Hello(request string, reply *string) error {
   
	*reply = "Hello, " + request
	return nil
}

接下来注册并开启 RPC 服务,我们可以基于 HTTP 服务:

// server.go

func main () {
   
    // 用将给客户端访问的名字和HelloService实例注册 RPC 服务
	rpc.RegisterName("HelloService", new(HelloService))

	// HTTP 服务
	rpc.HandleHTTP()
	err := http.ListenAndServe(":1234", nil)
	if err != nil {
   
		log.Fatal("Http Listen and Serve:", err)
	}
}

也可以使用 TCP 服务,替换上面的第 8~12 行代码:

	// TCP 服务
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
   
		log.Fatal("ListenTCP error:", err)
	}

	conn, err := listener.Accept()
	if err != nil {
   
		log.Fatal("Accept error:", err)
	}

	rpc.ServeConn(conn)

注意,这里服务端只 Accept 一个请求,在客户端请求过后就会自动关闭。如果需要一直保持处理,可以把后半部分代码换成:

    for {
   
        conn, err := listener.Accept()
        if err != nil {
   
            log.Fatal("Accept error:", err)
        }

        go rpc.ServeConn(conn)
    }

客户端

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

func main() {
   
	// HTTP
	// client, err := rpc.DialHTTP("tcp", "localhost:1234")
	
    //TCP
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
   
		log.Fatal("dialing:", err)
	}

	var reply string
	err = client.Call("HelloService.Hello", "world", &reply)
	if err != nil {
   
		log.Fatal(err)
	}

	fmt.Println(reply)
}

先启动服务端:

$ go run helloworld/server/server.go

在另一个终端调用客户端,即可得到结果:

$ go run helloworld/client/client.go
Hello, world

更规范的 RPC 接口

之前的代码服务端、客户端的注册、调用 RPC 服务都是写死的。所有的工作都放到了一块,相当不利于维护,需要考虑重构 HelloService 服务和客户端实现。

服务端

首先,用一个 interface 抽象服务接口:

// HelloServiceName is the name of HelloService
const HelloServiceName = "HelloService"

// HelloServiceInterface is a interface for HelloService
type HelloServiceInterface interface {
   
	Hello(request string, reply *string) error
}

// RegisterHelloService register the RPC service on svc
func RegisterHelloService(svc HelloServiceInterface) error {
   
	return rpc.RegisterName(HelloServiceName, svc)
}

在实例化服务时,注册用:

RegisterHelloService(new(HelloService))

其余的具体服务实现没有改变。

客户端

在客户端,考虑将 RPC 细节封装到一个客户端对象 HelloServiceClient 中:

// HelloServiceClient is a client for HelloService
type HelloServiceClient struct {
   
	*rpc.Client
}

var _ HelloServiceInterface = (*HelloServiceClient)(nil)

// DialHelloService dial HelloService
func DialHelloService(network, address string) (*HelloServiceClient, error) {
   
	c, err := rpc.Dial(network, address)
	if err != nil {
   
		return nil , err
	}
	return &HelloServiceClient{
   Client: c}, nil
}

// Hello calls HelloService.Hello
func (p *HelloServiceClient) Hello(request string, reply *string) error {
   
	return p.Client.Call(HelloServiceName + ".Hello", request, reply)
}

具体调用时,就不用去暴露处理 RPC 的细节了:

client, err := DialHelloService("tcp", "localhost:1234")
if err != nil {
   
    log.Fatal("dialing:", err)
}

var reply string
err = client.Hello("world", &reply)
if err != ni
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现RPC框架的过程可以分为以下几个步骤: 1. 定义服务接口:定义服务接口以及服务方法,并且确定参数和返回值的类型。 2. 定义服务端:服务端需要实现服务接口中的方法,并且提供网络通信功能,接收客户端请求并将请求转发给服务方法进行处理。 3. 定义客户端:客户端需要提供网络通信功能,以及调用远程服务的方法,并将请求发送给服务端进行处理。 4. 序列化和反序列化:客户端和服务端需要将数据进行序列化和反序列化,以便在网络中进行传输。 5. 注册服务:服务端需要将实现了服务接口的对象注册到服务框架中,以便客户端可以调用。 下面是使用Go语言实现RPC框架的代码示例: 1. 定义服务接口 ```go type Arith interface { Multiply(args *Args, reply *int) error } type Args struct { A, B int } ``` 2. 定义服务端 ```go type ArithService struct{} func (s *ArithService) Multiply(args *Args, reply *int) error { *reply = args.A * args.B return nil } func main() { rpc.Register(new(ArithService)) 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) } } ``` 3. 定义客户端 ```go func main() { client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("Dial error:", err) } args := &Args{7, 8} var reply int err = client.Call("Arith.Multiply", args, &reply) if err != nil { log.Fatal("Call error:", err) } fmt.Printf("Arith: %d*%d=%d", args.A, args.B, reply) } ``` 以上代码实现了一个简单的RPC框架,客户端通过`rpc.Dial`方法连接到服务端,然后调用`client.Call`方法调用远程服务。服务端通过`rpc.ServeConn`方法接收客户端请求并将请求转发给服务方法进行处理。注意,以上代码仅为示例代码,实际情况中需要考虑安全性和可靠性等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值