前面幾個小節我們介紹了如何基于Socket和HTTP來編寫網絡應用,通過學習我們了解了Socket和HTTP采用的是類似"信息交換"模式,即客戶端發送一條信息到服務端,然后(一般來說)服務器端都會返回一定的信息以表示響應。客戶端和服務端之間約定了交互信息的格式,以便雙方都能夠解析交互所產生的信息。但是很多獨立的應用并沒有采用這種模式,而是采用類似常規的函數調用的方式來完成想要的功能。
RPC就是想實現函數調用模式的網絡化。客戶端就像調用本地函數一樣,然后客戶端把這些參數打包之后通過網絡傳遞到服務端,服務端解包到處理過程中執行,然后執行的結果反饋給客戶端。
RPC(Remote Procedure Call Protocol)——遠程過程調用協議,是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議。它假定某些傳輸協議的存在,如TCP或UDP,以便為通信程序之間攜帶信息數據。通過它可以使函數調用模式網絡化。在OSI網絡通信模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網絡分布式多程序在內的應用程序更加容易。
## [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.4.md#rpc工作原理)RPC工作原理
[](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/images/8.4.rpc.png?raw=true)
圖8.8 RPC工作流程圖
運行時,一次客戶機對服務器的RPC調用,其內部操作大致有如下十步:
* 1.調用客戶端句柄;執行傳送參數
* 2.調用本地系統內核發送網絡消息
* 3.消息傳送到遠程主機
* 4.服務器句柄得到消息并取得參數
* 5.執行遠程過程
* 6.執行的過程將結果返回服務器句柄
* 7.服務器句柄返回結果,調用遠程系統內核
* 8.消息傳回本地主機
* 9.客戶句柄由內核接收消息
* 10.客戶接收句柄返回的數據
## [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.4.md#go-rpc)Go RPC
Go標準包中已經提供了對RPC的支持,而且支持三個級別的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是獨一無二的RPC,它和傳統的RPC系統不同,它只支持Go開發的服務器與客戶端之間的交互,因為在內部,它們采用了Gob來編碼。
Go RPC的函數只有符合下面的條件才能被遠程訪問,不然會被忽略,詳細的要求如下:
* 函數必須是導出的(首字母大寫)
* 必須有兩個導出類型的參數,
* 第一個參數是接收的參數,第二個參數是返回給客戶端的參數,第二個參數必須是指針類型的
* 函數還要有一個返回值error
舉個例子,正確的RPC函數格式如下:
~~~
func (t *T) MethodName(argType T1, replyType *T2) error
~~~
T、T1和T2類型必須能被`encoding/gob`包編解碼。
任何的RPC都需要通過網絡來傳遞數據,Go RPC可以利用HTTP和TCP來傳遞數據,利用HTTP的好處是可以直接復用`net/http`里面的一些函數。詳細的例子請看下面的實現
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.4.md#http-rpc)HTTP RPC
http的服務端代碼實現如下:
~~~
package main
import (
"errors"
"fmt"
"net/http"
"net/rpc"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
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
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
err := http.ListenAndServe(":1234", nil)
if err != nil {
fmt.Println(err.Error())
}
}
~~~
通過上面的例子可以看到,我們注冊了一個Arith的RPC服務,然后通過`rpc.HandleHTTP`函數把該服務注冊到了HTTP協議上,然后我們就可以利用http的方式來傳遞數據了。
請看下面的客戶端代碼:
~~~
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server")
os.Exit(1)
}
serverAddress := os.Args[1]
client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
~~~
我們把上面的服務端和客戶端的代碼分別編譯,然后先把服務端開啟,然后開啟客戶端,輸入代碼,就會輸出如下信息:
~~~
$ ./http_c localhost
Arith: 17*8=136
Arith: 17/8=2 remainder 1
~~~
通過上面的調用可以看到參數和返回值是我們定義的struct類型,在服務端我們把它們當做調用函數的參數的類型,在客戶端作為`client.Call`的第2,3兩個參數的類型。客戶端最重要的就是這個Call函數,它有3個參數,第1個要調用的函數的名字,第2個是要傳遞的參數,第3個要返回的參數(注意是指針類型),通過上面的代碼例子我們可以發現,使用Go的RPC實現相當的簡單,方便。
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.4.md#tcp-rpc)TCP RPC
上面我們實現了基于HTTP協議的RPC,接下來我們要實現基于TCP協議的RPC,服務端的實現代碼如下所示:
~~~
package main
import (
"errors"
"fmt"
"net"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
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
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
rpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
~~~
上面這個代碼和http的服務器相比,不同在于:在此處我們采用了TCP協議,然后需要自己控制連接,當有客戶端連接上來后,我們需要把這個連接交給rpc來處理。
如果你留心了,你會發現這它是一個阻塞型的單用戶的程序,如果想要實現多并發,那么可以使用goroutine來實現,我們前面在socket小節的時候已經介紹過如何處理goroutine。 下面展現了TCP實現的RPC客戶端:
~~~
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
os.Exit(1)
}
service := os.Args[1]
client, err := rpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
~~~
這個客戶端代碼和http的客戶端代碼對比,唯一的區別一個是DialHTTP,一個是Dial(tcp),其他處理一模一樣。
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.4.md#json-rpc)JSON RPC
JSON RPC是數據編碼采用了JSON,而不是gob編碼,其他和上面介紹的RPC概念一模一樣,下面我們來演示一下,如何使用Go提供的json-rpc標準包,請看服務端代碼的實現:
~~~
package main
import (
"errors"
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
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
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
jsonrpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
~~~
通過示例我們可以看出 json-rpc是基于TCP協議實現的,目前它還不支持HTTP方式。
請看客戶端的實現代碼:
~~~
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
log.Fatal(1)
}
service := os.Args[1]
client, err := jsonrpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
~~~
## [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.4.md#總結)總結
Go已經提供了對RPC的良好支持,通過上面HTTP、TCP、JSON RPC的實現,我們就可以很方便的開發很多分布式的Web應用,我想作為讀者的你已經領會到這一點。但遺憾的是目前Go尚未提供對SOAP RPC的支持,欣慰的是現在已經有第三方的開源實現了。
- 第一章 Go環境配置
- 1.1 Go安裝
- 1.2 GOPATH 與工作空間
- 1.3 Go 命令
- 1.4 Go開發工具
- 1.5 小結
- 第二章 Go語言基礎
- 2.1 你好,Go
- 2.2 Go基礎
- 2.3 流程和函數
- 2.4 struct類型
- 2.5 面向對象
- 2.6 interface
- 2.7 并發
- 2.8 總結
- 第三章 Web基礎
- 3.1 Web工作方式
- 3.2 Go搭建一個Web服務器
- 3.3 Go如何使得Web工作
- 3.4 Go的http包詳解
- 3.5 小結
- 第四章 表單
- 4.1 處理表單的輸入
- 4.2 驗證表單的輸入
- 4.3 預防跨站腳本
- 4.4 防止多次遞交表單
- 4.5 處理文件上傳
- 4.6 小結
- 第五章 訪問數據庫
- 5.1 database/sql接口
- 5.2 使用MySQL數據庫
- 5.3 使用SQLite數據庫
- 5.4 使用PostgreSQL數據庫
- 5.5 使用beedb庫進行ORM開發
- 5.6 NOSQL數據庫操作
- 5.7 小結
- 第六章 session和數據存儲
- 6.1 session和cookie
- 6.2 Go如何使用session
- 6.3 session存儲
- 6.4 預防session劫持
- 6.5 小結
- 第七章 文本處理
- 7.1 XML處理
- 7.2 JSON處理
- 7.3 正則處理
- 7.4 模板處理
- 7.5 文件操作
- 7.6 字符串處理
- 7.7 小結
- 第八章 Web服務
- 8.1 Socket編程
- 8.2 WebSocket
- 8.3 REST
- 8.4 RPC
- 8.5 小結
- 第九章 安全與加密
- 9.1 預防CSRF攻擊
- 9.2 確保輸入過濾
- 9.3 避免XSS攻擊
- 9.4 避免SQL注入
- 9.5 存儲密碼
- 9.6 加密和解密數據
- 9.7 小結
- 第十章 國際化和本地化
- 10.1 設置默認地區
- 10.2 本地化資源
- 10.3 國際化站點
- 10.4 小結
- 第十一章 錯誤處理,調試和測試
- 11.1 錯誤處理
- 11.2 使用GDB調試
- 11.3 Go怎么寫測試用例
- 11.4 小結
- 第十二章 部署與維護
- 12.1 應用日志
- 12.2 網站錯誤處理
- 12.3 應用部署
- 12.4 備份和恢復
- 12.5 小結
- 第十三章 如何設計一個Web框架
- 13.1 項目規劃
- 13.2 自定義路由器設計
- 13.3 controller設計
- 13.4 日志和配置設計
- 13.5 實現博客的增刪改
- 13.6 小結
- 第十四章 擴展Web框架
- 14.1 靜態文件支持
- 14.2 Session支持
- 14.3 表單及驗證支持
- 14.4 用戶認證
- 14.5 多語言支持
- 14.6 pprof支持
- 14.7 小結
- 附錄A 參考資料