1. 程式人生 > >[Golang] 從零開始寫Socket Server(2): 自定義通訊協議

[Golang] 從零開始寫Socket Server(2): 自定義通訊協議

        在上一章我們做出來一個最基礎的demo後,已經可以初步實現Server和Client之間的資訊交流了~ 這一章我會介紹一下怎麼在Server和Client之間實現一個簡單的通訊協議,從而增強整個資訊交流過程的穩定性。

        在Server和client的互動過程中,有時候很難避免出現網路波動,而在通訊質量較差的時候,Client有可能無法將資訊流一次性完整發送,最終傳到Server上的資訊很可能變為很多段。

        如下圖所示,本來應該是分條傳輸的json,結果因為一些原因連線在了一起,這時候就會出現問題啦,Server端要怎麼判斷收到的訊息是否完整呢?~




        唔,答案就是這篇文章的主題啦:在Server和Client互動的時候,加入一個通訊協議(protocol),讓二者的互動通過這個協議進行封裝,從而使Server能夠判斷收到的資訊是否為完整的一段。(也就是解決分包的問題)

        因為主要目的是為了讓Server能判斷客戶端發來的資訊是否完整,因此整個協議的核心思路並不是很複雜:

協議的核心就是設計一個頭部(headers),在Client每次傳送資訊的時候將header封裝進去,再讓Server在每次收到資訊的時候按照預定格式將訊息進行解析,這樣根據

Client傳來的資料中是否包含headers,就可以很輕鬆的判斷收到的資訊是否完整了~

        如果資訊完整,那麼就將該資訊傳送給下一個邏輯進行處理,如果資訊不完整(缺少headers),那麼Server就會把這條資訊與前一條資訊合併繼續處理。


        下面是協議部分的程式碼,主要分為資料的封裝(Enpack)和解析(Depack)兩個部分,其中Enpack用於Client端將傳給伺服器的資料封裝,而Depack是Server用來解析資料,其中Const部分用於定義Headers,HeaderLength則是Headers的長度,用於後面Server端的解析。這裡要說一下ConstMLength,這裡代表Client傳入資訊的長度,因為在golang中,int轉為byte後會佔4長度的空間,因此設定為4。每次Client向Server傳送資訊的時候,除了將Headers封裝進去意以外,還會將傳入資訊的長度也封裝進去,這樣可以方便Server進行解析和校驗。


//通訊協議處理
package protocol

import (
	"bytes"
	"encoding/binary"
)
const (
	ConstHeader         = "Headers"
	ConstHeaderLength   = 7
	ConstMLength = 4
)

//封包
func Enpack(message []byte) []byte {
	return append(append([]byte(ConstHeader), IntToBytes(len(message))...), message...)
}

//解包
func Depack(buffer []byte) []byte {
	length := len(buffer)

	var i int
	data := make([]byte, 32)
	for i = 0; i < length; i = i + 1 {
		if length < i+ConstHeaderLength+ConstMLength {
			break
		}
		if string(buffer[i:i+ConstHeaderLength]) == ConstHeader {
			messageLength := BytesToInt(buffer[i+ConstHeaderLength : i+ConstHeaderLength+ConstMLength])
			if length < i+ConstHeaderLength+ConstMLength+messageLength {
				break
			}
			data = buffer[i+ConstHeaderLength+ConstMLength : i+ConstHeaderLength+ConstMLength+messageLength]

		}
	}

	if i == length {
		return make([]byte, 0)
	}
	return data
}

//整形轉換成位元組
func IntToBytes(n int) []byte {
	x := int32(n)

	bytesBuffer := bytes.NewBuffer([]byte{})
	binary.Write(bytesBuffer, binary.BigEndian, x)
	return bytesBuffer.Bytes()
}

//位元組轉換成整形
func BytesToInt(b []byte) int {
	bytesBuffer := bytes.NewBuffer(b)

	var x int32
	binary.Read(bytesBuffer, binary.BigEndian, &x)

	return int(x)
}


        協議寫好之後,接下來就是在Server和Client的程式碼中應用協議啦,下面是Server端的程式碼,主要負責解析Client通過協議發來的資訊流:


package main  
  
import (  
    "protocol"  
    "fmt"  
    "net"  
    "os"  
)  
  
func main() {  
    netListen, err := net.Listen("tcp", "localhost:6060")  
    CheckError(err)  
  
    defer netListen.Close()  
  
    Log("Waiting for clients")  
    for {  
        conn, err := netListen.Accept()  
        if err != nil {  
            continue  
        }  
  
        //timeouSec :=10  
        //conn.  
        Log(conn.RemoteAddr().String(), " tcp connect success")  
        go handleConnection(conn)  
  
    }  
}  
  
func handleConnection(conn net.Conn) {  
  
  
    // 緩衝區,儲存被截斷的資料  
    tmpBuffer := make([]byte, 0)  
  
    //接收解包  
    readerChannel := make(chan []byte, 16)  
    go reader(readerChannel)  
  
    buffer := make([]byte, 1024)  
    for {  
    n, err := conn.Read(buffer)  
    if err != nil {  
    Log(conn.RemoteAddr().String(), " connection error: ", err)  
    return  
    }  
  
    tmpBuffer = protocol.Depack(append(tmpBuffer, buffer[:n]...))  
    }  
    defer conn.Close()  
}  
  
func reader(readerChannel chan []byte) {  
    for {  
        select {  
        case data := <-readerChannel:  
            Log(string(data))  
        }  
    }  
}  
  
func Log(v ...interface{}) {  
    fmt.Println(v...)  
}  
  
func CheckError(err error) {  
    if err != nil {  
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())  
        os.Exit(1)  
    }  
}  



        然後是Client端的程式碼,這個簡單多了,只要給資訊封裝一下就可以了~:


package main  
import (  
"protocol"  
"fmt"  
"net"  
"os"  
"time"  
"strconv"  
  
)  
  
func send(conn net.Conn) {  
    for i := 0; i < 100; i++ {  
        session:=GetSession()  
        words := "{\"ID\":"+ strconv.Itoa(i) +"\",\"Session\":"+session +"2015073109532345\",\"Meta\":\"golang\",\"Content\":\"message\"}"  
        conn.Write(protocol.Enpacket([]byte(words)))  
    }  
    fmt.Println("send over")  
    defer conn.Close()  
}  
  
func GetSession() string{  
    gs1:=time.Now().Unix()  
    gs2:=strconv.FormatInt(gs1,10)  
    return gs2  
}  
  
func main() {  
    server := "localhost:6060"  
    tcpAddr, err := net.ResolveTCPAddr("tcp4", server)  
    if err != nil {  
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())  
        os.Exit(1)  
    }  
  
    conn, err := net.DialTCP("tcp", nil, tcpAddr)  
    if err != nil {  
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())  
        os.Exit(1)  
    }  
  
  
    fmt.Println("connect success")  
    send(conn)  
  
  
  
}  



        這樣我們就成功實現在Server和Client之間建立一套自定義的基礎通訊協議啦,讓我們執行一下看下效果:




成功識別每一條Client發來的資訊啦~~

更多詳細資訊可以參考這篇文章: golang中tcp socket粘包問題和處理



我已經把SocketServer系列的程式碼整合到了一起,釋出到了我個人的github上:點選連結, 希望大家有興趣的可以學習star一下~