首页 > 编程笔记 > Go语言笔记

Go语言RPC(模拟远程过程调用)

服务器开发中会使用RPC(Remote Procedure Call,远程过程调用)简化进程间通信的过程。RPC 能有效地封装通信过程,让远程的数据收发通信过程看起来就像本地的函数调用一样。

本例中,使用通道代替 Socket 实现 RPC 的过程。客户端与服务器运行在同一个进程,服务器和客户端在两个 goroutine 中运行。

我们先给出完整代码,然后再详细分析每一个部分。
  1. package main
  2.  
  3. import (
  4. "errors"
  5. "fmt"
  6. "time"
  7. )
  8.  
  9. // 模拟RPC客户端的请求和接收消息封装
  10. func RPCClient(ch chan string, req string) (string, error) {
  11.  
  12. // 向服务器发送请求
  13. ch <- req
  14.  
  15. // 等待服务器返回
  16. select {
  17. case ack := <-ch: // 接收到服务器返回数据
  18. return ack, nil
  19. case <-time.After(time.Second): // 超时
  20. return "", errors.New("Time out")
  21. }
  22. }
  23.  
  24. // 模拟RPC服务器端接收客户端请求和回应
  25. func RPCServer(ch chan string) {
  26. for {
  27. // 接收客户端请求
  28. data := <-ch
  29.  
  30. // 打印接收到的数据
  31. fmt.Println("server received:", data)
  32.  
  33. // 反馈给客户端收到
  34. ch <- "roger"
  35. }
  36. }
  37.  
  38. func main() {
  39.  
  40. // 创建一个无缓冲字符串通道
  41. ch := make(chan string)
  42.  
  43. // 并发执行服务器逻辑
  44. go RPCServer(ch)
  45.  
  46. // 客户端请求数据和接收数据
  47. recv, err := RPCClient(ch, "hi")
  48. if err != nil {
  49. // 发生错误打印
  50. fmt.Println(err)
  51. } else {
  52. // 正常接收到数据
  53. fmt.Println("client received", recv)
  54. }
  55.  
  56. }

客户端请求和接收封装

下面的代码封装了向服务器请求数据,等待服务器返回数据,如果请求方超时,该函数还会处理超时逻辑。

模拟 RPC 的代码:
  1. // 模拟RPC客户端的请求和接收消息封装
  2. func RPCClient(ch chan string, req string) (string, error) {
  3.  
  4. // 向服务器发送请求
  5. ch <- req
  6.  
  7. // 等待服务器返回
  8. select {
  9. case ack := <-ch: // 接收到服务器返回数据
  10. return ack, nil
  11. case <-time.After(time.Second): // 超时
  12. return "", errors.New("Time out")
  13. }
  14. }
代码说明如下:
RPCClient() 函数中,执行到 select 语句时,第 9 行和第 11 行的通道操作会同时开启。如果第 9 行的通道先返回,则执行第 10 行逻辑,表示正常接收到服务器数据;如果第 11 行的通道先返回,则执行第 12 行的逻辑,表示请求超时,返回错误。

服务器接收和反馈数据

服务器接收到客户端的任意数据后,先打印再通过通道返回给客户端一个固定字符串,表示服务器已经收到请求。
  1. // 模拟RPC服务器端接收客户端请求和回应
  2. func RPCServer(ch chan string) {
  3. for {
  4. // 接收客户端请求
  5. data := <-ch
  6.  
  7. // 打印接收到的数据
  8. fmt.Println("server received:", data)
  9.  
  10. //向客户端反馈已收到
  11. ch <- "roger"
  12. }
  13. }
代码说明如下:
运行整个程序,客户端可以正确收到服务器返回的数据,客户端 RPCClient() 函数的代码按下面代码中加粗部分的分支执行。
  1. // 等待服务器返回
  2. select {
  3. case ack := <-ch: // 接收到服务器返回数据
  4. return ack, nil
  5. case <-time.After(time.Second): // 超时
  6. return "", errors.New("Time out")
  7. }
程序输出如下:

server received: hi
client received roger

模拟超时

上面的例子虽然有客户端超时处理,但是永远不会触发,因为服务器的处理速度很快,也没有真正的网络延时或者“服务器宕机”的情况。因此,为了展示 select 中超时的处理,在服务器逻辑中增加一条语句,故意让服务器延时处理一段时间,造成客户端请求超时,代码如下:
  1. // 模拟RPC服务器端接收客户端请求和回应
  2. func RPCServer(ch chan string) {
  3. for {
  4. // 接收客户端请求
  5. data := <-ch
  6.  
  7. // 打印接收到的数据
  8. fmt.Println("server received:", data)
  9.  
  10. // 通过睡眠函数让程序执行阻塞2秒的任务
  11. time.Sleep(time.Second * 2)
  12.  
  13. // 反馈给客户端收到
  14. ch <- "roger"
  15. }
  16. }
第 11 行中,time.Sleep() 函数会让 goroutine 执行暂停 2 秒。使用这种方法模拟服务器延时,造成客户端超时。客户端处理超时 1 秒时通道就会返回:
  1. // 等待服务器返回
  2. select {
  3. case ack := <-ch: // 接收到服务器返回数据
  4. return ack, nil
  5. case <-time.After(time.Second): // 超时
  6. return "", errors.New("Time out")
  7. }
上面代码中,加黑部分的代码就会被执行。

主流程

主流程中会创建一个无缓冲的字符串格式通道。将通道传给服务器的 RPCServer() 函数,这个函数并发执行。使用 RPCClient() 函数通过 ch 对服务器发出 RPC 请求,同时接收服务器反馈数据或者等待超时。参考下面代码:
  1. func main() {
  2.  
  3. // 创建一个无缓冲字符串通道
  4. ch := make(chan string)
  5.  
  6. // 并发执行服务器逻辑
  7. go RPCServer(ch)
  8.  
  9. // 客户端请求数据和接收数据
  10. recv, err := RPCClient(ch, "hi")
  11. if err != nil {
  12. // 发生错误打印
  13. fmt.Println(err)
  14. } else {
  15. // 正常接收到数据
  16. fmt.Println("client received", recv)
  17. }
  18.  
  19. }
代码说明如下:

所有教程

优秀文章