Golang 構建網路傳輸資料包
網路通訊中,端與端之間只能傳輸二進位制資料流。TCP/IP協議的解析已經完全交給了硬體裝置完成,即便是軟路由等用伺服器上裝軟體來替代硬體裝置也已經相當成熟。我們需要面對的都是應用層的通訊問題。而大部分情況下也無需考慮通訊細節,因為總有各種框架比如長連線的websocket框架,處理HTTP協議的網站框架。或者直接提供網路訪問包比如訪問資料庫的包,各種訊息佇列服務包。總之網路通訊只剩下序列化物件,傳送出去,另一端接收,反序列化成物件。甚至有些框架序列化的步驟都給省略了。
前人造出了各種輪子,所以我們只需要裝幾個沙發蒙張皮就可以賣車了。但深入研究才是根本。有時我們需要自行構建一套簡單的協議來實現客戶端與伺服器或者不同程式之間通訊。當然按照一些有影響力的協議來實現會更具有通用性,更規範安全。不過實現難度可就大大提高了。
定長資料包
golang建立網路連線還是相當容易的。以下程式碼為了儘可能清爽,省略錯誤處理等。客戶端和伺服器端都採用定長資料包,且採用 請求1 > 響應1 >> 請求2 > 響應2 這樣的模式通訊。
func main() {
go server()
client()
time.Sleep(time.Second * 4)
}
//客戶端實現
func client() {
con, _ := net.Dial("tcp", "127.0.0.1:6666")
buf := make([]byte, 50000)
for i := 0; i < 5; i++ {
data := make([]byte, 10000)
copy(data, []byte("PING"+string.))
con.Write(data)
n, _ := io.ReadFull(con, buf)
msg := string(buf[:n])
fmt.Printf("%v %v\n", msg[0:4], len(msg))
}
con.Close()
}
//伺服器端實現
func server() {
s, _ := net.Listen("tcp" , "127.0.0.1:6666")
for {
c, _ := s.Accept()
go func() {
fmt.Println("someone connected")
//固定PING資料包長度為10000
buf := make([]byte, 10000)
for {
//讀滿資料
n, err := io.ReadFull(c, buf)
if err != nil {
fmt.Println(err)
break
}
msg := string(buf[:n])
fmt.Printf("%v %v\n", msg[0:4], len(msg))
//固定PONG資料包長度為50000
data := make([]byte, 50000)
copy(data, []byte("PONG"))
c.Write(data)
}
fmt.Println("disconnect")
}()
}
}
output:
someone connected
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
EOF
disconnect
定長資料包是最容易處理的。如果實際應用中資料包長度平均,可以加一部分填補空資料進去形成定長資料。雖然浪費了一定的網路頻寬,但有得有失也是一種解決方案。
不定長資料包
不定長的資料包才是實際應用中經常遇到的。因此必須要解決資料流通訊的包完整性問題和粘包問題。包的完整性當然是接收時得接收夠資料。粘包問題是接收當前資料包時不能多擷取到下一個包的資料。解決方案中網站這種開啟連線傳資料,傳完資料關連線是最方便的。不過應用中除了網站很少採用這種模式通訊。那麼另一種解決方案就是在每個包末尾加個結束符號,當讀取到結束符號時,即代表包完整結束,否則一直讀取資料。
//客戶端實現
func client() {
con, _ := net.Dial("tcp", "127.0.0.1:6666")
r := bufio.NewReaderSize(con, 5000) //緩衝區大小大於資料包
data := make([]byte, 100)
for i := 0; i < 5; i++ {
data = append(data, data...)
copy(data, []byte("PING"))
con.Write(data)
con.Write([]byte{' '}) //以空格作為結束符
//接收以空格符作為結束的資料包
s, _ := r.ReadSlice(' ')
msg := string(s[:len(s)-1])
fmt.Printf("%v %v\n", msg[0:4], len(msg))
}
con.Close()
}
//伺服器端實現
func server() {
s, _ := net.Listen("tcp", "127.0.0.1:6666")
for {
c, _ := s.Accept()
go func() {
fmt.Println("someone connected")
//緩衝區大小大於資料包
r := bufio.NewReaderSize(c, 5000)
data := make([]byte, 90)
for {
//接收以空格符作為結束的資料包
s, err := r.ReadSlice(' ')
if err != nil {
fmt.Println(err)
break
}
msg := string(s[:len(s)-1])
fmt.Printf("%v %v\n", msg[0:4], len(msg))
data = append(data, data...)
copy(data, []byte("PONG"))
c.Write(data)
c.Write([]byte{' '})//以空格作為結束符
}
fmt.Println("disconnect")
}()
}
}
output:
someone connected
PING 200
PONG 180
PING 400
PONG 360
PING 800
PONG 720
PING 1600
PONG 1440
PING 3200
PONG 2880
EOF
disconnect
這種方式當資料包中的資料含有結束符時就會出錯。因此我們需要對資料進行一次編碼,比如Base64。Base64是網路上最常見的用於傳輸8Bit位元組碼的編碼方式之一,是一種基於64個可列印字元來表示二進位制資料的方法。編碼後的資料比原始資料略長,為原來的1.3倍。在電子郵件中,根據RFC822規定,每76個字元,還需要加上一個回車換行。可以估算編碼後資料長度大約為原長的135.1%。下面是通訊例子中更改的部分。
//傳送時
//將連線con用base64包裝一下,對data進行編碼
w := base64.NewEncoder(base64.RawStdEncoding, con)
w.Write(data)
w.Close()
//寫入結束符
con.Write([]byte{' '})
//接收時
s, _ := r.ReadSlice(' ')
//去掉結束符
s = s[:len(s)-1]
//解碼接收到的資料
de := base64.RawStdEncoding
d := make([]byte, de.DecodedLen(len(s)))
de.Decode(d, s)
msg := string(d)
以上方案只能適用簡單場景,耗費頻寬或者效能不高。
結構化資料包
結構化資料包應該是最常採用的方案。資料包擁有固定長度的訊息頭,不定長度的訊息體,訊息頭內含訊息體的長度。每次解析先讀取固定長度訊息頭,通過訊息頭內的訊息體長度再次讀取完整的訊息體。訊息頭內還可以包含各種命令,狀態等資料,能在解析訊息體之前先做一步業務處理。定義訊息頭的資料結構和含義的一整套規則可以統稱為xxx協議。比如下面這個是websocket協議的包結構定義。關於websocket的詳細資料可以在度娘上輕鬆找到。這裡只是借來做個例子。
FIN
標識是否為此訊息的最後一個數據包,佔 1 bit
RSV1, RSV2, RSV3: 用於擴充套件協議,一般為0,各佔1bit
Opcode
資料包型別(frame type),佔4bits
0x0:標識一箇中間資料包
0x1:標識一個text型別資料包
0x2:標識一個binary型別資料包
0x3-7:保留
0x8:標識一個斷開連線型別資料包
0x9:標識一個ping型別資料包
0xA:表示一個pong型別資料包
0xB-F:保留
MASK:佔1bits
用於標識PayloadData是否經過掩碼處理。如果是1,Masking-key域的資料即是掩碼金鑰,用於解碼PayloadData。客戶端發出的資料幀需要進行掩碼處理,所以此位是1。
Payload length
Payload data的長度,佔7bits,7+16bits,7+64bits:
如果其值在0-125,則是payload的真實長度。
如果值是126,則後面2個位元組形成的16bits無符號整型數的值是payload的真實長度。注意,網路位元組序,需要轉換。
如果值是127,則後面8個位元組形成的64bits無符號整型數的值是payload的真實長度。注意,網路位元組序,需要轉換。
處理websocket協議太麻煩了,在此定義一種新協議A,就一條規則:首4位元組為命令,後4位元組為訊息體長度。程式碼例子如下
//客戶端實現
func client() {
con, _ := net.Dial("tcp", "127.0.0.1:6666")
data := make([]byte, 10)
head := make([]byte, 8)
for i := 0; i < 4; i++ {
data = append(data, data...)
//4位元組的命令
copy(data, []byte("PING"))
//4位元組的訊息體長度
binary.BigEndian.PutUint32(data[4:8], uint32(len(data)-8))
con.Write(data)
io.ReadFull(con, head)
//取出命令
cmd := string(head[0:4])
//取出訊息體長度
bodylen := int(binary.BigEndian.Uint32(head[4:8]))
//按長度, 再次讀取訊息體
buf := make([]byte, bodylen)
io.ReadFull(con, buf)
msg := buf
fmt.Printf("%v %v\n", cmd, len(msg)+8)
}
con.Close()
}
//伺服器端實現
func server() {
s, _ := net.Listen("tcp", "127.0.0.1:6666")
for {
c, _ := s.Accept()
go func() {
fmt.Println("someone connected")
data := make([]byte, 9)
head := make([]byte, 8)
for {
_, err := io.ReadFull(c, head)
if err != nil {
fmt.Println(err)
break
}
cmd := string(head[0:4])
bodylen := int(binary.BigEndian.Uint32(head[4:8]))
buf := make([]byte, bodylen)
_, err2 := io.ReadFull(c, buf)
if err2 != nil {
fmt.Println(err2)
break
}
msg := buf
fmt.Printf("%v %v\n", cmd, len(msg)+8)
data = append(data, data...)
copy(data, []byte("PONG"))
binary.BigEndian.PutUint32(data[4:8], uint32(len(data)-8))
c.Write(data)
}
fmt.Println("disconnect")
}()
}
}
output:
someone connected
PING 20
PONG 18
PING 40
PONG 36
PING 80
PONG 72
PING 160
PONG 144
EOF
disconnect