Go語言實現RPC

豌豆射手_BiuBiu發表於2018-08-17

RPC定義,來源於百度百科

  • RPC(Remote Procedure Call)—遠端過程呼叫,它是一種通過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,為通訊程式之間攜帶資訊資料。在OSI網路通訊模型中,RPC跨越了傳輸層應用層。RPC使得開發包括網路分散式多程式在內的應用程式更加容易。

  • RPC採用客戶機/伺服器模式。請求程式就是一個客戶機,而服務提供程式就是一個伺服器。首先,客戶機呼叫程式傳送一個有程式引數的呼叫資訊到服務程式,然後等待應答資訊。在伺服器端,程式保持睡眠狀態直到呼叫資訊到達為止。當一個呼叫資訊到達,伺服器獲得程式引數,計算結果,傳送答覆資訊,然後等待下一個呼叫資訊,最後,客戶端呼叫程式接收答覆資訊,獲得程式結果,然後呼叫執行繼續進行。

  • 有多種 RPC模式和執行。最初由 Sun 公司提出。IETF ONC 憲章重新修訂了 Sun 版本,使得 ONC RPC 協議成為 IETF 標準協議。現在使用最普遍的模式和執行是開放式軟體基礎的分散式計算環境(DCE)。

  • 個人的理解:不用管什麼底層網路技術的協議,是一種通過網路從計算機程式上請求服務,通俗一點,我們寫程式碼,要在一個地方,比如安卓,就需要在一個工程裡面,才可以呼叫到其他的程式程式碼執行的過程。Go語言提供RPC支援使得開發網路分散式多程式在內的應用程式更加的easy

RPC工作流程圖

圖片來源於gitHub

  • 1.呼叫客戶端控制程式碼;執行傳送引數
  • 2.呼叫本地系統核心傳送網路訊息
  • 3.訊息傳送到遠端主機
  • 4.伺服器控制程式碼得到訊息並取得引數
  • 5.執行遠端過程
  • 6.執行的過程將結果返回伺服器控制程式碼
  • 7.伺服器控制程式碼返回結果,呼叫遠端系統核心
  • 8.訊息傳回本地主機
  • 9.客戶控制程式碼由核心接收訊息
  • 10.客戶接收控制程式碼返回的資料

Go語言提供對RPC的支援:HTTP、TCP、JSPNRPC,但是在GoRPC是獨一無二的,它採用了GoLang Gob編碼,只能支援Go語言!

  • GoLang Gob:是Golang包自帶的一個資料結構序列化的編碼/解碼工具。編碼使用Encoder,解碼使用Decoder。一種典型的應用場景就是RPC(remote procedure calls)。

HTTP RPC Demo

  • 服務端的程式碼
package main

import (
	"fmt"
	"net/rpc"
	"net/http"
	"errors"
)
func main() {
     rpcDemo()
}
type Arith int
func rpcDemo() {
	arith:=new(Arith)
	//arith=== 0xc04204e090
	fmt.Println("arith===",arith)

	rpc.Register(arith)
	//HandleHTTP將RPC訊息的HTTP處理程式註冊到Debug伺服器
	//DEFAUTUPCPATH和Debug除錯路徑上的除錯處理程式。
	//仍然需要呼叫http.Services(),通常是在GO語句中。
    rpc.HandleHTTP()
	err:=http.ListenAndServe(":1234",nil)
	if err != nil {
		fmt.Println("err=====",err.Error())
	}
}
type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

//函式必須是匯出的(首字母大寫)
//必須有兩個匯出型別的引數,
//第一個引數是接收的引數,第二個引數是返回給客戶端的引數,第二個引數必須是指標型別的
//函式還要有一個返回值error
func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	fmt.Println("這個方法執行了啊---嘿嘿--- Multiply ",reply)
	return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
	if args.B == 0 {
		return errors.New("divide by zero")
	}
	quo.Quo = args.A / args.B
	quo.Rem = args.A % args.B
	fmt.Println("這個方法執行了啊---嘿嘿--- Divide quo==",quo)
	return nil
}

