Go语言高并发与微服务实战专题精讲——远程过程调用 RPC——实践案例:Go 语言 RPC 过程调用实践

远程过程调用 RPC —— 实践案例:Go语言RPC过程调用实践

  Go语言的官方RPC库net/rpc为开发者提供了实现远程过程调用的强大功能,使得通过网络访问对象的方法成为可能。这种机制极大地促进了分布式系统的构建,让不同的服务能够轻松地进行相互通信和协作。

  在使用Go的RPC库时,服务器需要注册相应的对象,并通过对象的类型名来公开其服务。对象一旦注册成功,其公开的方法便可进行远程调用。该库精心封装了底层实现的细节,例如序列化、网络传输和反射调用等,从而极大地简化了RPC的开发流程。

  服务器可以注册多个不同类型的对象以提供丰富的服务,但需注意,不能同时注册多个相同类型的对象。此外,为了让对象的方法能够被远程调用,它们必须满足以下条件:

    1. 方法的类型必须是可导出的(即首字母大写),以确保外部能够访问。
    2. 方法本身也必须是可导出的。
    3. 方法必须接受两个参数。第一个参数是从客户端接收的数据,第二个参数是用于返回数据的结构体指针。这两个参数的类型必须是可导出的或者是Go语言的内建类型。
    4. 方法的第二个参数必须是指针类型,以便能够修改其指向的数据并将修改后的数据返回给客户端。
    5. 方法的返回类型必须是error,用于指示方法调用过程中是否发生了错误。

  满足上述条件的能够进行远程调用的方法格式通常如下所示:

func (t *YourType) YourRemoteProcedure(args *ArgsType, reply *ReplyType) error {  
    // 实现远程过程的逻辑  
    // ...  
  
    // 如果有错误发生,返回一个非nil的错误  
    // 如果没有错误,将结果填充到reply中,并返回nil  
    return nil  
}
  在这里,YourType是你希望暴露给RPC服务的类型,ArgsType是传递给远程过程的参数类型,而ReplyType是用于存储过程返回结果的类型。YourRemoteProcedure是具体的远程过程名称,它会被RPC客户端调用。
  这样的格式和规则确保了RPC服务的清晰性和一致性,同时也为开发者提供了一个明确且易于遵循的框架来构建分布式系统中的远程调用接口。
  Go 语言原生的RPC过程调用实现起来非常简单。服务端只需要实现对外提供的远程过程调用方法和结构体,然后将其注册到RPC服务中,然后客户端就可以通过其服务名称和方法名称进行RPC方法调用。

(1)首先,我们定义远程过程调用的请求和响应数据结构。例如,对于字符串操作的请求,我们可以定义如下结构,调用字符串操作的请求包括两个参数:字符串A和字符串B。

type StringRequest struct {  
    A string  
    B string  
}  
  
type StringResponse struct {  
    Value string  
}

(2)接下来,我们定义一个服务接口,比如Service,并给出两个方法ConcatDiff:

type Service interface {  
    Concat(req *StringRequest, resp *StringResponse) error  
    Diff(req *StringRequest, resp *StringResponse) error  
}

在 Go 语言的 RPC(远程过程调用)框架中,response定义为指针类型有几个重要的原因:

    1. 性能优化: RPC调用中, 请求和响应的数据通常需要经过网络传输。如果response不是指针类型, 那么每次函数调用时都会创建一个新的response对象, 并且需要将整个对象在网络上发送和接收。而如果response是指针类型, 那么只需要发送和接收指针所指向的数据的内存地址,这可以减少网络传输的数据量,从而提高性能。当然,在实际的网络通信中,指针本身并不会被直接传输,而是传输指针指向的数据内容;但使用指针意味着服务端可以直接修改传入的响应对象的字段,而不需要复制整个对象。

    2. 能够修改传入的对象: response是指针类型时,函数可以直接修改指针所指向的数据,这样调用方可以得到修改后的结果。在RPC调用中,服务端通常需要填充response对象以返回结果给客户端。如果>response不是指针类型,服务端将只能修改其本地副本,而无法修改调用方传入的实际对象。

    3. 内存效率: 在函数调用过程中,如果response是值类型(非指针类型),那么当函数被调用时,response将会被复制到函数的栈上。对于大的数据结构,这可能会导致不必要的内存分配和复制开销。而使用指针可以避免这种开销,因为只需要传递指向数据的引用。

  综上所述,将response定义为指针类型在 RPC 调用中是常见的做法,它有助于优化性能、减少内存使用,并允许服务端直接修改并返回结果给客户端。这种做法符合 Go 语言的设计哲学,即尽量减少不必要的内存分配和复制操作,提高程序的效率和性能。

(3)然后,我们实现这个Service接口:

type StringService struct{}  
  
