一、基本使用方式說明
// 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