複製程式碼
  • Go RPC 的函式只有符合四個條件才能夠被遠端訪問,不然會被忽略
    • 函式必須是首字母大寫(可以匯出的)
    • 必須有兩個匯出型別的引數
    • 第一個引數是接受的引數,第二個引數是返回給客戶端的引數,而且第二個引數是指標的型別
    • 函式還要有一個返回值error
func (t *T) MethodName(argType T1, replyType *T2) error
複製程式碼
  • T、T1和T2型別必須能被encoding/gob包編解碼。

  • 客戶端的程式碼


package main

import (
	"log"
	"fmt"
	"os"
	"net/rpc"
	"strconv"
)

type ArgsTwo struct {
	A, B int
}

type QuotientTwo struct {
	Quo, Rem int
}

func main() {
	// 如果什麼都不輸入的話 ,就是以下的這個值
	//os***************** [C:\Users\win7\AppData\Local\Temp\go-build669605574\command-
	//line-arguments\_obj\exe\GoRPCWeb.exe 127.0.0.1] **********************

	fmt.Println("os*****************",os.Args,"**********************")
	if len(os.Args) != 4 { //   todo  第二個地址是  我們本地的地址
		fmt.Println("老子要退出了哦 傻逼 一號start--------》》》", os.Args[0], "《《《---------------server  end")
		os.Exit(1)
	}else{
		fmt.Println("長度是多少 "+strconv.Itoa( len(os.Args))+"才是準確的長度 哦---》")
	}
    //獲取輸入的地址是獲取輸入得 os 資料的 第一個位置的值
	serverAddress := os.Args[1]
    fmt.Println("severAddress==",serverAddress)
	// //DelayHTTP在指定的網路地址連線到HTTP RPC伺服器
	///在預設HTTP RPC路徑上監聽。
	client, err := rpc.DialHTTP("tcp", serverAddress)
	if err != nil {
		log.Fatal("發生錯誤了 在這裡地方  DialHTTP", err)
	}
	i1,_:=strconv.Atoi( os.Args[2])
	i2,_:=strconv.Atoi( os.Args[3])
	args := ArgsTwo{i1, i2}
	var reply int
	//呼叫呼叫命名函式,等待它完成,並返回其錯誤狀態。
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("Call Multiply  發生錯誤了哦   arith error:", err)
	}
	fmt.Printf("Arith 乘法: %d*%d=%d\n", args.A, args.B, reply)

	var quot QuotientTwo
	//呼叫呼叫命名函式,等待它完成,並返回其錯誤狀態。
	err = client.Call("Arith.Divide", args, &quot)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith 除法取整數: %d/%d=%d 餘數 %d\n", args.A, args.B, quot.Quo, quot.Rem)

}
複製程式碼

執行的結果圖

E:\new_code\GoDemo\web_server>go run GoRPCWeb8.go 127.0.0.1:1234  20 3
os***************** [C:\Users\win7\AppData\Local\Temp\go-build011170718\command-
line-arguments\_obj\exe\GoRPCWeb8.exe 127.0.0.1:1234 20 3] *********************
*
長度是多少 4才是準確的長度 哦---》
severAddress== 127.0.0.1:1234
Arith 乘法: 20*3=60
Arith 除法取整數: 20/3=6 餘數 2
複製程式碼
  • go run GoRPCWeb8.go 127.0.0.1:1234 20 3
    • go run 執行的命令
    • GoRPCWeb8.go :這是檔案的名稱,需要到指定的目錄下啟動cmd
    • 127.0.0.1:1234 : ip地址和埠號
    • 20 3 這是客服端傳入的值:一個除數,一個被除數,傳入到伺服器做乘法運算 乘法: 20*3=60和除法取整數: 20/3=6 餘數 2,怎麼做的,客戶端不關心,給到服務端去完成
  • os.Args[0]=[C:\Users\win7\AppData\Local\Temp\go-build011170718\command- line-arguments\_obj\exe\GoRPCWeb8.exe 127.0.0.1:1234 20 3]
	if len(os.Args) != 4 { //   todo  第二個地址是  我們本地的地址
		fmt.Println("老子要退出了哦 傻逼 一號start--------》》》", os.Args[0], "《《《---------------server  end")
		os.Exit(1)
	}else{
		fmt.Println("長度是多少 "+strconv.Itoa( len(os.Args))+"才是準確的長度 哦---》")
	}
