引入
服務器開發中會使用RPC(Remote Procedure Call,遠程過程調用)簡化進程間通信的過程。RPC能有效地封裝通信過程。RPC能有效地封裝通信過程,讓遠程的數據收發通信過程看起來就像本地的函數調用一樣。
本例中,使用通道代理Socket 實現RPC的過程。客戶端與服務器運行在同一個進程,服務器和客服端在兩個goroutine中運行。先給出完整代碼,然後在詳細分析每一個部分。
package main
import (
"errors"
"fmt"
"time"
)
//模擬RPC客戶端的請求和接收消息封裝
func RPCClient(ch chan string, req string) (string, error) {
//向服務器發送請求
ch <- req
//等待服務器返回
select {
//等待服務器返回
case ack := <-ch: //接收服務返回數據
return ack, nil
case <-time.After(time.Second): //超時
return "", errors.New("Time Out")
}
}
//模擬RPC服務器端接收客戶端請求和迴應
func RPCServer(ch chan string) {
for {
//接收客戶端請求
data := <-ch
//打印接收到的數據
fmt.Println("server received:", data)
//反饋給客戶端收到
ch <- "roger"
}
}
func main() {
//創建一個無緩衝串通道
ch := make(chan string)
//併發執行服務器邏輯
go RPCServer(ch)
recv, err := RPCClient(ch, "hi")
if err != nil {
//發生錯誤打印
fmt.Println(err)
} else {
//正常接收到數據
fmt.Println("Client received", recv)
}
}
運行輸出:
server received: hi
Client received roger
客戶端請求和接收數據封裝
下面的代碼封裝了向服務器請求數據,等待服務器返回數據,如果請求方超時,該函數還會處理超時邏輯。
//模擬RPC客戶端的請求和接收消息封裝
func RPCClient(ch chan string, req string) (string, error) {
//向服務器發送請求
ch <- req
//等待服務器返回
select {
//等待服務器返回
case ack := <-ch: //接收服務返回數據
return ack, nil
case <-time.After(time.Second): //超時
return "", errors.New("Time Out")
}
}
代碼說明如下:
第 5 行,模擬 socket 向服務器發送一個字符串信息。服務器接收後,結束阻塞執行下一行。
第 8 行,使用 select 開始做多路複用。注意,select 雖然在寫法上和 switch 一樣,都可以擁有 case 和 default。但是 select 關鍵字後面不接任何語句,而是將要複用的多個通道語句寫在每一個 case 上,如第 9 行和第 11 行所示。
第 11 行,使用了 time 包提供的函數 After(),從字面意思看就是多少時間之後,其參數是 time 包的一個常量,time.Second 表示 1 秒。time.After 返回一個通道,這個通道在指定時間後,通過通道返回當前時間。
第 12 行,在超時時,返回超時錯誤。
RPCClient() 函數中,執行到 select 語句時,第 9 行和第 11 行的通道操作會同時開啓。如果第 9 行的通道先返回,則執行第 10 行邏輯,表示正常接收到服務器數據;如果第 11 行的通道先返回,則執行第 12 行的邏輯,表示請求超時,返回錯誤。
服務器接收和反饋數據
服務器接收到客服端的任意數據後,先打印在通過通道返回給客戶端一個固定字符串,表示服務器已經接收到請求。
//模擬RPC服務器端接收客戶端請求和迴應
func RPCServer(ch chan string) {
for {
//接收客戶端請求
data := <-ch
//打印接收到的數據
fmt.Println("server received:", data)
//反饋給客戶端收到
ch <- "roger"
}
}
代碼說明如下:
第 3 行,構造出一個無限循環。服務器處理完客戶端請求後,通過無限循環繼續處理下一個客戶端請求。
第 5 行,通過字符串通道接收一個客戶端的請求。
第 8 行,將接收到的數據打印出來。
第 11 行,給客戶端反饋一個字符串。
運行整個程序,客戶端可以正確收到服務器返回的數據,客戶端RPCClient()函數的代碼下面代碼加粗部分的分支執行。
//等待服務器返回
select {
//等待服務器返回
case ack := <-ch: //接收服務返回數據
return ack, nil
case <-time.After(time.Second): //超時
return "", errors.New("Time Out")
}
運行輸出:
server received: hi
Client received roger
模擬超時
上面的例子雖然有客戶端超時處理,但永遠不會觸發,因爲服務器的處理速度很快,也沒有真正的網絡延時或者“服務器宕機”的情況。因此,爲了展示select中超時的處理,在服務器邏輯中增加一條語句,故意讓服務器延時處理一段時間,造成客戶端請求超時,代碼如下:
//模擬RPC服務器端接收客戶端請求和迴應
func RPCServer(ch chan string) {
for {
//接收客戶端請求
data := <-ch
//打印接收到的數據
fmt.Println("server received:", data)
//通過睡眠函數讓程序執行阻塞2秒的任務
time.Sleep(time.Second * 2)
//反饋給客戶端收到
ch <- "roger"
}
}
第11行中,time.Sleep()函數會讓goroutine執行暫停2秒。使用這種方法模擬服務器會延時,造成客戶端超時。客戶端處理超時1秒時通道就會返回:
//等待服務器返回
select {
//等待服務器返回
case ack := <-ch: //接收服務返回數據
return ack, nil
case <-time.After(time.Second): //超時
return "", errors.New("Time Out")
}
主程序
主程序中會創建一個無緩衝的字符串格式通道。將通道傳給服務器的RPCServer()函數,這個函數併發執行。使用RPCClient()函數通過ch對服務器發出RPC請求,同時接收服務器反饋數據或者等待超時,參考下面代碼:
func main() {
//創建一個無緩衝串通道
ch := make(chan string)
//併發執行服務器邏輯
go RPCServer(ch)
recv, err := RPCClient(ch, "hi")
if err != nil {
//發生錯誤打印
fmt.Println(err)
} else {
//正常接收到數據
fmt.Println("Client received", recv)
}
}
代碼說明如下:
第 4 行,創建無緩衝的字符串通道,這個通道用於模擬網絡和 socke t概念,既可以從通道接收數據,也可以發送。
第 7 行,併發執行服務器邏輯。服務器一般都是獨立進程的,這裏使用併發將服務器和客戶端邏輯同時在一個進程內運行。
第 10 行,使用 RPCClient() 函數,發送“hi”給服務器,同步等待服務器返回。
第 13 行,如果通信過程發生錯誤,打印錯誤。
第 16 行,正常接收時,打印收到的數據。