標籤:有一個 flags 服務 逾時 說明 報錯 name return 請求
背景
目前後台業務系統的大部分介面都是以同步阻塞式的方式工作,資源使用率低,單機qps有限。因為go語言原生支援協程,能夠同時滿足開發效率和程式效能,於是決定引入go語言進行改造。
主要是分享以下三點心得:
- C/C++庫的封裝
- map內部成員賦值,以及protobuf協議的支援
- 網路I/O逾時處理
C/C++庫的封裝
環境搭建,文法這些就不贅述了。閱讀 A tour of go 可以很好的認識go語言。
引入go語言,碰到的第一個問題是如何複用已有的經過了長期線上檢驗的C/C++基礎庫,所幸go語言通過Cgo可以很方便的調用C庫中函數。具體方法見Command cgo。在使用過程中發現,Cgo可以支援C,但無法完美支援C++,在import C關鍵字上面include進來的標頭檔中,如果有包含C++的標頭檔時例如string,Cgo將會報錯,另外C++的namespace也會讓我們無法用go語言訪問namespace下的函數或變數。解決辦法是用C來包裹C++,通過C來調用C++,go來調用C解決。如我有一個用C++編寫的庫libtest.a和test.h,其中test.h中有包含C++中的string,這時候按照Command cgo中的方法,在test.go檔案中引入:
packege test// #cgo LDFLAGS: -L/usr/local/comm/lib -ltest -lstdc++// #cgo CPPFLAGS: -I/usr/local/comm/include// #include "test.h"
編譯過程中會報錯。注意,上面使用的都是絕對路徑,這是因為當我們在build go項目的時候,相對路徑是以執行build命令時的路徑來作為起點的,否則一旦build時的目錄改變,就會導致Cgo封裝失敗。
這時候,為了調用test庫,我們只好再寫一個t.h和t.cpp(隨便取的)
其中t.h的內容
#ifdef __cplusplusextern "C" {#endifvoid C_test();//我們通過該函數來調用test庫中的函數,這裡的命名都是隨便取的#ifdef __cplusplus}#endif
在t.cpp檔案的內容:
#include "t.h"#include "test.h"#ifdef __cplusplusextern "C" {#endifvoid C_test(){ 以C_test函數來封裝test庫中的函數 ...}#ifdef __cplusplus}#endif
通過將t.cpp編譯成目標檔案:
g++ -c t.cpp -Iabcd -Lefg -ltest
有兩種方式,一是將t.o打包進原來的libtest.a中,第二種當然就是將其打包為獨立的一個靜態庫,這裡推崇第二種方式:
ar -r libt.a t.o
這時候在對應的go檔案中引入t.h和t.a即可完成go對C++庫的調用。
packege test// #cgo LDFLAGS: -L./ -lt -L/usr/local/comm/lib -ltest -lstdc++// #cgo CPPFLAGS: -I./ -I/usr/local/comm/include// #include "t.h"
map內部成員賦值,以及protobuf協議的支援
在go語言的使用過程中,發現map結構中的value為自訂類型時,無法對該自訂類型內部的成員進行進行"寫"操作(map結構中返回的value不可對其成員定址),如運行以下代碼:
package mainimport "fmt"type A struct{ T int}func main(){ m := make(map[int]A) a := A{1} m[1] = a m[1].T = 2 fmt.Println(m)}
編譯器會返回不可對m[1].T賦值的錯誤。但是當map中的value為指標時即可,如將第7,8,9行代碼換成如下:
m := make(map[int]*A) a := A{1} m[1] = &a
這時即可完成對m[1].T的賦值。另外在map結構中的value類型為go的原生類型時則不存在這個問題(即byte/int8/16/32/64 float32/64,string,map,slice等等),如:
m := make(map[int]map[int]int) ma := make(map[int]int) ma[1] = 1 m[1] = ma m[1][1] = -1 fmt.Println(m)
這時讀寫都沒問題。但是在slice中卻不存在這個問題。
在go中,將protobuf協議檔案產生相應的go檔案之後,對於每個欄位產生的變數都是指標類型。protobuf協議在git上有golang開源的protobuf協議(畢竟都是google的東西),詳細使用可以參見連結這裡簡單談一下對於用指標的理解,使用指標可以更大程度的達到節流的目的,(而這裡的節流帶來的好處是更快的編解碼速度,資料包互動完成速度(因為要傳輸的資料量少了),節省頻寬),我們通過判斷指標值是否為nil,為nil時則直接跳過,不為nil時說明有資料才將其序列化。
網路I/O逾時處理
由於複雜的網路環境,我們必須考慮對來自用戶端的每個請求的收包和回包以及系統內部調用鏈之間的網路請求設定逾時處理,否則系統可能會存在大量無法釋放的串連。
1 http server與client之間的逾時設定
對於http伺服器一般都是使用go標準庫中的http包,只需要簡單幾行代碼即可建立一個http server,如:
http.HandleFunc("/hello",func(w http.ResponseWriter,r *http.Request){ io.WriteString(w,"hello world") });http.ListenAndServe()
這裡其實是使用了內部的變數DefaultServeMux,但是它預設並不支援I/O逾時,因此我們需要自己建立http.Server來提供服務:
svr := &http.Server{ Addr: "0.0.0.0:8080", ReadTimeout: 4 * time.Second, WriteTimeout: 4 * time.Second,}svr.ListenAndServe()
其中ReadTimeOut是從Accept請求後到RequestBody完全讀取的時間,WriteTimeOut是從RequstBody開始讀取到完整回包的時間。
2 系統內部調用鏈的逾時設定
系統內部的服務之間經常需要協同服務才能完成對外的請求,因此通訊無法避免。通過給建立好的連線物件net.Connd調用SetDeadline,SetReadDeadline,SetWriteDeadline三個方法設定Deadline可以達到對每次I/O進行逾時控制,一旦逾時後對該連線物件進行Close操作。顧名思義,就是分別對連線物件設定讀寫逾時,讀逾時和寫逾時的函數,他們的設定是永久生效而不是只作用於一次I/O,一旦逾時後將返回逾時錯誤,因此每次I/O操作前都需要調用它們,這裡貼一個簡單的例子:
func SendOnePacket(conn net.Conn, packet []byte, timeout int64) error { conn.SetWriteDeadline(time.Now().Add(time.Duration(int64(time.Millisecond) * timeout))) for { n, err := conn.Write(packet) if err != nil { return errors.New("Write error:" + err.Error()) } if n == len(packet) { return nil } packet = packet[n:] } return nil}func RecvOnePacket(conn net.Conn, timeout int64) ([]byte, error) { conn.SetReadDeadline(time.Now().Add(time.Duration(int64(time.Millisecond)*timeout))) recvBuf := bytes.NewBuffer(nil) var msgLen int = 0 var buf [4096]byte for { n, err := conn.Read(buf[0:]) recvBuf.Write(buf[0:n]) if err != nil && err != io.EOF { str := string(recvBuf.Bytes()) return nil, errors.New("Read Error:" + err.Error()+ " data:"+str) } msgLen = CheckPacket(recvBuf) if msgLen > 0 { break } if msgLen < 0 { return nil,errors.New("CheckPacket error,ret:" + strconv.FormatInt(int64(msgLen),10)) } } packet := recvBuf.Bytes() return packet[:msgLen], nil}
CheckPacket函數根據自己的通訊協定來實現,用於檢查包是否完整,返回負數代表出錯,為0表示不完整,大於0表示是接收完成,且該值為該資料包的完整長度。其中RecvOnePacket中的buf數組其實非常講究,它會決定每一次的系統調用---Read函數,最多讀取多少個位元組(如果傳入Read函數的切片長度為0的話直接就返回了),這個樣本並為對資料緩衝,因此出現粘包的話就雪崩了。
總結
目前小組內部也是剛引入go語言,對go的特性,深層次的一些實現理解還很淺。
go語言初體驗