複製程式碼

TCP RPC Demo

  • 基於TCP協議實現的RPC,服務端的程式碼
package main

import (
	"fmt"
	"net/rpc"
	"net"
	"os"
	"errors"
)

func init() {
	fmt.Println("基於TCP協議實現的RPC,服務端的程式碼如下")
}
type Me struct {
	A,B int
}
type You struct {
	CC,D int
}
type Num int

/*
Go RPC的函式只有符合下面的條件才能夠被遠端訪問,不然會被忽略
1 函式必須是匯出的(首字母大寫)
2 必須有兩個匯出型別的引數
3 第一個引數是接受的引數,第二個引數是返回給客戶端的引數,第二個引數必須是指正型別的
4 函式還必須有一個返回值error
 */
func (n *Num) M(args *Me,reply *int) error  {
	*reply=args.A * args.B
	return nil
}


func (n *Num) F(args * Me,u *You ) error  {
	if  args.B==0{
		return errors.New("輸入不能夠為0 被除數")
	}
	u.D=args.A/args.B
	u.CC=args.A % args.B
	return nil
}



func main() {
	//內建函式new本質上說跟其它語言中的同名函式功能一樣:new(T)分配了零值填充的T型別的記憶體空間,並且返回其地址,即一個*T型別的值。用Go的術語說,它返回了一個指標,指向新分配的型別T的零值。有一點非常重要:
	//new返回指標。
    num:=new(Num)
    rpc.Register(num)
    //ResolveTCPAddr返回TCP端點的地址。
	//網路必須是TCP網路名稱。
    tcpAddr,err:=net.ResolveTCPAddr("tcp",":1234")

	if err != nil {
		fmt.Println("錯誤了哦")
		os.Exit(1)
	}
    listener,err:=net.ListenTCP("tcp",tcpAddr)
	for  {
		// todo   需要自己控制連線,當有客戶端連線上來後,我們需要把這個連線交給rpc 來處理
		conn,err:=listener.Accept()
		if err != nil {
			continue
		}
		rpc.ServeConn(conn)
	}
}

複製程式碼
  • 基於TCP客戶端程式碼
package main

import (
	"fmt"
	"os"
	"net/rpc"
	"log"
	"strconv"
)

func main() {
	fmt.Println("客戶端 其他端 去呼叫的地方  對應的例子是 GoTCPRPC9.go")

	if len(os.Args)==4{
		fmt.Println("長度必須等於4,因為呢,你輸入的肯定是一個ip的地址ip=",os.Args[1],"嘿嘿,加上後面的被除數os.Args[2]=",os.Args[2],"和除數os.Args[3]=",os.Args[3])
		//os.Exit(1)
	}
    // 獲取 ip 地址
    service:= os.Args[1]
    //連線 撥號連線到指定的網路地址的RPC伺服器。
    client,err:=rpc.Dial("tcp",service)
	if err!=nil {
		log.Fatal("老子在連線Dial的發生了錯誤,我要退出了",err)
	}
	num1:=os.Args[2]
	i1,error1:=strconv.Atoi(num1)
	if error1!=nil {
		fmt.Println("自己不知道 自己輸入錯誤了啊 請看error :",error1)
		os.Exit(1)
	}
	num2:=os.Args[3]
	i2,error2:=strconv.Atoi(num2)
	if error2!=nil {
		fmt.Println("自己不知道 自己輸入錯誤了啊 請看error :",error2)
		os.Exit(1)
	}
	aa:=AAA{i1,i2}
	var reply  int
	err1:=client.Call("Num.M",aa,&reply)

	if err1 != nil{
		log.Fatal("我要退出了,因為我在Call的時候發生了 錯誤",err1)
	}
	fmt.Println("我進行正常結果如下")
	fmt.Printf("Num : %d*%d=%d\n",aa.A,aa.B,reply)

	var bb BDemo
	//呼叫呼叫命名函式,等待它完成,並返回其錯誤狀態。
	err= client.Call("Num.F",aa,&bb)
	if err!=nil {
		log.Fatal("我對這個方法發生了過敏的反應 哈哈哈哈  err=====",err)
	}
	fmt.Printf("Num: %d/%d=%d 餘數 %d\n",aa.A,aa.B,bb.DD,bb.CC)
	
}


