【Go网络编程:Go如何通过RPC实现跨平台服务】

什么是RPC服务

​ RPC,就是远程过程调用,是分布式系统中不同节点调用的方式(进程间通信),属于 C/S 模式。 RPC 由客户端发起,调用服务端的方法进行通信,然后服务端把结果返回给客户端。
​ RPC 的核心有两个: 通信协议和序列化。在 HTTP2.0之前,一般采用自定义 TCP 协议的方式进行通信,HTTP2.0出来后,也有采用该协议的, 比如流行的 gRPC。 序列化和反序列化是一种把传输的内容编码和解码的方式,常见的编解码方式有 JSON、Protobuf等。

​ 在大多数的RPC架构设计中,都有Client、Client Stub、Servere、Server Stub这四个组件,Client 和 Server 之间通过 Socket 进行通信。RPC 架构设计如图:

image-20211222143818485

RPC 调用的流程:

1. 客户端(Client) 调用客户端存根(Client Stub) ,同时把参数传给客户端存根;
1. 客户端存根将参数打包编码,并通过系统调用发送到服务器;
1. 客户端本地系统发送消息到服务器;
1. 服务器系统将信息发送到服务器存根(Server Stub);
1. 服务端存根解析信息,也就是解码;
1. 服务端存根调用真正的服务端程序(Server);
1. 服务端(Server) 处理后,通过同样的方式,把结果返回给客户端(Client)。

RPC 调用常用于大型项目,即我们常说的微服务,而且还有包含服务注册、治理、监控等功能,是一套完整的体系

Go 语言 RPC 简单入门

RPC很流行,Go语言当然也会支持,在 Go SDK 中,已经内置 net/rpc包来帮助开发者实现RPC。简单来说,net/rpc 包提供了通过网络访问服务端对象方法的能力。演示一个服务端代码:

package server

type MathServer struct{
    
}

type Args struct {
    A, B int
}

func (m *MathServer) Add(args Args, reply *int) error {
    *reply = args.A + args.B
    return nil
}

// 示例代码中:
// 定义 MathServer,用于表示一个远程服务对象
// Args 结构体用于表示参数
// Add 这个方法实现了加法功能,加法结果通过 reply 这个指针变量返回

有了定义好的服务对象,就可以把它注册到暴露的服务列表中,以供其他客户端使用了。在Go 语言中,要注册一个RPC服务对象还是很简单的,通过 RegisterName 方法即可,示例如下:

package main

import (
	"gotest/server"
    "log"
    "net"
    "net/rpc"
)

func main() {
    rpc.RegisterName("MathService", new(server.MathService))
    l, e := net.Listen("tcp", ":1111")
    if e != nil {
        log.Fatal("listen er")
    }
    
    rpc.Accept(l)
}


// 示例中, 通过 RegisterName 函数注册了一个服务对象,该函数接收两个参数:
// 服务名称(MathService)
// 具体服务对象,即刚定义的MathService 结构体

// 然后通过 net.Listen 函数简历一个 TCP 连接,在1111端口监听,最后通过rpc.Accept函数在该 TCP 链接上提供 MathService 这个 RPC服务。现在服务端可以看到MathService 这个服务以及它的Add方法了。

​ 任何一个框架都有自己的规则, net/rpc 这个 Go语言提供的RPC框架也不例外。要想把一个对象注册为 RPC 服务,可以让 客户端远程访问,那么该对象(类型)的方法必须满足下条件:

  1. 方法的类型是可导出的(公开的);
  2. 方法本身也是可导出的;
  3. 方法必须有两个参数,并且参数类型是可导出或內建的;
  4. 方法必须返回一个 error 类型
func (t *T) MethName(argType T1, replyType *T2) error

T1、T2都是可以被encoding/gob 序列化的。

  • argType 是调用者(客户端)提供的
  • replyType 是返回给调用者的结果,必须是指针类型

有了提供好的RPC服务,这样客户端就可以调用了,示例代码如下:

package main

import (
	"fmt"
    "gotest/server"
    "lgo"
    "net/rpc"
)

func main() {
    client, err := rpc.Dial("tcp", "localhost:1111")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    
    args := server.Args{A:1, B:2}
    
   	var reply int
    err = client.Call("MathService.Add", args, &reply)
    if err != nil {
        log.Fatal("MathService.Add error:", err)
    }
    
    fmt.Println("MathService.Add:  %d+%d=%d", args.A, args.B, reply)
}

// 示例中,首先通过rpc.Dial 函数建立TCP链接, 需要注意的是 IP、 端口要和 RPC 服务提供的一致,确保可以建立TPC链接。

// TCP链接建立成功后,就需要准备远程方法需要的参数,即args和reply。参数准备好后,可以通过Call方法调用远程的RPC服务了。Call有三个参数,它们的作用分别介绍:
// 1 调用的远程方法的名字,MathService.Add, 点前面的是注册的服务名称, 点后面的是该服务的方法
// 2 客户端为了调用远程方法提供的参数,这里是 args
// 3 为了接收远程方法返回的结果,必须是一个指针,这样客户端就可以获得服务端返回的结果了



// 让我们来运行一下,看看效果
go run gotest/server_main.go

go run gotest/client_main.go

基于HTTP 的RPC

​ RPC除了可以通过TCP协议调用之外,还可以通过HTTP协议进行通信,而且内置的 net/rpc 包已经支持,修改上述示例,支持HTTP协议。

// service
fun main() {
    rpc.RegisterName("MathService", new(service.MathService))
    rpc.HandleHTTP()
    l,e := net.Listen("tcp", ":1111") 
    if e != nil {
        log.Fatal("listen error:", e)
    }
    
    http.Serve(l, nil)
    
}


