搭建一個(gè)微信小程序要多少錢快速整站優(yōu)化
Go微服務(wù)與云原生
1、微服務(wù)架構(gòu)介紹
單體架構(gòu)(電商)
SOA架構(gòu)(電商)
微服務(wù)架構(gòu)(電商)
優(yōu)勢(shì)
挑戰(zhàn)
拆分
發(fā)展史
第一代:基于RPC的傳統(tǒng)服務(wù)架構(gòu)
第二代:Service Mesh(istio)
微服務(wù)架構(gòu)分層
核心組件
Summary
2、RPC
2、什么是RPC
RPC (Remote Procedure Call),即遠(yuǎn)程過(guò)程調(diào)用。它允許像調(diào)用本地服務(wù)一樣調(diào)用遠(yuǎn)程服務(wù)。
RPC是一種服務(wù)器-客戶端(Client/Server)模式,經(jīng)典實(shí)現(xiàn)是一個(gè)通過(guò)發(fā)送請(qǐng)求-接受回應(yīng)進(jìn)行信息交互的系統(tǒng)。
首先與RPC(遠(yuǎn)程過(guò)程調(diào)用)相對(duì)應(yīng)的是本地調(diào)用
本地調(diào)用
RPC調(diào)用
本地過(guò)程調(diào)用發(fā)生在同一進(jìn)程中——定義add函數(shù)的代碼和調(diào)用add函數(shù)的代碼共享同一個(gè)內(nèi)存空間,所以調(diào)用能夠正常執(zhí)行。
但是我們無(wú)法直接在另一個(gè)程序——app2中調(diào)用add函數(shù),因?yàn)樗鼈兪莾蓚€(gè)程序——內(nèi)存空間是相互隔離的。(app1和app2可能部署在同一臺(tái)服務(wù)器上也可能部署在互聯(lián)網(wǎng)的不同服務(wù)器上。)
RPC就是為了解決類似遠(yuǎn)程、跨內(nèi)存空間、的函數(shù)/方法調(diào)用的。要實(shí)現(xiàn)RPC就需要解決以下三個(gè)問(wèn)題。
- 1、如何確定要執(zhí)行的函數(shù)?在本地調(diào)用中,函數(shù)主體通過(guò)函數(shù)指針函數(shù)指定,然后調(diào)用add函數(shù),編譯器通過(guò)函數(shù)指針函數(shù)自動(dòng)確定add函數(shù)在內(nèi)存中的位置。但是在RPC中,調(diào)用不能通過(guò)函數(shù)指針完成,因?yàn)樗麄兊膬?nèi)存地址可能完全不同。因此,調(diào)用方和被調(diào)用方都需要維護(hù) 一個(gè){function <->ID}映射表,以確保調(diào)用正確的函數(shù)。
- 2、如何表達(dá)參數(shù)?本地過(guò)程調(diào)用中傳遞的參數(shù)是通過(guò)堆棧內(nèi)存結(jié)構(gòu)實(shí)現(xiàn)的,但是RPC不能直接使用內(nèi)存?zhèn)鬟f參數(shù),因此參數(shù)或返回值需要在傳輸期間序列化并轉(zhuǎn)換成字節(jié)流,反之亦然。
- 3、如何進(jìn)行網(wǎng)絡(luò)傳輸? 函數(shù)的調(diào)用方河北調(diào)用方式通過(guò)網(wǎng)絡(luò)連接的,也就是說(shuō),function ID和序列化字節(jié)流需要通過(guò)網(wǎng)絡(luò)傳輸,因此,只要能夠完成傳輸,調(diào)用方河北調(diào)用方就不熟某個(gè)網(wǎng)絡(luò)協(xié)議的限制。例如,一些RPC框架使用TCP協(xié)議,一些使用HTTP。
以往實(shí)現(xiàn)跨服務(wù)調(diào)用的時(shí)候,我們會(huì)采用RESTful API的方式,被調(diào)用方會(huì)對(duì)外提供一個(gè)HTTP接口,調(diào)用方按要求發(fā)起HTTP請(qǐng)求并接受APT交界口返回的相應(yīng)數(shù)據(jù)。下面的示例是將add函數(shù)包裝秤一個(gè)RESTful API。
HTTP調(diào)用RESTful API
首先,我們編寫一個(gè)基于HTTP的server服務(wù),它將接收其他程序發(fā)來(lái)的HTTP請(qǐng)求,執(zhí)行特定的程序并將結(jié)果返回。
package mainimport("encoding/json""io/ioutil""log"“net/http”
)type addParam struct{X int `json:"x"`Y int `json:"y"`
}type addResult struct{Code int `json:"code"`Data int `json:"data"`
}func add(x. y int) int{return x + y
}func addHandler(w http.ResponseWriter, r *http.Request){//解析參數(shù)b , _ := ioutil.ReadAll(r.Body)var param addParamjson.Unmarshal(b , ¶m)//業(yè)務(wù)邏輯ret := add(param.X,param.Y)//返回響應(yīng)respBytes, _ := json.Marshal(addResult{Code:0, Data: ret})w.Write(respBytes)
}func main(){http.HandleFunc("/add", addHandler)log.Fatal(http.ListenAndServe(":9090",nil))}
我們編寫一個(gè)客戶端來(lái)請(qǐng)求上述HTTP服務(wù),傳遞x和y兩個(gè)整數(shù),等待返回結(jié)果。
package mainimport ("bytes""encoding/json""fmt""io/ioutil""net/http"
)type addParam struct{X int `json:"x"`Y int `json:"y"`
}type addResult struct {Code int `json:"code"`Data int `json:"data"`
}func main(){//通過(guò)HTTP請(qǐng)求調(diào)用其他服務(wù)器上的add服務(wù)url := "http://127.0.0.1:9090/add"param := &addParam{X:20,Y:20,}paramBytes, _ := json.Marshal(param)resp, _ := http.Post(url,"application/json", bytes.NewReader(paramBytes))defer resp.Body.Close()respBytes, _ := ioutil.ReadAll(resp.Body)var respData addResultjson.Unmarshal(respBytes, &respData)fmt.Println(respData.data) //30
}
這種模式是我們目前比較常見(jiàn)的跨服務(wù)或跨語(yǔ)言之間基于RESTful API的服務(wù)調(diào)用模式。 既然使用API調(diào)用也能實(shí)現(xiàn)類似遠(yuǎn)程調(diào)用的目的,為什么還要用RPC呢?
使用 RPC 的目的是讓我們調(diào)用遠(yuǎn)程方法像調(diào)用本地方法一樣無(wú)差別。并且基于RESTful API通常是基于HTTP協(xié)議,傳輸數(shù)據(jù)采用JSON等文本協(xié)議,相較于RPC 直接使用TCP協(xié)議,傳輸數(shù)據(jù)多采用二進(jìn)制協(xié)議來(lái)說(shuō),RPC通常相比RESTful API性能會(huì)更好。
RESTful API多用于前后端之間的數(shù)據(jù)傳輸,而目前微服務(wù)架構(gòu)下各個(gè)微服務(wù)之間多采用RPC調(diào)用。
net/rpc
基礎(chǔ)RPC示例
Go語(yǔ)言的 rpc 包提供對(duì)通過(guò)網(wǎng)絡(luò)或其他 i/o 連接導(dǎo)出的對(duì)象方法的訪問(wèn),服務(wù)器注冊(cè)一個(gè)對(duì)象,并把它作為服務(wù)對(duì)外可見(jiàn)(服務(wù)名稱就是類型名稱)。注冊(cè)后,對(duì)象的導(dǎo)出方法將支持遠(yuǎn)程訪問(wèn)。服務(wù)器可以注冊(cè)不同類型的多個(gè)對(duì)象(服務(wù)) ,但是不支持注冊(cè)同一類型的多個(gè)對(duì)象。
基于TCP協(xié)議的RPC
使用JSON協(xié)議的RPC
Python調(diào)用RPC
RPC原理
① 服務(wù)調(diào)用方(client)以本地調(diào)用方式調(diào)用服務(wù);
② client stub接收到調(diào)用后負(fù)責(zé)將方法、參數(shù)等組裝成能夠進(jìn)行網(wǎng)絡(luò)傳輸?shù)南Ⅲw;
③ client stub找到服務(wù)地址,并將消息發(fā)送到服務(wù)端;
④ server 端接收到消息;
⑤ server stub收到消息后進(jìn)行解碼;
⑥ server stub根據(jù)解碼結(jié)果調(diào)用本地的服務(wù);
⑦ 本地服務(wù)執(zhí)行并將結(jié)果返回給server stub;
⑧ server stub將返回結(jié)果打包成能夠進(jìn)行網(wǎng)絡(luò)傳輸?shù)南Ⅲw;
⑨ 按地址將消息發(fā)送至調(diào)用方;
⑩ client 端接收到消息;
? client stub收到消息并進(jìn)行解碼;
? 調(diào)用方得到最終結(jié)果。
使用RPC框架的目標(biāo)是只需要關(guān)心第1步和最后1步,中間的其他步驟統(tǒng)統(tǒng)封裝起來(lái),讓使用者無(wú)需關(guān)心。例如社區(qū)中各式RPC框架(grpc、thrift等)就是為了讓RPC調(diào)用更方便。
4、protocol Buffers V3中文語(yǔ)法指南
指南原版
定義一個(gè)消息類型
首先讓我們看一個(gè)非常簡(jiǎn)單的例子。假設(shè)你想要定義一個(gè)搜索請(qǐng)求消息格式,其中每個(gè)搜索請(qǐng)求都包含一個(gè)查詢?cè)~字符串、你感興趣的查詢結(jié)果所在的特定頁(yè)碼數(shù)和每一頁(yè)應(yīng)展示的結(jié)果數(shù)。
下面是用于定義這個(gè)消息類型的 .proto 文件。
syntax = "proto3";message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;
}
- 文件的第一行指定使用 proto3 語(yǔ)法: 如果不這樣寫,protocol buffer編譯器將假定你使用 proto2。這個(gè)聲明必須是文件的第一個(gè)非空非注釋行。
- SearchRequest 消息定義指定了三個(gè)字段(名稱/值對(duì)) ,每個(gè)字段表示希望包含在此類消息中的每一段數(shù)據(jù)。每個(gè)字段都有一個(gè)名稱和一個(gè)類型
指定字段類型
在上面的示例中,所有字段都是標(biāo)量類型(scalar types): 兩個(gè)整數(shù)(page_number和 result_per_page)和一個(gè)字符串(query)。但是也可以為字段指定組合類型,包括枚舉和其他消息類型。
分配字段編號(hào)
指定字段規(guī)則
你可以在 Protocol Buffer Encoding](https://developers.google.com/protocol-buffers/docs/encoding#packed) 中找到關(guān)于packed編碼的更多信息。
添加更多消息類型
可以在一個(gè).proto 文件中定義多個(gè)消息類型。如果你正在定義多個(gè)相關(guān)的消息,這是非常有用的——例如,如果想定義與 SearchRequest 消息類型對(duì)應(yīng)的應(yīng)答消息格式SearchResponse,你就可以將其添加到同一個(gè).proto文件中。
message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;
}message SearchResponse {...
}
添加注釋
要給你的.proto文件添加注釋,需要使用C/C++風(fēng)格的//和/* … */語(yǔ)法。
/* SearchRequest 表示一個(gè)分頁(yè)查詢 * 其中有一些字段指示響應(yīng)中包含哪些結(jié)果 */message SearchRequest {string query = 1;int32 page_number = 2; // 頁(yè)碼數(shù)int32 result_per_page = 3; // 每頁(yè)返回的結(jié)果數(shù)
}
保留字段
如果你通過(guò)完全刪除字段或?qū)⑵渥⑨尩魜?lái)更新消息類型,那么未來(lái)的用戶在對(duì)該類型進(jìn)行自己的更新時(shí)可以重用字段號(hào)。如果其他人以后加載舊版本的相同.proto文件,這可能會(huì)導(dǎo)致嚴(yán)重的問(wèn)題,包括數(shù)據(jù)損壞,隱私漏洞等等。確保這種情況不會(huì)發(fā)生的一種方法是指定已刪除字段的字段編號(hào)(和/或名稱,這也可能導(dǎo)致 JSON 序列化問(wèn)題)是保留的(reserved)。如果將來(lái)有任何用戶嘗試使用這些字段標(biāo)識(shí)符,protocol buffer編譯器將發(fā)出提示。
message Foo {reserved 2, 15, 9 to 11;reserved "foo", "bar";
}
注意,不能在同一個(gè)reserved語(yǔ)句中混合字段名和字段編號(hào)。
從你的.proto文件生成了什么?
標(biāo)量值類型
標(biāo)量消息字段可以具有以下類型之一——該表顯示了.proto文件,以及自動(dòng)生成類中的對(duì)應(yīng)類型(省略了Ruby、C#和Dart):
[1] Kotlin 使用來(lái)自 Java 的相應(yīng)類型,甚至是無(wú)符號(hào)類型,以確?;旌?Java/Kotlin 代碼庫(kù)的兼容性。
[2] 在 Java 中,無(wú)符號(hào)的32位和64位整數(shù)使用它們的有符號(hào)對(duì)應(yīng)項(xiàng)來(lái)表示,最高位存儲(chǔ)在有符號(hào)位中。
[3] 在任何情況下,為字段設(shè)置值都將執(zhí)行類型檢查,以確保其有效。
[4] 64位或無(wú)符號(hào)的32位整數(shù)在解碼時(shí)總是表示為 long ,但如果在設(shè)置字段時(shí)給出 int,則可以表示為 int。在任何情況下,值必須與設(shè)置時(shí)表示的類型相匹配。見(jiàn)[2]。
[5] Python 字符串在解碼時(shí)表示為 unicode,但如果給出了 ASCII 字符串,則可以表示為 str (這可能會(huì)更改)。
[6] 整數(shù)用于64位機(jī)器,字符串用于32位機(jī)器。
默認(rèn)值
當(dāng)解析消息時(shí),如果編碼消息不包含特定的 singular 元素,則解析對(duì)象中的相應(yīng)字段將設(shè)置為該字段的默認(rèn)值。
枚舉
在定義消息類型時(shí),你可能希望其中一個(gè)字段只能是預(yù)定義的值列表中的一個(gè)值。例如,假設(shè)你想為每個(gè) SearchRequest 添加一個(gè)語(yǔ)料庫(kù)字段,其中語(yǔ)料庫(kù)可以是 UNIVERSAL、 WEB、 IMAGES、 LOCAL、 NEWS、 PRODUCTS 或 VIDEO。你可以通過(guò)在消息定義中添加一個(gè)枚舉,為每個(gè)可能的值添加一個(gè)常量來(lái)非常簡(jiǎn)單地完成這項(xiàng)工作。
在下面的例子中,我們添加了一個(gè)名為 Corpus 的enum,包含所有可能的值,以及一個(gè)類型為 Corpus 的字段:
message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;enum Corpus {UNIVERSAL = 0;WEB = 1;IMAGES = 2;LOCAL = 3;NEWS = 4;PRODUCTS = 5;VIDEO = 6;}Corpus corpus = 4;
}
message MyMessage1 {enum EnumAllowingAlias {option allow_alias = true;UNKNOWN = 0;STARTED = 1;RUNNING = 1;}
}
message MyMessage2 {enum EnumNotAllowingAlias {UNKNOWN = 0;STARTED = 1;// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.}
}
預(yù)留值
使用其他消息類型
你可以使用其他消息類型作為字段類型。例如,假設(shè)你希望在每個(gè) SearchResponse消息中包含UI個(gè) Result消息——為了做到這一點(diǎn),你可以在同一個(gè).proto文件中定義 Result消息類型。然后在 SearchResponse中指定 Result 類型的字段。
message SearchResponse {repeated Result results = 1;
}message Result {string url = 1;string title = 2;repeated string snippets = 3;
}
導(dǎo)入定義
在上面的示例中,Result消息類型定義在與 SearchResponse相同的文件中——如果你希望用作字段類型的消息類型已經(jīng)在另一個(gè).proto文件中定義了,該怎么辦?
你可以通過(guò) import 來(lái)使用來(lái)自其他 .proto 文件的定義。要導(dǎo)入另一個(gè).proto 的定義,你需要在文件頂部添加一個(gè) import 語(yǔ)句:
import "myproject/other_protos.proto";
默認(rèn)情況下,只能從直接導(dǎo)入的 .proto 文件中使用定義。但是,有時(shí)你可能需要將 .proto 文件移動(dòng)到新的位置。你可以在舊目錄放一個(gè)占位的.proto文件使用import public 概念將所有導(dǎo)入轉(zhuǎn)發(fā)到新位置,而不必直接移動(dòng).proto文件并修改所有的地方。
使用proto2消息類型
導(dǎo)入 proto2消息類型并在 proto3消息中使用它們是可能的,反之亦然。然而,proto2 enum 不能直接在 proto3語(yǔ)法中使用(如果一個(gè)導(dǎo)入的 proto2消息使用了它們,那沒(méi)問(wèn)題)。
嵌套類型
更新消息類型
如果現(xiàn)有的消息類型不再滿足你的所有需要——例如,你希望消息格式有一個(gè)額外的字段——但是你仍然希望使用用舊格式創(chuàng)建的代碼,不要擔(dān)心!在不破壞任何現(xiàn)有代碼的情況下更新消息類型非常簡(jiǎn)單,只需記住以下規(guī)則:
未知字段
未知字段是格式良好的協(xié)議緩沖區(qū)序列化數(shù)據(jù),表示解析器不識(shí)別的字段。例如,當(dāng)舊二進(jìn)制解析由新二進(jìn)制發(fā)送的帶有新字段的數(shù)據(jù)時(shí),這些新字段將成為舊二進(jìn)制中的未知字段。
最初,proto3消息在解析過(guò)程中總是丟棄未知字段,但在3.5版本中,我們重新引入了未知字段的保存來(lái)匹配 proto2行為。在3.5及以后的版本中,解析期間保留未知字段,并將其包含在序列化輸出中。
Any
oneof
如果你有一條包含多個(gè)字段的消息,并且最多同時(shí)設(shè)置其中一個(gè)字段,那么你可以通過(guò)使用oneof來(lái)實(shí)現(xiàn)并節(jié)省內(nèi)存。
oneof字段類似于常規(guī)字段,只不過(guò)oneof中的所有字段共享內(nèi)存,而且最多可以同時(shí)設(shè)置一個(gè)字段。設(shè)置其中的任何成員都會(huì)自動(dòng)清除所有其他成員。根據(jù)所選擇的語(yǔ)言,可以使用特殊 case()或 WhichOneof() 方法檢查 one of 中的哪個(gè)值被設(shè)置(如果有的話)。
Maps
Packages
定義服務(wù)
JSON 映射
proto3支持 JSON 的規(guī)范編碼,使得系統(tǒng)之間更容易共享數(shù)據(jù)。下表按類型逐一描述了編碼。
如果 json 編碼的數(shù)據(jù)中缺少某個(gè)值,或者該值為 null,那么在解析為 protocol buffer 時(shí),該值將被解釋為適當(dāng)?shù)哪J(rèn)值。如果一個(gè)字段在 protocol buffer 中具有默認(rèn)值,為了節(jié)省空間,默認(rèn)情況下 json 編碼的數(shù)據(jù)中將省略該字段。具體實(shí)現(xiàn)可以提供在JSON編碼中可選的默認(rèn)值。
5、protocol buffers使用指南
protobuf介紹
Protobuf全稱Protocol Buffer,是 Google 公司于2008年開(kāi)源的一種語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)、可擴(kuò)展的用于序列化結(jié)構(gòu)化數(shù)據(jù)——類似于XML,但比XML更小、更快、更簡(jiǎn)單,它可用于(數(shù)據(jù))通信協(xié)議、數(shù)據(jù)存儲(chǔ)等。你只需要定義一次你想要的數(shù)據(jù)結(jié)構(gòu),然后你就可以使用特殊生成的源代碼來(lái)輕松地從各種數(shù)據(jù)流和各種語(yǔ)言中寫入和讀取你的結(jié)構(gòu)化數(shù)據(jù)。目前 Protobuf 被廣泛用作微服務(wù)中的通信協(xié)議。
Go語(yǔ)言使用protoc示例
我們新建一個(gè)名為demo的項(xiàng)目,并且將項(xiàng)目中定義的.proto文件都保存在proto目錄下。
本文后續(xù)的操作命令默認(rèn)都在demo目錄下執(zhí)行。
普通編譯
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/price.proto
protoc -I=proto --go_out=proto --go_opt=paths=source_relative book/price.proto
protoc --go_out=. --go_opt=paths=source_relative proto/book/price.proto
上面的命令都是將代碼生成到demo/proto目錄,如果想要將生成的Go代碼保存在其他文件夾中(例如pb文件夾),那么我們需要先在demo目錄下創(chuàng)建一個(gè)pb文件夾。然后在命令行通過(guò)–go_out=pb指定生成的Go代碼保存的路徑。完整命令如下:
protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative book/price.proto
import同目錄下protobuf文件
隨著業(yè)務(wù)的復(fù)雜度上升,我們可能會(huì)定義多個(gè).proto源文件,然后根據(jù)需要引入其他的protobuf文件。
在這個(gè)示例中,我們?cè)赿emo/proto/book目錄下新建一個(gè)book.proto文件,它通過(guò)import “book/price.proto”;語(yǔ)句引用了同目錄下的price.proto文件。
// demo/proto/book/book.protosyntax = "proto3";// 聲明protobuf中的包名
package book;// 聲明生成的Go代碼的導(dǎo)入路徑
option go_package = "github.com/Q1mi/demo/proto/book";// 引入同目錄下的protobuf文件(注意起始位置為proto_path的下層)
import "book/price.proto";message Book {string title = 1;Price price = 2;
}
編譯命令如下:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto
import其他目錄下文件
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
import google proto文件
protoc --proto_path=/Users/liwenzhou/workspace/go/pkg/mod/github.com/protocolbuffers/protobuf@v3.21.2+incompatible/src/ --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto#### 生成gRPC代碼
生成gRPC代碼
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
gRPC-Gateway
gRPC-Gateway也是日常開(kāi)發(fā)中比較常用的一個(gè)工具,它同樣也是根據(jù) protobuf 生成相應(yīng)的代碼。
安裝工具
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
為protobuf文件添加注釋
編譯
這一次編譯命令在之前的基礎(chǔ)上要繼續(xù)加上 gRPC-Gateway相關(guān)的 --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative 參數(shù)。
完整的編譯命令如下:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
為了方便編譯可以在項(xiàng)目下定義Makefile。
.PHONY: gen helpPROTO_DIR=protogen:protoc \--proto_path=$(PROTO_DIR) \--go_out=$(PROTO_DIR) \--go_opt=paths=source_relative \--go-grpc_out=$(PROTO_DIR) \--go-grpc_opt=paths=source_relative \--grpc-gateway_out=$(PROTO_DIR) \--grpc-gateway_opt=paths=source_relative \$(shell find $(PROTO_DIR) -iname "*.proto")help:@echo "make gen - 生成pb及grpc代碼"
后續(xù)想要編譯只需在項(xiàng)目目錄下執(zhí)行make gen即可。
管理 protobuf
在企業(yè)的項(xiàng)目開(kāi)發(fā)中,我們通常會(huì)把 protobuf 文件存儲(chǔ)到一個(gè)單獨(dú)的代碼庫(kù)中,并在具體項(xiàng)目中通過(guò)git submodule引入。這樣做的好處是能夠?qū)?protobuf 文件統(tǒng)一管理和維護(hù),避免因 protobuf 文件改動(dòng)導(dǎo)致的問(wèn)題。
本文示例代碼已上傳至github倉(cāng)庫(kù):https://github.com/Q1mi/demo,請(qǐng)點(diǎn)擊查看完整代碼。
6、protobuf中使用oneof、WrapValue和FieldMask
本文介紹了在Go語(yǔ)言中如何使用oneof字段以及如何通過(guò)使用google/protobuf/wrappers.proto中定義的類型區(qū)分默認(rèn)值和沒(méi)有傳值;最后演示了Go語(yǔ)言中借助fieldmask-utils庫(kù)使用google/protobuf/field_mask.proto實(shí)現(xiàn)部分更新的方法。
oneof
如果你有一條包含多個(gè)字段的消息,并且最多同時(shí)設(shè)置其中一個(gè)字段,那么你可以通過(guò)使用oneof來(lái)實(shí)現(xiàn)并節(jié)省內(nèi)存。
oneof字段類似于常規(guī)字段,只不過(guò)oneof中的所有字段共享內(nèi)存,而且最多可以同時(shí)設(shè)置一個(gè)字段。設(shè)置其中的任何成員都會(huì)自動(dòng)清除所有其他成員。根據(jù)所選擇的語(yǔ)言,可以使用特殊 case()或 WhichOneof() 方法檢查 one of 中的哪個(gè)值被設(shè)置(如果有的話)。
protobuf定義
serveri端代碼
WrapValue
Golang判斷是息定義零值還是默認(rèn)零值敀方法
protobuf定義
client端代碼
serveri端代碼
syntax = "proto3";package api;option go_package = "protobuffers02/api";import "google/protobuf/wrappers.proto";// 通知讀者的消息
message NoticeReaderRequest{string msg = 1;oneof notice_way{string email = 2;string phone = 3;}
}message Book{string title =1;string author = 2;// int64 price = 3;google.protobuf.Int64Value price = 3; // int64google.protobuf.DoubleValue sale_price = 4; // float64google.protobuf.StringValue memo = 5; // string
}
protoc --proto_path=api \
--go_out=api --go_opt=paths=source_relative \
notice.proto
package mainimport ("fmt""google.golang.org/protobuf/types/known/wrapperspb""protobuffers02/api"
)
// Go語(yǔ)言中判斷是自定義零值還是默認(rèn)零值的方法
//type Book struct{
// //Price int64 // ?區(qū)分默認(rèn)值和0
// // Price sql.NullInt64 // 第一種:自定義結(jié)構(gòu)體
// Price *int64 // 第二種:指針
//}
//
//func foo(){
// var book Book
// if book.Price == nil{
//
// }else{
//
// }
//}func oneofDemo(){// clientres1 := &api.NoticeReaderRequest{Msg: "田毅的博客更新了",NoticeWay: &api.NoticeReaderRequest_Email{Email: "123@xxx.com",},}//res2 := &api.NoticeReaderRequest{// Msg: "李文周的博客更新了",// NoticeWay: &api.NoticeReaderRequest_Phone{// Phone: "12345645678",// },//}// serverreq := res1// 類型斷言switch v := req.NoticeWay.(type){case *api.NoticeReaderRequest_Email:noticeWithEmail(v)case *api.NoticeReaderRequest_Phone:noticeWithPhone(v)}
}func noticeWithEmail(in *api.NoticeReaderRequest_Email){fmt.Printf("notice reader by emali:%v\n",in.Email)
}func noticeWithPhone(in *api.NoticeReaderRequest_Phone){fmt.Printf("notice reader by phone:%v\n",in.Phone)
}func WrapperValueDemo(){// clientbook := api.Book{Title: "跟著天意學(xué)Go語(yǔ)言",//Price: &wrapperspb.Int64Value{Value: 9900},Memo: &wrapperspb.StringValue{Value: "學(xué)就完事了"},}// serverif book.GetPrice() == nil{fmt.Println("沒(méi)有給price賦值")}else {fmt.Println("拿到值了", book.GetPrice().GetValue())}if book.GetMemo() == nil{fmt.Println("沒(méi)有給Memo賦值")}else {fmt.Println("拿到值了", book.GetMemo().GetValue())}
}func main(){oneofDemo()WrapperValueDemo()
}
optional
client端代碼
將值通過(guò)proto包轉(zhuǎn)換成指針類型以適應(yīng) option
server端代碼
FieldMask
client端代碼
我們通過(guò)paths記錄本次更新的字段路徑,如果是嵌套的消息類型則通過(guò)x.y的方式標(biāo)識(shí)。
// clientpaths := []string{"price", "info.b","author","info.a"} // 記錄更新的字段路徑updateReq := api.UpdateBookRequest{Book: &api.Book{Author: "七米2號(hào)",Price: proto.Int64(8800),Info: &api.Book_Info{B:"bbbb",A: "aaaa",},},UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},}
server端代碼
在收到更新消息后,我們需要根據(jù)UpdateMask字段中記錄的更新路徑去讀取更新數(shù)據(jù)。這里借助第三方庫(kù)github.com/mennanov/fieldmask-utils實(shí)現(xiàn)。
// servermask, _ := fieldmask_utils.MaskFromProtoFieldMask(updateReq.UpdateMask, generator.CamelCase)var bookDst = make(map[string]interface{})// 將數(shù)據(jù)讀取到map[string]interface{}// fieldmask-utils支持讀取到結(jié)構(gòu)體等,更多用法可查看文檔。fieldmask_utils.StructToMap(mask, updateReq.Book, bookDst)// do update with bookDstfmt.Printf("bookDst:%#v\n", bookDst)
2022-11-20更新:由于github.com/golang/protobuf/protoc-gen-go/generator包已棄用,而MaskFromProtoFieldMask函數(shù)(簽名如下)
func MaskFromProtoFieldMask(fm *field_mask.FieldMask, naming func(string) string) (Mask, error)
接收的naming參數(shù)本質(zhì)上是一個(gè)將字段掩碼字段名映射到 Go 結(jié)構(gòu)中使用的名稱的函數(shù),它必須根據(jù)你的實(shí)際需求實(shí)現(xiàn)。
例如在我們這個(gè)示例中,還可以使用github.com/iancoleman/strcase包提供的ToCamel方法:
import "github.com/iancoleman/strcase"
import fieldmask_utils "github.com/mennanov/fieldmask-utils"mask, _ := fieldmask_utils.MaskFromProtoFieldMask(updateReq.UpdateMask, strcase.ToCamel)
var bookDst = make(map[string]interface{})
// 將數(shù)據(jù)讀取到map[string]interface{}
// fieldmask-utils支持讀取到結(jié)構(gòu)體等,更多用法可查看文檔。
fieldmask_utils.StructToMap(mask, updateReq.Book, bookDst)
// do update with bookDstfmt.Printf("bookDst:%#v\n", bookDst)
3、GRPC
gRPC是什么
gRPC是一種現(xiàn)代化開(kāi)源的高性能RPC框架,能夠運(yùn)行于任意環(huán)境之中。最初由谷歌進(jìn)行開(kāi)發(fā)。它使用HTTP/2作為傳輸協(xié)議。
HTTP/2 相比 1.0 有哪些重大改進(jìn)?
在gRPC里,客戶端可以像調(diào)用本地方法一樣直接調(diào)用其他機(jī)器上的服務(wù)端應(yīng)用程序的方法,幫助你更容易創(chuàng)建分布式應(yīng)用程序和服務(wù)。與許多RPC系統(tǒng)一樣,gRPC是基于定義一個(gè)服務(wù),指定一個(gè)可以遠(yuǎn)程調(diào)用的帶有參數(shù)和返回類型的的方法。在服務(wù)端程序中實(shí)現(xiàn)這個(gè)接口并且運(yùn)行g(shù)RPC服務(wù)處理客戶端調(diào)用。在客戶端,有一個(gè)stub提供和服務(wù)端相同的方法。
為什么要用gRPC
使用gRPC, 我們可以一次性的在一個(gè).proto文件中定義服務(wù)并使用任何支持它的語(yǔ)言去實(shí)現(xiàn)客戶端和服務(wù)端,反過(guò)來(lái),它們可以應(yīng)用在各種場(chǎng)景中,從Google的服務(wù)器到你自己的平板電腦—— gRPC幫你解決了不同語(yǔ)言及環(huán)境間通信的復(fù)雜性。使用protocol buffers還能獲得其他好處,包括高效的序列化,簡(jiǎn)單的IDL以及容易進(jìn)行接口更新。總之一句話,使用gRPC能讓我們更容易編寫跨語(yǔ)言的分布式代碼。
使用gRPC, 我們可以一次性的在一個(gè).proto文件中定義服務(wù)并使用任何支持它的語(yǔ)言去實(shí)現(xiàn)客戶端和服務(wù)端,反過(guò)來(lái),它們可以應(yīng)用在各種場(chǎng)景中,從Google的服務(wù)器到你自己的平板電腦—— gRPC幫你解決了不同語(yǔ)言及環(huán)境間通信的復(fù)雜性。使用protocol buffers還能獲得其他好處,包括高效的序列化,簡(jiǎn)單的IDL以及容易進(jìn)行接口更新??傊痪湓?#xff0c;使用gRPC能讓我們更容易編寫跨語(yǔ)言的分布式代碼。
安裝gRPC
安裝Protocol Buffers v3
安裝用于生成gRPC服務(wù)代碼的協(xié)議編譯器,最簡(jiǎn)單的方法是從下面的鏈接:https://github.com/google/protobuf/releases下載適合你平臺(tái)的預(yù)編譯好的二進(jìn)制文件(protoc--.zip)。
- 適用Windows 64位protoc-3.20.1-win64.zip
- 適用于Mac Intel 64位protoc-3.20.1-osx-x86_64.zip
- 適用于Mac ARM 64位protoc-3.20.1-osx-aarch_64.zip
- 適用于Linux 64位protoc-3.20.1-linux-x86_64.zip
例如,我使用 Intel 芯片的 Mac 系統(tǒng)則下載 protoc-3.20.1-osx-x86_64.zip 文件,解壓之后得到如下內(nèi)容。
安裝插件
檢查
gRPC的開(kāi)發(fā)方式
編寫.proto文件定義服務(wù)(grpc有1四種工作模式)
像許多 RPC 系統(tǒng)一樣,gRPC 基于定義服務(wù)的思想,指定可以通過(guò)參數(shù)和返回類型遠(yuǎn)程調(diào)用的方法。默認(rèn)情況下,gRPC 使用 protocol buffers作為接口定義語(yǔ)言(IDL)來(lái)描述服務(wù)接口和有效負(fù)載消息的結(jié)構(gòu)。可以根據(jù)需要使用其他的IDL代替。
例如,下面使用 protocol buffers 定義了一個(gè)HelloService服務(wù)。
service HelloService {rpc SayHello (HelloRequest) returns (HelloResponse);
}message HelloRequest {string greeting = 1;
}message HelloResponse {string reply = 1;
}
生成指定語(yǔ)言的代碼
編寫業(yè)務(wù)邏輯代碼
gRPC 幫我們解決了 RPC 中的服務(wù)調(diào)用、數(shù)據(jù)傳輸以及消息編解碼,我們剩下的工作就是要編寫業(yè)務(wù)邏輯代碼。
在服務(wù)端編寫業(yè)務(wù)代碼實(shí)現(xiàn)具體的服務(wù)方法,在客戶端按需調(diào)用這些方法。
gRPC入門示例
編寫proto代碼
Protocol Buffers是一種與語(yǔ)言無(wú)關(guān),平臺(tái)無(wú)關(guān)的可擴(kuò)展機(jī)制,用于序列化結(jié)構(gòu)化數(shù)據(jù)。使用Protocol Buffers可以一次定義結(jié)構(gòu)化的數(shù)據(jù),然后可以使用特殊生成的源代碼輕松地在各種數(shù)據(jù)流中使用各種語(yǔ)言編寫和讀取結(jié)構(gòu)化數(shù)據(jù)。
關(guān)于Protocol Buffers的教程可以查看Protocol Buffers V3中文指南,本文后續(xù)內(nèi)容默認(rèn)讀者熟悉Protocol Buffers。
syntax = "proto3"; // 版本聲明option go_package = "hello_server/proto"; // 項(xiàng)目中import導(dǎo)入生成的Go代碼的名稱package proto; // proto文件模塊// 定義服務(wù)
service Greeter {// 定義方法rpc SayHello (HelloRequest)returns(HelloResponse){}
}// 定義請(qǐng)求消息
message HelloRequest{string name = 1; // 字段序號(hào)
}
// 定義相應(yīng)消息
message HelloResponse{string reply = 1;
}
編寫Server端Go代碼
我們新建一個(gè)hello_server項(xiàng)目,在項(xiàng)目根目錄下執(zhí)行g(shù)o mod init hello_server。
再新建一個(gè)proto文件夾,將上面的 proto 文件保存為hello.proto,將go_package按如下方式修改。
// ...option go_package = "hello_server/proto";// ...
此時(shí),項(xiàng)目的目錄結(jié)構(gòu)為:
hello_server
├── go.mod
├── go.sum
├── main.go