// 定義兩個類,那邊需要操作的類
type AAA struct {
	A,B int
}
//記住這裡不能夠大寫 兩個連著一起大寫 有點意思
//reading body gob: type mismatch: no fields matched compiling decoder for  DDDD
//  todo 為啥 第二個引數  只要是兩個連在一起的DDDD   就會報錯   reading body gob: type mismatch: no fields matched compiling decoder for
type BDemo struct {
	DD, CC int
}

複製程式碼
  • 執行結果圖
    結果圖
E:\new_code\GoDemo\web_server>go run GoTCPRPCWeb10.go 127.0.0.1:1234  20 1
客戶端 其他端 去呼叫的地方  對應的例子是 GoTCPRPC9.go
長度必須等於4,因為呢,你輸入的肯定是一個ip的地址ip= 127.0.0.1:1234 嘿嘿,加上後面
的被除數os.Args[2]= 20 和除數os.Args[3]= 1
我進行正常結果如下
Num : 20*1=20
Num: 20/1=0 餘數 0

E:\new_code\GoDemo\web_server>go run GoTCPRPCWeb10.go 127.0.0.1:1234  20 2
客戶端 其他端 去呼叫的地方  對應的例子是 GoTCPRPC9.go
長度必須等於4,因為呢,你輸入的肯定是一個ip的地址ip= 127.0.0.1:1234 嘿嘿,加上後面
的被除數os.Args[2]= 20 和除數os.Args[3]= 2
我進行正常結果如下
Num : 20*2=40
Num: 20/2=0 餘數 0

E:\new_code\GoDemo\web_server>go run GoTCPRPCWeb10.go 127.0.0.1:1234  20 3
客戶端 其他端 去呼叫的地方  對應的例子是 GoTCPRPC9.go
長度必須等於4,因為呢,你輸入的肯定是一個ip的地址ip= 127.0.0.1:1234 嘿嘿,加上後面
的被除數os.Args[2]= 20 和除數os.Args[3]= 3
我進行正常結果如下
Num : 20*3=60
Num: 20/3=0 餘數 2
複製程式碼
  • 在定義BDemo 的時候, 如果定義的DD, CC int和服務端不一樣,就會報錯 reading body gob: type mismatch: no fields matched compiling decoder for ,其實發現好多種情況,也會出現這種錯誤,但是目前不知道為啥會這樣,後續,等原始碼深入一點,回來看這個問題 todo2018/07/19
  • 這種TCP方式和上面的HTTP不同的是
    • HTTP:指定的網路地址連線到HTTP RPC伺服器
         //DelayHTTP在指定的網路地址連線到HTTP RPC伺服器
	///在預設HTTP RPC路徑上監聽。
	client, err := rpc.DialHTTP("tcp", serverAddress)
複製程式碼
  • TCP:指定的網路地址連線到HTTP RPC伺服器
    client,err:=rpc.Dial("tcp",service)
複製程式碼

JSON RPC

  • JSON RPC是資料編碼採用了JSON,而不是gob編碼,其他和上面介紹的RPC概念一模一樣的。

  • 服務端的程式碼如下

package main

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

//使用Go提供的json-rpc 標準包
func init() {
	fmt.Println("JSON RPC 採用了JSON,而不是 gob編碼,和RPC概念一模一樣,")
}
type Work struct {
	Who,DoWhat string
}

