goalng中net/rpc的使用

夜遊神 發表於 2022-11-24
Go

一、基本使用方式說明

// server/server.go
package main

import (
    "net"
    "net/rpc"
)

type Args struct {
    A, B int
}

type Calculator int

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

func (t *Calculator) Sub(args *Args, reply *int) error {
    *reply = args.A - args.B
    return nil
}

func main() {
    // 1. 建立 rpc 服務端
    rpcServer := rpc.NewServer()

    // 2. 註冊服務
    // 待註冊的服務方法必須是公開的,2 個引數都是與 client 約定好的固定型別,且為指標;
    // 第 1 個為 client 提交的引數,第 2 個是給 client 的返回值。
    _ = rpcServer.Register(new(Calculator))

    // 3. 開啟監聽指定的公開埠(比如此處的 8090)
    l, _ := net.Listen("tcp", ":8090")

    // 4. 迴圈往復同 client 建立 tcp 連線,並開啟一個 goroutine 處理
    // 呼叫了 go server.ServeConn(conn)
    rpcServer.Accept(l)

    // 5. server.ServeConn(conn) 迴圈往復 接收請求、處理請求

    // 6. 處理單個請求時,必然是以 gob 壓縮資料, gob_encode(header) + gob_encode(body)
    //  header 為固定的資料結構 rpc.Request{},內容含 ServiceMethod、Seq(會返回給 client,client 可能會併發請求,根據返回的 Seq 區分是哪個請求)
    //  1) gob 先解析出固定結構的 header,
    //  2) 根據 header 中的 ServiceMethod 找到註冊的服務,
    //  3) 根據找到的服務確定同 client 約定好的該服務的 body 結構(client 提交的引數),
    //  4) gob 根據 body 結構解析出請求引數資訊,
    //  5) ServiceMethod + 引數,處理任務,完成後返回,
    //  6) 返回資訊同樣是 gob_encode(header) + gob_encode(body),header(rpc.Response{})中含 Seq,body 為同客戶端約定好的返回結構,body為約定好的reply結構體
}

// client/client.go
package main

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

type Args struct {
    A, B int
}