func (s *StringService) Concat(req *StringRequest, resp *StringResponse) error {  
    resp.Value = req.A + req.B  
    return nil  
}  
  
func (s *StringService) Diff(req *StringRequest, resp *StringResponse) error {  
    // Diff方法是返回两个字符串的不同部分  
    resp.Value = strings.Replace(req.A, req.B, "", -1)  
    return nil  
}


(4)之后,我们需要将服务注册到RPC服务中。这通常涉及到创建一个RPC服务器,注册服务,并开始监听请求。以下是一个简单的RPC服务器设置示例:

import (  
    "log"  
    "net"  
    "net/rpc"  
)  
  
func main() {  
    // 实例化服务对象  
    service := new(StringService)  
      
    // 注册服务  
    rpc.Register(service)  
      
    // 设置RPC监听  
    l, err := net.Listen("tcp", ":1234")  
    if err != nil {  
        log.Fatal("Listen error:", err)  
    }  
    defer l.Close()  
      
    // 接受并处理RPC请求  
    for {  
        conn, err := l.Accept()  
        if err != nil {  
            log.Fatal("Accept error:", err)  
        }  
        go rpc.ServeConn(conn)  
    }  
}

现在,服务端已经准备好接受RPC请求了。客户端可以通过RPC调用Concat和Diff方法。

让我们看看 rpc-server.go 的完整代码:

package main  
  
import (  
	"log"  
	"net"  
	"net/rpc"  
)  
  
// StringRequest 表示字符串操作的 RPC 请求结构。  
type StringRequest struct {  
	A string  
	B string  
}  
  
// StringResponse 表示字符串操作的 RPC 响应结构。  
type StringResponse struct {  
	Value string  
}  
  
// Service 接口定义了 RPC 服务器将暴露的方法。  
type Service interface {  
	Concat(req *StringRequest, resp *StringResponse) error  
	Diff(req *StringRequest, resp *StringResponse) error  
}  
  
// StringService 是 Service 接口的实现。  
type StringService struct{}  
  
func (s *StringService) Concat(req *StringRequest, resp *StringResponse) error {  
	resp.Value = req.A + req.B // 拼接字符串
	return nil  
}  
  
func (s *StringService) Diff(req *StringRequest, resp *StringResponse) error {  
	/*参数说明:
		s:源字符串,即需要进行替换操作的字符串。
		old:需要被替换的子串。
		new:替换后的新子串。
		n:指定替换操作的次数。如果n为-1,则表示替换所有old子串;如果n为一个非负整数,则替换前n个old子串。
	*/
	resp.Value = strings.Replace(req.A, req.B, "", -1) // 函数返回一个新的字符串,其中源字符串s中前n个old子串被new替换。
	return nil  
}  
  
func main() {  
	// 实例化服务对象。  
	service := new(StringService)  
  
	// 注册服务到 RPC 服务器。  
	rpc.Register(service)  
  
	// 设置 RPC 监听在指定端口。  
	l, err := net.Listen("tcp", ":1234")  
	if err != nil {  
		log.Fatal("监听错误:", err)  
	}  
	defer l.Close()  
  
	// 接受并处理 RPC 请求。  
	for {  
		conn, err := l.Accept()  
		if err != nil {  
			log.Fatal("接受错误:", err)  
		}  
		go rpc.ServeConn(conn)  
	}  
}

运行 rpc-server.go 的的代码:

让我们看看 rpc-client.go 的完整代码:

package main

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

// StringRequest 和 StringResponse 与服务器代码共享。
// 在实际场景中,可能会将它们放在一个公共包中。
type StringRequest struct {
	A string
	B string
}

type StringResponse struct {
	Value string
}

func main() {
	// 连接到 RPC 服务器。
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
		log.Fatal("拨号错误:", err)
	}

	// 准备 Concat 和 Diff 方法的请求。
	reqConcat := &StringRequest{A: "Hello, ", B: "World!"}
	reqDiff := &StringRequest{A: "Hello, World!", B: "World!"}

	// 准备响应容器。
	var respConcat StringResponse
	var respDiff StringResponse

	// 调用远程 Concat 方法。
	err = client.Call("StringService.Concat", reqConcat, &respConcat)
	if err != nil {
		log.Fatal("Concat RPC 调用错误:", err)
	}
	fmt.Println("Concat 结果:", respConcat.Value)

	// 调用远程 Diff 方法。
	err = client.Call("StringService.Diff", reqDiff, &respDiff)
	if err != nil {
		log.Fatal("Diff RPC 调用错误:", err)
	}
	fmt.Println("Diff 结果:", respDiff.Value)
}

先运行了 rpc-server.go 的代码后,再运行 rpc-client.go 的的代码:  

posted @ 2024-04-19 22:02  左扬  阅读(17)  评论(0编辑  收藏  举报
levels of contents