type DemoM string

func (m *DemoM) DoWork(w *Work,whoT *string) error  {
    *whoT="是誰:"+w.Who+",在做什麼---"+w.DoWhat
	return nil
}

func main() {
    str:=new(DemoM)
    rpc.Register(str)

    tcpAddr,err:=net.ResolveTCPAddr("tcp",":8080")
	if  err!=nil{
		fmt.Println("大哥發生錯誤了啊,請看錯誤 ResolveTCPAddr err=",err)
	}

    listener,err:=net.ListenTCP("tcp",tcpAddr)
	if err!=nil {
		fmt.Println("發生錯誤了--》err=",err)
	}

	for  {
		 conn,err:= listener.Accept()
		if err!=nil {
			continue
		}
		jsonrpc.ServeConn(conn)

	}

}

複製程式碼
  • 客戶端的程式碼
package main

import (
	"fmt"
	"os"
	"net/rpc/jsonrpc"
	"log"
)

func main() {
	fmt.Println("這是客戶端,用來啟動,通過命令列來啟動")

	fmt.Println("客戶端 其他端 去呼叫的地方  對應的例子是 GoTCPRPC9.go")

	if len(os.Args)==4{
		fmt.Println("長度必須等於4,因為呢,你輸入的肯定是一個ip的地址ip=",os.Args[1],"嘿嘿,加上後面的被除數os.Args[2]=",os.Args[2],"和除數os.Args[3]=",os.Args[3])
		//os.Exit(1)
	}

	 service:=os.Args[1]
	 client,err:=jsonrpc.Dial("tcp",service)
	if err != nil {
		log.Fatal("Dial 發生了錯誤了哦 錯誤的資訊為   err=",err)
	}
	send:=Send{os.Args[2],os.Args[3]}
	var  resive  string
	err1:=client.Call("DemoM.DoWork",send,&resive)
	if err1!=nil {
		fmt.Println("shiming call error    ")
		fmt.Println("Call 的時候發生了錯誤了哦  err=",err1)
	}
	fmt.Println("收到資訊了",resive)




}
// 類可以不一樣 但是 Who 和DoWhat 要必須一樣  要不然接收到不到值,等我在詳細的瞭解了 才去分析下原因  感覺有點矇蔽啊
type Send struct {
	Who, DoWhat string
}





複製程式碼
  • 執行的結果如下
    執行結果
E:\new_code\GoDemo\web_server>go run GoJSONRPCWeb11.go 127.0.0.1:8080  shiming g
ongzuo
這是客戶端,用來啟動,通過命令列來啟動
客戶端 其他端 去呼叫的地方  對應的例子是 GoTCPRPC9.go
長度必須等於4,因為呢,你輸入的肯定是一個ip的地址ip= 127.0.0.1:8080 嘿嘿,加上後面
的被除數os.Args[2]= shiming 和除數os.Args[3]= gongzuo
收到資訊了 是誰:shiming,在做什麼---gongzuo

E:\new_code\GoDemo\web_server>go run GoJSONRPCWeb11.go 127.0.0.1:8080  shiming q
iaodaima
這是客戶端,用來啟動,通過命令列來啟動
客戶端 其他端 去呼叫的地方  對應的例子是 GoTCPRPC9.go
長度必須等於4,因為呢,你輸入的肯定是一個ip的地址ip= 127.0.0.1:8080 嘿嘿,加上後面
的被除數os.Args[2]= shiming 和除數os.Args[3]= qiaodaima
收到資訊了 是誰:shiming,在做什麼---qiaodaima
複製程式碼
  • os.Args是一個陣列 var Args []string,通過輸入獲取到,然後把這個客戶端輸入的內容傳送到服務端,服務端做些操作,然後在返回給客戶端
  • Go已經提供了RPC良好的支援,通過HTTP TCP JSONRPC的實現,可以很方便開發分散式的Web應用,但是我目前還不會,在學習中。遺憾的是Go沒有提供SOAP RPC的支援~~~

相關文章