func main() {
    // 1. 建立 tcp 連線
    conn, _ := net.Dial("tcp", "127.0.0.1:8090")

    // 2. 根據 tcp 連線建立 client
    //    同時開啟一個 goroutine 迴圈往復讀取 server 返回的結果
    //    server 返回按照 header(rpc.Response{}: ServiceName+Seq) + body(具體服務約定好的返回結構)
    //    此步驟由於還沒有發出請求,暫時不會讀取到資料
    client := rpc.NewClient(conn)

    wg := &sync.WaitGroup{}
    wg.Add(2)

    // 3. client 可以併發發起請求
    //    但是由於使用了同一個 tcp 連線,為了不互相影響,是排隊寫入的
    //    透過加鎖,寫入一個完整的請求後[ header(rpc.Request{}: ServiceName+Seq) + body(具體的引數結構) ],再另外寫入一個請求
    //    server 讀取是按照約定,先讀取 header,確定 service,再讀取 body(具體的引數)
    go func() {
        args := &Args{100, 20}
        reply := new(int)
        // client.Call() 方法使用了 channel 進行阻塞,直到步驟 2 中的讀取到 server 返回的資料
        _ = client.Call("Calculator.Add", args, reply)
        fmt.Printf("Calculator.Add: %d + %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    go func() {
        args := &Args{100, 20}
        reply := new(int)
        _ = client.Call("Calculator.Sub", args, reply)
        fmt.Printf("Calculator.Sub: %d - %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    wg.Wait()
}

$ cd path/server
$ go run ./server.go

$ cd path/client
$ go run ./client.go
Calculator.Sub: 100 - 20 = 80
Calculator.Add: 100 + 20 = 120

二、利用已有的 DefaultServer 及 “http 轉 rpc”

net/rpc 包已有一個初始化好的 DefaultServer

且提供了有先透過 http 連線轉 rpc 連線的方法。

// server/server.go
package main

import (
    "net/http"
    "net/rpc"
)

type Args struct {
    A, B int
}

type Calculator int

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

func (t *Calculator) Sub(args *Args, reply *int) error {
    *reply = args.A - args.B
    return nil
}

func main() {
    // 1. 將 Calculator 服務註冊至預設的 rpc 伺服器 DefaultServer
    _ = rpc.Register(new(Calculator))
    // 2. DefaultServer 註冊至預設的 http 伺服器 DefaultServeMux
    //    其註冊的 http 地址為 /_goRPC_
    //    當 http 伺服器收到訪問地址 /_goRPC_ 的 http 請求時,會啟動一個 goroutine 呼叫 DefaultServer.ServeHTTP() 處理 http 請求
    //    DefaultServer.ServeHTTP() 同 client 進行完一輪 http 請求後,不會釋放當前 tcp 連線,而是轉為普通的 rpc 請求
    rpc.HandleHTTP()
    // 3. http 伺服器開始監聽 "埠 8090、地址 /_goRPC_" 的 http 請求,處理完 http 請求(相當於校驗)後,轉為 rpc 請求
    _ = http.ListenAndServe(":8090", nil)
}

// client/client.go
package main

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

type Args struct {
    A, B int
}

func main() {
    // 1. 傳送 http 請求至 "埠 8090、地址 /_goRPC_",
    //    等到 http 成功返回並校驗成功,將其轉為 rpc 請求,並建立 client 返回
    client, _ := rpc.DialHTTP("tcp", "127.0.0.1:8090")

    wg := &sync.WaitGroup{}
    wg.Add(2)
    go func() {
        args := &Args{100, 20}
        reply := new(int)
        // client.Call() 方法使用了 channel 進行阻塞,直到步驟 2 中的讀取到 server 返回的資料
        _ = client.Call("Calculator.Add", args, reply)
        fmt.Printf("Calculator.Add: %d + %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    go func() {
        args := &Args{100, 20}
        reply := new(int)
        _ = client.Call("Calculator.Sub", args, reply)
        fmt.Printf("Calculator.Sub: %d - %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    wg.Wait()
}


$ cd path/server
$ go run ./server.go

$ cd path/client
$ go run ./client.go
Calculator.Sub: 100 - 20 = 80
Calculator.Add: 100 + 20 = 120

三、基於前述http轉rpc,加入token許可權校驗

// server/server.go
package main

import (
    "io"
    "net/http"
    "net/rpc"
)

type Args struct {
    A, B int
}

type Calculator int

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

func (t *Calculator) Sub(args *Args, reply *int) error {
    *reply = args.A - args.B
    return nil
}

func main() {
    addr := ":8090"
    requestURI := "/_custom_http_to_rpc"
    token := "bb"

    rpcServer := rpc.NewServer()
    _ = rpcServer.Register(new(Calculator))
    http.Handle(requestURI, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
        head := request.Header
        if head.Get("token") != token {
            writer.WriteHeader(http.StatusForbidden)
            _, _ = io.WriteString(writer, "403 Forbidden\n")
            return
        }
        rpcServer.ServeHTTP(writer, request)
    }))
    _ = http.ListenAndServe(addr, nil)
}


// client/client.go
package main

import (
    "bufio"
    "errors"
    "fmt"
    "io"
    "net"
    "net/http"
    "net/rpc"
    "sync"
)

type Args struct {
    A, B int
}

func dialHTTPPath(network, address, path, token string) (*rpc.Client, error) {
    connected := "200 Connected to Go RPC"
    conn, err := net.Dial(network, address)
    if err != nil {
        return nil, err
    }
    _, _ = io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\nToken: "+token+"\n\n")

    // Require successful HTTP response
    // before switching to RPC protocol.
    resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
    if err == nil && resp.Status == connected {
        return rpc.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,
    }
}

func main() {
    addr := "127.0.0.1:8090"
    requestURI := "/_custom_http_to_rpc"
    token := "bb"

    client, err := dialHTTPPath("tcp", addr, requestURI, token)
    if err != nil {
        fmt.Println("建立客戶端失敗", err)
        return
    }
    wg := &sync.WaitGroup{}
    wg.Add(2)
    go func() {
        args := &Args{100, 20}
        reply := new(int)
        _ = client.Call("Calculator.Add", args, reply)
        fmt.Printf("Calculator.Add: %d + %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    go func() {
        args := &Args{100, 20}
        reply := new(int)
        _ = client.Call("Calculator.Sub", args, reply)
        fmt.Printf("Calculator.Sub: %d - %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    wg.Wait()
}

$ cd path/server
$ go run ./server.go

$ cd path/client
$ go run ./client.go
Calculator.Sub: 100 - 20 = 80
Calculator.Add: 100 + 20 = 120