// client
func main() {
    client, err := rpc.DialHTTP("tcp", "localhost:1111")
}

// 客户端调用只需要把建立链接的方法从 Dial 换成 DialHTTP 即可。

​ 此外,Go语言的 net/rpc 包提供的 HTTP协议的 RPC还有一个调试的URL,运行服务端代码后,在浏览器舒服 http://localhost:1111/debug/rpc 回车,即可看到服务端注册的RPC服务,以及每个服务的方法,如图
image-20211222161707082

JSON RPC 跨平台通信

​ 上面的RPC示例服务是基于gob编码的,这种编码在跨语言调用的时候比较困难,当前在微服务架构中,RPC 服务的实现者和调用者都可能是不同的编程语言,因为我们实现的RPC服务要支持多语言的调用。

基于 TCP 的 JSON RPC

实现跨语言 RPC 服务的核心在于选择一个通用的编码,这样大多数语言都支持,比如常用的JSON。在Go语言中,实现一个 JSON RPC服务非常简单,只需要 net/rpc/jsonrpc包即可。我们改造一下上述示例,支持 JSON的RPC服务,如下:

// service
func main() {
    rpc.RegisterName("MathService", new(server.MathService))
    l, e := net.Listen("tcp", ":1111")
    if e != nil {
        log.Fatal("listen error:", e)
    }
    
    for {
        conn, e := l.Accept()
        if e != nil {
            log.Println("jsonrpc,Server: accept:", err.Error())
            return 
        }
        
        // json rpc
        go jsonrpc.ServeConn(conn)
    }
}
// 相比gob编码的RPC服务, JSON的RPC服务把链接建立交到 jsonrpc.ServeConn 函数处理,达到了基于JSON进行RPC调用的目的


// client
func main(){
    client, err := jsonrpc.Dial("tcp", "localhost:1111")
}
// 只需要把建立链接的Dial方法换成 jsonrpc 包中的即可。

​ 其他编程语言只需要遵守JSON-RPC 规范 即可。

基于HTTP的JSON RPC

​ 相比基于 TCP 调用的RPC来说,使用 HTTP 会更方便,也更通用。Go语言内置的 jsonrpc 并没有实现基于 HTTP的传输,所以要自己实现,我们参考 gob 编码的HTTP RPC实现方式,来实现基于HTTP 的JSON RPC 服务

// service
func main () {
    rpc.RegisterName("MathService", new(service.MathService))
    // 注册一个path,用于提供基于http的json rpc 服务
    http.HanleFunc(rpc.DefaultRPCPath, func(rw http.ResponseWriter, r *http.Sequest) {
        conn, err := rw.(http.Hijacker).Hijack()
        if err != nil {
            log.Print("rpc hijacking", r.RemoteAddr, ":", err.Error())
            return
        }
        
        var connected = "200 Connected to JSON RPC"
        io.WriteString(conn, "HTTP/1.0 " + connected+"\n\n")
        jsonrpc.ServeConn(conn)
    })
    
    l, e := net.Listen("tcp", ":1111")
    if e != nil {
        log.Fatal("listen error: ", err)
    }
    http.Serve(l, nil)
}

// 以上代码的实现基于HTTP协议的核心,即使用 http.HandleFunc 注册一个path,对外提供基于HTTP 的JSON RPC服务。 在这个HTTP服务的实现中,通过 HiJack 方法劫持链接,然后转交给jsonrpc处理,这样就实现了基于 HTTP 协议的 JSON RPC服务。



// client 
func main () {
    client, err := DialHTTP("tcp", "localhost:1111")
    if err != nil {
        log.Fatal("dialing: ", err)
    }
    
    args := server.Args{a:1, B:1}
    var reply int
    err = client.Call("MathService.Add", args, &reply)
    if err != nil {
        log.Fatal("MathService.Add error:", err)
    }
    fmt.Printf("MathService.Add: %d+%d= %d", args.A, args.B, reply)
}


// DialHTTP  connects to an HTTP RPC server at the specified network address listening on the default HTTP RPC path.

func DialHTTP(network, address string) (*rpc.Client, error) {
    return DialHTTPPath(network, address, rpc.DefaultRPCPath)
}

// DialHTTPPath connects to an HTTP RPC server at the specified network address and path.

func DialHTTPPath(network, address, path string) (*rpc.Client, error) {
    var err error
    conn, err := net.Dial(network, address)
    if err != nil {
        retun nil, err
    }
    
    io.WriteString(conn, "GET" + path + " HTTP/1.0 \n\n")
    // Require successful HTTP response
     // before switching to RPC protocol.
     resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "GET"})
     connected := "200 Connected to JSON RPC"
     if err == nil && resp.Status == connected {
        return jsonrpc.NewClient(conn), nil
     }
     if err == nil {
        err = errors.New("unexpected HTTP response: " + resp.Status)
     }
     conn.Close()
     return nil, &net.OpError{
        Op:   "dial-http",
        Net:  network + " " + address,
        Addr: nil,
        Err:  err,
     }
}

// 这段代码的核心在于通过建立好的TCP链接,发送HTTP请求调用远程的HTTP RPC服务,这里使用 HTTP GET方法。

总结

​ 我们了解了基于Go语言自带的RPC框架,学习了RPC服务的实现及调用。也了解了基于TCP 和 HTTP实现的RPC服务有什么不同,它们是如何实现的。实际开发中,使用Go语言原生的RPC框架并不多,大多数是第三方框架,比如 Google 的 gRPC框架,它是通过 Protobuf 序列化的,是基于 HTTP/2协议的二进制传输,并且支持很多编程语言,效率也比较高。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值