└── pb└── hello.proto
在項(xiàng)目根目錄下執(zhí)行以下命令,根據(jù)hello.proto生成 go 源碼文件。
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
pb/hello.proto
package mainimport ("context""fmt""google.golang.org/grpc""hello_server/proto""net"
)
// grpc servertype server struct{proto.UnimplementedGreeterServer
}// SayHello 是我們需要實(shí)現(xiàn)的方法
// 這個(gè)方法是我們對(duì)外提供的服務(wù)
func (s *server)SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error){reply := "hello" + in.GetName()return &proto.HelloResponse{Reply: reply}, nil
}func main(){// 啟動(dòng)服務(wù)l, err := net.Listen("tcp",":8972")if err != nil {fmt.Println("net.Listen falied, err=", err)return}// 創(chuàng)建grpc服務(wù)s := grpc.NewServer()// 注冊(cè)服務(wù)proto.RegisterGreeterServer(s,&server{})// 啟動(dòng)服務(wù)err = s.Serve(l)if err != nil {fmt.Println("s.Serve(l) failed,err=", err)return}
}
編寫Client端Go代碼
在http_client/main.go文件中按下面的代碼調(diào)用http_server提供的 SayHello RPC服務(wù)。
package mainimport ("context""flag""google.golang.org/grpc""google.golang.org/grpc/credentials/insecure""hello_client/proto""log""time"
)var name = flag.String("name","天意","通過(guò)-name 告訴server你是誰(shuí)?")// grpc 客戶端
// 調(diào)用server端的 SayHello 方法func main(){flag.Parse() // 解析命令行參數(shù)// 連接server端conn,err:=grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))if err != nil {log.Printf("grpc.Dial failed,err=%v\n", err)return}defer conn.Close()// 創(chuàng)建客戶端c := proto.NewGreeterClient(conn) // 使用生成的Go代碼// 調(diào)用rpc方法ctx, cancel := context.WithTimeout(context.Background(),time.Second)defer cancel()resp,err := c.SayHello(ctx, &proto.HelloRequest{Name: *name})if err != nil {log.Printf("c.SayHello failed,err=%v\n", err)return}// 拿到了RPC響應(yīng)log.Printf("resp:%v\n",resp.GetReply())
}
gRPC跨語(yǔ)言調(diào)用
生成Python代碼
新建一個(gè)py_client目錄,將hello.proto文件保存到py_client/pb/目錄下。 在py_client目錄下執(zhí)行以下命令,生成python源碼文件。
cd py_cleint
python3 -m grpc_tools.protoc -Ipb --python_out=. --grpc_python_out=. pb/hello.proto
編寫Python版RPC客戶端
將下面的代碼保存到py_client/client.py文件中。
from __future__ import print_functionimport loggingimport grpc
import hello_pb2
import hello_pb2_grpcdef run():# NOTE(gRPC Python Team): .close() is possible on a channel and should be# used in circumstances in which the with statement does not fit the needs# of the code.with grpc.insecure_channel('127.0.0.1:8972') as channel:stub = hello_pb2_grpc.GreeterStub(channel)resp = stub.SayHello(hello_pb2.HelloRequest(name='q1mi'))print("Greeter client received: " + resp.reply)if __name__ == '__main__':logging.basicConfig()run()
Python RPC 調(diào)用
gRPC_demo完整代碼
4、gRPC流式示例
在上面的示例中,客戶端發(fā)起了一個(gè)RPC請(qǐng)求到服務(wù)端,服務(wù)端進(jìn)行業(yè)務(wù)處理并返回響應(yīng)給客戶端,這是gRPC最基本的一種工作方式(Unary RPC)。除此之外,依托于HTTP2,gRPC還支持流式RPC(Streaming RPC)。
服務(wù)端流式RPC
客戶端發(fā)出一個(gè)RPC請(qǐng)求,服務(wù)端與客戶端之間建立一個(gè)單向的流,服務(wù)端可以向流中寫入多個(gè)響應(yīng)消息,最后主動(dòng)關(guān)閉流;而客戶端需要監(jiān)聽(tīng)這個(gè)流,不斷獲取響應(yīng)直到流關(guān)閉。應(yīng)用場(chǎng)景舉例:客戶端向服務(wù)端發(fā)送一個(gè)股票代碼,服務(wù)端就把該股票的實(shí)時(shí)數(shù)據(jù)源源不斷的返回給客戶端。
我們?cè)诖司帉懸粋€(gè)使用多種語(yǔ)言打招呼的方法,客戶端發(fā)來(lái)一個(gè)用戶名,服務(wù)端分多次返回打招呼的信息。
1、定義服務(wù)
2、服務(wù)端需要實(shí)現(xiàn) LotsOfReplies 方法
// LotsOfReplies 返回使用多種語(yǔ)言打招呼
func (s *server) LotsOfReplies(in *pb.HelloRequest, stream pb.Greeter_LotsOfRepliesServer) error {words := []string{"你好","hello","こんにちは","?????",}for _, word := range words {data := &pb.HelloResponse{Reply: word + in.GetName(),}// 使用Send方法返回多個(gè)數(shù)據(jù)if err := stream.Send(data); err != nil {return err}}return nil
}
3.客戶端調(diào)用LotsOfReplies 并將收到的數(shù)據(jù)依次打印出來(lái)
func runLotsOfReplies(c pb.GreeterClient) {// server端流式RPCctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()stream, err := c.LotsOfReplies(ctx, &pb.HelloRequest{Name: *name})if err != nil {log.Fatalf("c.LotsOfReplies failed, err: %v", err)}for {// 接收服務(wù)端返回的流式數(shù)據(jù),當(dāng)收到io.EOF或錯(cuò)誤時(shí)退出res, err := stream.Recv()if err == io.EOF {break}if err != nil {log.Fatalf("c.LotsOfReplies failed, err: %v", err)}log.Printf("got reply: %q\n", res.GetReply())}
}
客戶端流式RPC
客戶端傳入多個(gè)請(qǐng)求對(duì)象,服務(wù)端返回一個(gè)響應(yīng)結(jié)果。典型的應(yīng)用場(chǎng)景舉例:物聯(lián)網(wǎng)終端向服務(wù)器上報(bào)數(shù)據(jù)、大數(shù)據(jù)流式計(jì)算等。
在這個(gè)示例中,我們編寫一個(gè)多次發(fā)送人名,服務(wù)端統(tǒng)一返回一個(gè)打招呼消息的程序。
1.定義服務(wù)
1.2.服務(wù)端實(shí)現(xiàn)LotsOfGreetings方法
// LotsOfGreetings 接收流式數(shù)據(jù)
func (s *server) LotsOfGreetings(stream pb.Greeter_LotsOfGreetingsServer) error {reply := "你好:"for {// 接收客戶端發(fā)來(lái)的流式數(shù)據(jù)res, err := stream.Recv()if err == io.EOF {// 最終統(tǒng)一回復(fù)return stream.SendAndClose(&pb.HelloResponse{Reply: reply,})}if err != nil {return err}reply += res.GetName()}
}
3.客戶端調(diào)用LotsOfGreetings方法,向服務(wù)端發(fā)送流式請(qǐng)求數(shù)據(jù),接收返回值并打印
func runLotsOfGreeting(c pb.GreeterClient) {ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()// 客戶端流式RPCstream, err := c.LotsOfGreetings(ctx)if err != nil {log.Fatalf("c.LotsOfGreetings failed, err: %v", err)}names := []string{"七米", "q1mi", "沙河娜扎"}for _, name := range names {// 發(fā)送流式數(shù)據(jù)err := stream.Send(&pb.HelloRequest{Name: name})if err != nil {log.Fatalf("c.LotsOfGreetings stream.Send(%v) failed, err: %v", name, err)}}res, err := stream.CloseAndRecv()if err != nil {log.Fatalf("c.LotsOfGreetings failed: %v", err)}log.Printf("got reply: %v", res.GetReply())
}
雙向流式RPC
雙向流式RPC即客戶端和服務(wù)端均為流式的RPC,能發(fā)送多個(gè)請(qǐng)求對(duì)象也能接收到多個(gè)響應(yīng)對(duì)象。典型應(yīng)用示例:聊天應(yīng)用等。
我們這里還是編寫一個(gè)客戶端和服務(wù)端進(jìn)行人機(jī)對(duì)話的雙向流式RPC示例。
1.定義服務(wù)
2.服務(wù)端實(shí)現(xiàn)BidiHello方法
// BidiHello 雙向流式打招呼
func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error {for {// 接收流式請(qǐng)求in, err := stream.Recv()if err == io.EOF {return nil}if err != nil {return err}reply := magic(in.GetName()) // 對(duì)收到的數(shù)據(jù)做些處理// 返回流式響應(yīng)if err := stream.Send(&pb.HelloResponse{Reply: reply}); err != nil {return err}}
}
這里我們還定義了一個(gè)處理數(shù)據(jù)的magic函數(shù),其內(nèi)容如下。
// magic 一段價(jià)值連城的“人工智能”代碼
func magic(s string) string {s = strings.ReplaceAll(s, "嗎", "")s = strings.ReplaceAll(s, "吧", "")s = strings.ReplaceAll(s, "你", "我")s = strings.ReplaceAll(s, "?", "!")s = strings.ReplaceAll(s, "?", "!")return s
}
3.客戶端調(diào)用BidiHello方法,一邊從終端獲取輸入的請(qǐng)求數(shù)據(jù)發(fā)送至服務(wù)端,一邊從服務(wù)端接收流式響應(yīng)
func runBidiHello(c pb.GreeterClient) {ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)defer cancel()// 雙向流模式stream, err := c.BidiHello(ctx)if err != nil {log.Fatalf("c.BidiHello failed, err: %v", err)}waitc := make(chan struct{})go func() {for {// 接收服務(wù)端返回的響應(yīng)in, err := stream.Recv()if err == io.EOF {// read done.close(waitc)return}if err != nil {log.Fatalf("c.BidiHello stream.Recv() failed, err: %v", err)}fmt.Printf("AI:%s\n", in.GetReply())}}()// 從標(biāo)準(zhǔn)輸入獲取用戶輸入reader := bufio.NewReader(os.Stdin) // 從標(biāo)準(zhǔn)輸入生成讀對(duì)象for {cmd, _ := reader.ReadString('\n') // 讀到換行cmd = strings.TrimSpace(cmd)if len(cmd) == 0 {continue}if strings.ToUpper(cmd) == "QUIT" {break}// 將獲取到的數(shù)據(jù)發(fā)送至服務(wù)端if err := stream.Send(&pb.HelloRequest{Name: cmd}); err != nil {log.Fatalf("c.BidiHello stream.Send(%v) failed: %v", cmd, err)}}stream.CloseSend()<-waitc
}
5、metadata
元數(shù)據(jù)(metadata)是指在處理RPC請(qǐng)求和響應(yīng)過(guò)程中需要但又不屬于具體業(yè)務(wù)(例如身份驗(yàn)證詳細(xì)信息)的信息,采用鍵值對(duì)列表的形式,其中鍵是string類型,值通常是[]string類型,但也可以是二進(jìn)制數(shù)據(jù)。gRPC中的 metadata 類似于我們?cè)?HTTP headers中的鍵值對(duì),元數(shù)據(jù)可以包含認(rèn)證token、請(qǐng)求標(biāo)識(shí)和監(jiān)控標(biāo)簽等。
metadata中的鍵是大小寫不敏感的,由字母、數(shù)字和特殊字符-、_、.組成并且不能以grpc-開(kāi)頭(gRPC保留自用),二進(jìn)制值的鍵名必須以-bin結(jié)尾。
元數(shù)據(jù)對(duì) gRPC 本身是不可見(jiàn)的,我們通常是在應(yīng)用程序代碼或中間件中處理元數(shù)據(jù),我們不需要在.proto文件中指定元數(shù)據(jù)。
如何訪問(wèn)元數(shù)據(jù)取決于具體使用的編程語(yǔ)言。 在Go語(yǔ)言中我們是用google.golang.org/grpc/metadata這個(gè)庫(kù)來(lái)操作metadata。
元數(shù)據(jù)可以像普通map一樣讀取。注意,這個(gè) map 的值類型是[]string,因此用戶可以使用一個(gè)鍵附加多個(gè)值。
創(chuàng)建新的metadata
元數(shù)據(jù)中存儲(chǔ)二進(jìn)制數(shù)據(jù)
從請(qǐng)求上下文中獲取元數(shù)據(jù)
發(fā)送和接收元數(shù)據(jù)-客戶端
發(fā)送metadata
接收metadata
發(fā)送和接收元數(shù)據(jù)-服務(wù)器端
普通RPC調(diào)用metadata示例
client端的metadata操作
// unaryCallWithMetadata 普通RPC調(diào)用客戶端metadata操作
func unaryCallWithMetadata(c pb.GreeterClient, name string) {fmt.Println("--- UnarySayHello client---")// 創(chuàng)建metadatamd := metadata.Pairs("token", "app-test-q1mi","request_id", "1234567",)// 基于metadata創(chuàng)建context.ctx := metadata.NewOutgoingContext(context.Background(), md)// RPC調(diào)用var header, trailer metadata.MDr, err := c.SayHello(ctx,&pb.HelloRequest{Name: name},grpc.Header(&header), // 接收服務(wù)端發(fā)來(lái)的headergrpc.Trailer(&trailer), // 接收服務(wù)端發(fā)來(lái)的trailer)if err != nil {log.Printf("failed to call SayHello: %v", err)return}// 從header中取locationif t, ok := header["location"]; ok {fmt.Printf("location from header:\n")for i, e := range t {fmt.Printf(" %d. %s\n", i, e)}} else {log.Printf("location expected but doesn't exist in header")return}// 獲取響應(yīng)結(jié)果fmt.Printf("got response: %s\n", r.Reply)// 從trailer中取timestampif t, ok := trailer["timestamp"]; ok {fmt.Printf("timestamp from trailer:\n")for i, e := range t {fmt.Printf(" %d. %s\n", i, e)}} else {log.Printf("timestamp expected but doesn't exist in trailer")}
}
server端metadata操作
下面的代碼片段演示了server端如何設(shè)置和獲取metadata。
// UnarySayHello 普通RPC調(diào)用服務(wù)端metadata操作
func (s *server) UnarySayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {// 通過(guò)defer中設(shè)置trailer.defer func() {trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))grpc.SetTrailer(ctx, trailer)}()// 從客戶端請(qǐng)求上下文中讀取metadata.md, ok := metadata.FromIncomingContext(ctx)if !ok {return nil, status.Errorf(codes.DataLoss, "UnarySayHello: failed to get metadata")}if t, ok := md["token"]; ok {fmt.Printf("token from metadata:\n")if len(t) < 1 || t[0] != "app-test-q1mi" {return nil, status.Error(codes.Unauthenticated, "認(rèn)證失敗")}}// 創(chuàng)建和發(fā)送header.header := metadata.New(map[string]string{"location": "BeiJing"})grpc.SendHeader(ctx, header)fmt.Printf("request received: %v, say hello...\n", in)return &pb.HelloResponse{Reply: in.Name}, nil
}
流式RPC調(diào)用metadata示例
// bidirectionalWithMetadata 流式RPC調(diào)用客戶端metadata操作
func bidirectionalWithMetadata(c pb.GreeterClient, name string) {// 創(chuàng)建metadata和context.md := metadata.Pairs("token", "app-test-q1mi")ctx := metadata.NewOutgoingContext(context.Background(), md)// 使用帶有metadata的context執(zhí)行RPC調(diào)用.stream, err := c.BidiHello(ctx)if err != nil {log.Fatalf("failed to call BidiHello: %v\n", err)}go func() {// 當(dāng)header到達(dá)時(shí)讀取header.header, err := stream.Header()if err != nil {log.Fatalf("failed to get header from stream: %v", err)}// 從返回響應(yīng)的header中讀取數(shù)據(jù).if l, ok := header["location"]; ok {fmt.Printf("location from header:\n")for i, e := range l {fmt.Printf(" %d. %s\n", i, e)}} else {log.Println("location expected but doesn't exist in header")return}// 發(fā)送所有的請(qǐng)求數(shù)據(jù)到server.for i := 0; i < 5; i++ {if err := stream.Send(&pb.HelloRequest{Name: name}); err != nil {log.Fatalf("failed to send streaming: %v\n", err)}}stream.CloseSend()}()// 讀取所有的響應(yīng).var rpcStatus errorfmt.Printf("got response:\n")for {r, err := stream.Recv()if err != nil {rpcStatus = errbreak}fmt.Printf(" - %s\n", r.Reply)}if rpcStatus != io.EOF {log.Printf("failed to finish server streaming: %v", rpcStatus)return}// 當(dāng)RPC結(jié)束時(shí)讀取trailertrailer := stream.Trailer()// 從返回響應(yīng)的trailer中讀取metadata.if t, ok := trailer["timestamp"]; ok {fmt.Printf("timestamp from trailer:\n")for i, e := range t {fmt.Printf(" %d. %s\n", i, e)}} else {log.Printf("timestamp expected but doesn't exist in trailer")}
}
// BidirectionalStreamingSayHello 流式RPC調(diào)用客戶端metadata操作
func (s *server) BidirectionalStreamingSayHello(stream pb.Greeter_BidiHelloServer) error {// 在defer中創(chuàng)建trailer記錄函數(shù)的返回時(shí)間.defer func() {trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))stream.SetTrailer(trailer)}()// 從client讀取metadata.md, ok := metadata.FromIncomingContext(stream.Context())if !ok {return status.Errorf(codes.DataLoss, "BidirectionalStreamingSayHello: failed to get metadata")}if t, ok := md["token"]; ok {fmt.Printf("token from metadata:\n")for i, e := range t {fmt.Printf(" %d. %s\n", i, e)}}// 創(chuàng)建和發(fā)送header.header := metadata.New(map[string]string{"location": "X2Q"})stream.SendHeader(header)// 讀取請(qǐng)求數(shù)據(jù)發(fā)送響應(yīng)數(shù)據(jù).for {in, err := stream.Recv()if err == io.EOF {return nil}if err != nil {return err}fmt.Printf("request received %v, sending reply\n", in)if err := stream.Send(&pb.HelloResponse{Reply: in.Name}); err != nil {return err}}
6、錯(cuò)誤處理
gRPC code
類似于HTTP定義了一套響應(yīng)狀態(tài)碼,gRPC也定義有一些狀態(tài)碼。Go語(yǔ)言中此狀態(tài)碼由codes定義,本質(zhì)上是一個(gè)uint32。
gRPC Status
Go語(yǔ)言使用的gRPC Status 定義在google.golang.org/grpc/status,使用時(shí)需導(dǎo)入。
import “google.golang.org/grpc/status”
RPC服務(wù)的方法應(yīng)該返回 nil 或來(lái)自status.Status類型的錯(cuò)誤??蛻舳丝梢灾苯釉L問(wèn)錯(cuò)誤。
代碼示例
我們現(xiàn)在要為hello服務(wù)設(shè)置訪問(wèn)限制,每個(gè)name只能調(diào)用一次SayHello方法,超過(guò)此限制就返回一個(gè)請(qǐng)求超過(guò)限制的錯(cuò)誤。
服務(wù)端
使用map存儲(chǔ)每個(gè)name的請(qǐng)求次數(shù),超過(guò)1次則返回錯(cuò)誤,并且記錄錯(cuò)誤詳情。
**package mainimport ("context""fmt""hello_server/pb""net""sync""google.golang.org/genproto/googleapis/rpc/errdetails""google.golang.org/grpc""google.golang.org/grpc/codes""google.golang.org/grpc/status"
)// grpc servertype server struct {pb.UnimplementedGreeterServermu sync.Mutex // count的并發(fā)鎖count map[string]int // 記錄每個(gè)name的請(qǐng)求次數(shù)
}// SayHello 是我們需要實(shí)現(xiàn)的方法
// 這個(gè)方法是我們對(duì)外提供的服務(wù)
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {s.mu.Lock()defer s.mu.Unlock()s.count[in.Name]++ // 記錄用戶的請(qǐng)求次數(shù)// 超過(guò)1次就返回錯(cuò)誤if s.count[in.Name] > 1 {st := status.New(codes.ResourceExhausted, "Request limit exceeded.")ds, err := st.WithDetails(&errdetails.QuotaFailure{Violations: []*errdetails.QuotaFailure_Violation{{Subject: fmt.Sprintf("name:%s", in.Name),Description: "限制每個(gè)name調(diào)用一次",}},},)if err != nil {return nil, st.Err()}return nil, ds.Err()}// 正常返回響應(yīng)reply := "hello " + in.GetName()return &pb.HelloResponse{Reply: reply}, nil
}func main() {// 啟動(dòng)服務(wù)l, err := net.Listen("tcp", ":8972")if err != nil {fmt.Printf("failed to listen, err:%v\n", err)return}s := grpc.NewServer() // 創(chuàng)建grpc服務(wù)// 注冊(cè)服務(wù),注意初始化countpb.RegisterGreeterServer(s, &server{count: make(map[string]int)})// 啟動(dòng)服務(wù)err = s.Serve(l)if err != nil {fmt.Printf("failed to serve,err:%v\n", err)return}
}
客戶端
當(dāng)服務(wù)端返回錯(cuò)誤時(shí),嘗試從錯(cuò)誤中獲取detail信息。
package mainimport ("context""flag""fmt""google.golang.org/grpc/status""hello_client/pb""log""time""google.golang.org/genproto/googleapis/rpc/errdetails""google.golang.org/grpc""google.golang.org/grpc/credentials/insecure"
)// grpc 客戶端
// 調(diào)用server端的 SayHello 方法var name = flag.String("name", "七米", "通過(guò)-name告訴server你是誰(shuí)")func main() {flag.Parse() // 解析命令行參數(shù)// 連接serverconn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))if err != nil {log.Fatalf("grpc.Dial failed,err:%v", err)return}defer conn.Close()// 創(chuàng)建客戶端c := pb.NewGreeterClient(conn) // 使用生成的Go代碼// 調(diào)用RPC方法ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})if err != nil {s := status.Convert(err) // 將err轉(zhuǎn)為statusfor _, d := range s.Details() { // 獲取detailsswitch info := d.(type) {case *errdetails.QuotaFailure:fmt.Printf("Quota failure: %s\n", info)default:fmt.Printf("Unexpected type: %s\n", info)}}fmt.Printf("c.SayHello failed, err:%v\n", err)return}// 拿到了RPC響應(yīng)log.Printf("resp:%v\n", resp.GetReply())
}
7、加密或認(rèn)證
無(wú)加密認(rèn)證
在上面的示例中,我們都沒(méi)有為我們的 gRPC 配置加密或認(rèn)證,屬于不安全的連接(insecure connection)。
Client端:
conn, _ := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := pb.NewGreeterClient(conn)
Server端:
s := grpc.NewServer()
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// error handling omitted
s.Serve(lis)
使用服務(wù)器身份驗(yàn)證 SSL/TLS
gRPC 內(nèi)置支持 SSL/TLS,可以通過(guò) SSL/TLS 證書建立安全連接,對(duì)傳輸?shù)臄?shù)據(jù)進(jìn)行加密處理。
這里我們演示如何使用自簽名證書進(jìn)行server端加密。
生成證書
生成私鑰
生成自簽名的證書
transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0
為了在證書中添加SANs信息,我們將下面自定義配置保存到server.cnf文件中。
[ req ]
default_bits = 4096
default_md = sha256
distinguished_name = req_distinguished_name
req_extensions = req_ext[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = BEIJING
localityName = Locality Name (eg, city)
localityName_default = BEIJING
organizationName = Organization Name (eg, company)
organizationName_default = DEV
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_max = 64
commonName_default = liwenzhou.com[ req_ext ]
subjectAltName = @alt_names[alt_names]
DNS.1 = localhost
DNS.2 = liwenzhou.com
IP = 127.0.0.1
執(zhí)行下面的命令生成自簽名證書——server.crt。
openssl req -nodes -new -x509 -sha256 -days 3650 -config server.cnf -extensions 'req_ext' -key server.key -out server.crt
建立安全連接
8、攔截器(中間件)
gRPC 為在每個(gè) ClientConn/Server 基礎(chǔ)上實(shí)現(xiàn)和安裝攔截器提供了一些簡(jiǎn)單的 API。 攔截器攔截每個(gè) RPC 調(diào)用的執(zhí)行。用戶可以使用攔截器進(jìn)行日志記錄、身份驗(yàn)證/授權(quán)、指標(biāo)收集以及許多其他可以跨 RPC 共享的功能。
在 gRPC 中,攔截器根據(jù)攔截的 RPC 調(diào)用類型可以分為兩類。第一個(gè)是普通攔截器(一元攔截器),它攔截普通RPC 調(diào)用。另一個(gè)是流攔截器,它處理流式 RPC 調(diào)用。而客戶端和服務(wù)端都有自己的普通攔截器和流攔截器類型。因此,在 gRPC 中總共有四種不同類型的攔截器。
客戶端端攔截器
普通攔截器/一元攔截器
UnaryClientInterceptor是客戶端一元攔截器的類型,它的函數(shù)前面如下:
func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
流攔截器
StreamClientInterceptor是客戶端流攔截器的類型。它的函數(shù)簽名是
func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)
server端攔截器
服務(wù)器端攔截器與客戶端類似,但提供的信息略有不同。
普通攔截器/一元攔截器
UnaryServerInterceptor是服務(wù)端的一元攔截器類型,它的函數(shù)簽名是
func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
流攔截器
StreamServerInterceptor是服務(wù)端的一元攔截器類型,它的函數(shù)簽名是
func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error
實(shí)現(xiàn)細(xì)節(jié)類似于客戶端流攔截器部分。
若要為服務(wù)端安裝流攔截器,請(qǐng)使用 StreamInterceptor 的ServerOption來(lái)配置 NewServer。
攔截器示例
下面將演示一個(gè)完整的攔截器示例,我們?yōu)橐辉猂PC和流式RPC服務(wù)都添加上攔截器。
我們首先定義一個(gè)名為valid的校驗(yàn)函數(shù)。
// valid 校驗(yàn)認(rèn)證信息.
func valid(authorization []string) bool {if len(authorization) < 1 {return false}token := strings.TrimPrefix(authorization[0], "Bearer ")// 執(zhí)行token認(rèn)證的邏輯// 這里是為了演示方便簡(jiǎn)單判斷token是否與"some-secret-token"相等return token == "some-secret-token"
}
客戶端攔截器定義
一元攔截器
// unaryInterceptor 客戶端一元攔截器
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {var credsConfigured boolfor _, o := range opts {_, ok := o.(grpc.PerRPCCredsCallOption)if ok {credsConfigured = truebreak}}if !credsConfigured {opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{AccessToken: "some-secret-token",})))}start := time.Now()err := invoker(ctx, method, req, reply, cc, opts...)end := time.Now()fmt.Printf("RPC: %s, start time: %s, end time: %s, err: %v\n", method, start.Format("Basic"), end.Format(time.RFC3339), err)return err
}
流式攔截器
func (w *wrappedStream) RecvMsg(m interface{}) error {logger("Receive a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))return w.ClientStream.RecvMsg(m)
}func (w *wrappedStream) SendMsg(m interface{}) error {logger("Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))return w.ClientStream.SendMsg(m)
}func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {return &wrappedStream{s}
}
這里的wrappedStream嵌入了grpc.ClientStream接口類型,然后又重新實(shí)現(xiàn)了一遍grpc.ClientStream接口的方法。
下面就定義一個(gè)流式攔截器,最后返回上面定義的wrappedStream。
// streamInterceptor 客戶端流式攔截器
func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {var credsConfigured boolfor _, o := range opts {_, ok := o.(*grpc.PerRPCCredsCallOption)if ok {credsConfigured = truebreak}}if !credsConfigured {opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{AccessToken: "some-secret-token",})))}s, err := streamer(ctx, desc, cc, method, opts...)if err != nil {return nil, err}return newWrappedStream(s), nil
}
服務(wù)端攔截器定義
一元攔截器
服務(wù)端定義一個(gè)一元攔截器,對(duì)從請(qǐng)求元數(shù)據(jù)中獲取的authorization進(jìn)行校驗(yàn)。
// unaryInterceptor 服務(wù)端一元攔截器
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {// authentication (token verification)md, ok := metadata.FromIncomingContext(ctx)if !ok {return nil, status.Errorf(codes.InvalidArgument, "missing metadata")}if !valid(md["authorization"]) {return nil, status.Errorf(codes.Unauthenticated, "invalid token")}m, err := handler(ctx, req)if err != nil {fmt.Printf("RPC failed with error %v\n", err)}return m, err
}
流攔截器
同樣為流RPC也定義一個(gè)從元數(shù)據(jù)中獲取認(rèn)證信息的流式攔截器。
// streamInterceptor 服務(wù)端流攔截器
func streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {// authentication (token verification)md, ok := metadata.FromIncomingContext(ss.Context())if !ok {return status.Errorf(codes.InvalidArgument, "missing metadata")}if !valid(md["authorization"]) {return status.Errorf(codes.Unauthenticated, "invalid token")}err := handler(srv, newWrappedStream(ss))if err != nil {fmt.Printf("RPC failed with error %v\n", err)}return err
}
注冊(cè)攔截器
客戶端注冊(cè)攔截器
conn, err := grpc.Dial("127.0.0.1:8972",grpc.WithTransportCredentials(creds),grpc.WithUnaryInterceptor(unaryInterceptor),grpc.WithStreamInterceptor(streamInterceptor),
)
服務(wù)端注冊(cè)攔截器
s := grpc.NewServer(grpc.Creds(creds),grpc.UnaryInterceptor(unaryInterceptor),grpc.StreamInterceptor(streamInterceptor),
)
go-grpc-middleware
社區(qū)中有很多開(kāi)源的常用的grpc中間件— https://github.com/grpc-ecosystem/go-grpc-middleware 可以根據(jù)需要選擇使用。
9、gRPC-Gateway
gRPC-Gateway介紹
gRPC-Gateway 是一個(gè) protoc 插件。它讀取 gRPC 服務(wù)定義并生成一個(gè)反向代理服務(wù)器,該服務(wù)器將 RESTful JSON API 轉(zhuǎn)換為 gRPC。此服務(wù)器根據(jù) gRPC 定義中的自定義選項(xiàng)生成。
鑒于復(fù)雜的外部環(huán)境 gRPC 并不是萬(wàn)能的工具。在某些情況下,我們?nèi)匀幌M峁﹤鹘y(tǒng)的 HTTP/JSON API,來(lái)滿足維護(hù)向后兼容性或者那些不支持 gRPC 的客戶端。但是為我們的RPC服務(wù)再編寫另一個(gè)服務(wù)只是為了對(duì)外提供一個(gè) HTTP/JSON API,這是一項(xiàng)相當(dāng)耗時(shí)和乏味的任務(wù)。
GRPC-Gateway 能幫助你同時(shí)提供 gRPC 和 RESTful 風(fēng)格的 API。GRPC-Gateway 是 Google protocol buffers 編譯器 protoc 的一個(gè)插件。它讀取 Protobuf 服務(wù)定義并生成一個(gè)反向代理服務(wù)器,該服務(wù)器將 RESTful HTTP API 轉(zhuǎn)換為 gRPC。該服務(wù)器是根據(jù)服務(wù)定義中的 google.api.http 注釋生成的。
基本使用示例
使用protobuf定義 gRPC 服務(wù)
新建一個(gè)項(xiàng)目greeter,在項(xiàng)目目錄下執(zhí)行g(shù)o mod init命令完成go module初始化。
在項(xiàng)目目錄下創(chuàng)建一個(gè)proto/helloworld/hello_world.proto文件,其內(nèi)容如下。
syntax = "proto3";package helloworld;option go_package="github.com/Q1mi/greeter/proto/helloworld";// 定義一個(gè)Greeter服務(wù)
service Greeter {// 打招呼方法rpc SayHello (HelloRequest) returns (HelloReply) {}
}// 定義請(qǐng)求的message
message HelloRequest {string name = 1;
}// 定義響應(yīng)的message
message HelloReply {string message = 1;
}
生成代碼
protoc -I=proto \--go_out=proto --go_opt=paths=source_relative \--go-grpc_out=proto --go-grpc_opt=paths=source_relative \helloworld/hello_world.proto
生成pb和gRPC相關(guān)代碼后,在main函數(shù)中注冊(cè)RPC服務(wù)并啟動(dòng)gRPC Server。
// greeter/main.gopackage mainimport ("context""log""net""google.golang.org/grpc"helloworldpb "github.com/Q1mi/greeter/proto/helloworld"
)type server struct {helloworldpb.UnimplementedGreeterServer
}func NewServer() *server {return &server{}
}func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}func main() {// Create a listener on TCP portlis, err := net.Listen("tcp", ":8080")if err != nil {log.Fatalln("Failed to listen:", err)}// 創(chuàng)建一個(gè)gRPC server對(duì)象s := grpc.NewServer()// 注冊(cè)Greeter service到serverhelloworldpb.RegisterGreeterServer(s, &server{})// 啟動(dòng)gRPC Serverlog.Println("Serving gRPC on 0.0.0.0:8080")log.Fatal(s.Serve(lis))
}