Golang远程调用框架RPC的具体使用
梦想画家 人气:0gRPC
gRPC远程过程调用框架是基于动作的模式,类似远程调用微服务。这使得gRPC成为一种围绕Protobufs构建的进程间通信(IPC)协议,用于处理客户端和服务器之间的消息传递。gRPC非常适合密集而高效的通信,因为它支持客户端和服务器流。
与REST对比
REST是一种基于资源的协议,客户端根据请求体告诉服务器需要创建、读取、更新或删除哪些资源。gRPC还可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。gRPC为什么比REST更快?
gRPC利用HTTP/2协议,提供多种方式提供性能:
- 报头压缩和重用以减少消息大小
- 在单个TCP连接上同时发送多个请求和接收多个响应的多路复用
- 持久TCP连接用于单个TCP连接上的多个连续请求和响应
- 二进制格式支持,如协议缓冲区
需求说明
Go RPC服务器允许注册任何Go类型及其方法,通过RPC协议公开这些方法,即可以从远程客户端按名称进行调用。我们要实现的需求可以简单描述为:
- 用户可以添加书籍的信息
- 用户添加书籍的阅读进度
- 用户查询书籍的阅读进度
当然我们还可以实现其他更复杂的功能,这里为了简化,仅说明这几个方法。下面首先定义Book类型:
// Book represents a book entry type Book struct { ISBN string Title, Author string Year, Pages int } // ReadingList keeps tracks of books and pages read type ReadingList struct { Books []Book Progress []int }
ReadingList 类内部包括两个slice,模拟数据库存储书籍及对应进度。下面定义助手方法:
func (r *ReadingList) bookIndex(isbn string) int { for i := range r.Books { if isbn == r.Books[i].ISBN { return i } } return -1 }
上面通过ISBN查询书籍对应索引号,现在继续在ReadingList类型上定义几个方法:
// AddBook checks if the book is not present and adds it func (r *ReadingList) AddBook(b Book) error { if b.ISBN == "" { return ErrISBN } if r.bookIndex(b.ISBN) != -1 { return ErrDuplicate } r.Books = append(r.Books, b) r.Progress = append(r.Progress, 0) return nil } // GetProgress returns the progress of a book func (r *ReadingList) GetProgress(isbn string) (int, error) { if isbn == "" { return -1, ErrISBN } i := r.bookIndex(isbn) if i == -1 { return -1, ErrMissing } return r.Progress[i], nil } // 设置进度 func (r *ReadingList) SetProgress(isbn string, pages int) error { if isbn == "" { return ErrISBN } i := r.bookIndex(isbn) if i == -1 { return ErrMissing } if p := r.Books[i].Pages; pages > p { pages = p } r.Progress[i] = pages return nil }
我们需求就是通过RPC协议公开这些方法,让客户端进行远程调用。下面实现RPC服务器。
创建RPC服务器
上节已经准备好了公开的方法,创建RPC服务需要遵守一些规则:
- 方法的类型和方法自身必须是公开的(大写开头)
- 方法有两个参数,类型也是公开的
- 第二个参数是指针类型
- 方法返回一个error类型
语法如下:
func (t *T) Method(in T1, out *T2) error
知道了方法规范,下面对上节的方法进行包装,首先我们定义几个错误类型:
// List of errors var ( ErrISBN = fmt.Errorf("missing ISBN") ErrDuplicate = fmt.Errorf("duplicate book") ErrMissing = fmt.Errorf("missing book") )
再定义一个助手方法:
// sets the success pointer value from error func setSuccess(err error, b *bool) error { *b = err == nil return err }
首先实现Addbook和GetProgress包装方法:
func (r *ReadingService) AddBook(b Book, success *bool) error { return setSuccess(r.ReadingList.AddBook(b), success) } func (r *ReadingService) GetProgress(isbn string, pages *int) (err error) { *pages, err = r.ReadingList.GetProgress(isbn) return err }
由于更新阅读进度需要传入两个参数,但rpc方法不能传入多个参数,因此我们定义类型进行参数封装:
type Progress struct { ISBN string Pages int } func (r *ReadingService) SetProgress(p Progress, success *bool) error { return setSuccess(r.ReadingList.SetProgress(p.ISBN, p.Pages), success) }
准备了包装方法,下面定义RPC服务就很简单了:
func main() { if err := rpc.Register(&book.ReadingService{}); err != nil { log.Fatalln(err) } rpc.HandleHTTP() l, err := net.Listen("tcp", ":8900") if err != nil { log.Fatalln(err) } log.Println("Server Started") if err := http.Serve(l, nil); err != nil { log.Fatal(err) } }
服务端实现完毕,下面实现客户端。基础类型Book,Progress再客户端也需要使用,因此可以独立定义进行共享。
// Book represents a book entry type Book struct { ISBN string Title, Author string Year, Pages int } type Progress struct { ISBN string Pages int }
实现客户端
首先定义测试数据:
const hp = "H.P. Lovecraft" var books = []Book{ {ISBN: "1540335534", Author: hp, Title: "The Call of Cthulhu", Pages: 36}, {ISBN: "1980722803", Author: hp, Title: "The Dunwich Horror ", Pages: 53}, {ISBN: "197620299X", Author: hp, Title: "The Shadow Over Innsmouth", Pages: 40}, {ISBN: "1540335536", Author: hp, Title: "The Case of Charles Dexter Ward", Pages: 176}, }
下面定义rpc客户端:
client, err := rpc.DialHTTP("tcp", ":8900") if err != nil { log.Fatalln(err) } defer client.Close()
连接服务端后,可以调用服务端方法,下面定义统一方法进行调用:
func callClient(client *rpc.Client, method string, in, out interface{}) { var r interface{} if err := client.Call(method, in, out); err != nil { out = err } switch v := out.(type) { case error: r = v case *int: r = *v case *bool: r = *v } log.Printf("%s: [%+v] -> %+v", method, in, r) }
最后是循环测试数据,依此调用RPC方法:
for i, book := range books { callClient(client, "ReadingService.AddBook", book, new(bool)) callClient(client, "ReadingService.SetProgress", Progress{ ISBN: book.ISBN, Pages: 10 + i*i, }, new(bool)) callClient(client, "ReadingService.GetProgress", book.ISBN, new(int)) } }
完整代码如下:
package main import ( log "log" "net/rpc" ) func main() { client, err := rpc.DialHTTP("tcp", ":8900") if err != nil { log.Fatalln(err) } defer client.Close() for i, book := range books { callClient(client, "ReadingService.AddBook", book, new(bool)) callClient(client, "ReadingService.SetProgress", Progress{ ISBN: book.ISBN, Pages: 10 + i*i, }, new(bool)) callClient(client, "ReadingService.GetProgress", book.ISBN, new(int)) } } // Book represents a book entry type Book struct { ISBN string Title, Author string Year, Pages int } type Progress struct { ISBN string Pages int } const hp = "H.P. Lovecraft" var books = []Book{ {ISBN: "1540335534", Author: hp, Title: "The Call of Cthulhu", Pages: 36}, {ISBN: "1980722803", Author: hp, Title: "The Dunwich Horror ", Pages: 53}, {ISBN: "197620299X", Author: hp, Title: "The Shadow Over Innsmouth", Pages: 40}, {ISBN: "1540335536", Author: hp, Title: "The Case of Charles Dexter Ward", Pages: 176}, } func callClient(client *rpc.Client, method string, in, out interface{}) { var r interface{} if err := client.Call(method, in, out); err != nil { out = err } switch v := out.(type) { case error: r = v case *int: r = *v case *bool: r = *v } log.Printf("%s: [%+v] -> %+v", method, in, r) }
加载全部内容