Go: 拆解網絡數據包(1)

通常從網絡連接中讀取數據後,都需要對接收的數據進行處理,也就意味着你的代碼需要理解接收到的數據內容。由於 TCP 是面向流的協議,客戶端可以接收多個數據包的字節流。與我們能理解的普通語句不同,二進制數據不包括固有的標點符號,不能告訴你一條信息從哪裏開始和在哪裏結束。例如下面的方式讀取的數據只能是一堆字節流,無法理解具體內容。

buf := make([]byte, 1 << 19) //512KB
    for  {
        n, err := conn.Read(buf)
        if err != nil {
            if err != io.EOF {
                t.Error(err)
            }
            break
        }
        t.Logf("read %d bytes", n)
    }

再舉個例子,如果你寫代碼要從一個服務器上面讀取一封電子郵件,你的代碼必須檢查每個字節,並通過分隔符來判斷信息流的分界。或者,客戶端可能已經與服務器建立了協議,服務器發送固定數量的字節,以指示服務器接下來將發送的有效負載大小。你的代碼可以根據這個字節數來爲負載創建合適的讀取緩衝區。我們將在第二部分中通過例子說明。

如果你選擇使用分隔符來表示一個消息的結尾和另一個消息的開始的話,處理邊界的代碼不會很簡單。例如,你可能從網絡連接中讀取了 1KB 的數據但是發現內容中包含兩個分隔符。這表示你有兩個完整的消息,但是,關於第二個分隔符後面的數據塊,您沒有足夠的信息來知道它是否也是一個完整的消息。如果你再讀取 1KB 的數據而且沒發現分隔符,你可以得出這 1KB 的數據塊是和前面 1KB 是連續的。如果你讀取到 1KB 到分隔符怎麼處理呢?

以上內容看起來有些複雜,這是因爲你必須考慮多個 Read 調用之間的數據,並在此過程中處理任何錯誤。每當你想用自己的方法來解決這個問題時,查看下標準庫是否有現成可用的實現。剛說的字節流分隔的問題,可以使用標準庫中的 bufio.Scanner 來實現,它實現了對讀取的流數據的分隔。bufio.Scanner 是 Go 標準庫中的結構,可以讀取帶分隔符的數據。Scanner 接收一個 io.Reader 對象作爲參數。因爲 net.Conn 實現了 Read 方法,也就實現了 io.Reader 接口,你可以使用 Scanner 輕鬆地讀取網絡連接中帶分隔符的數據。如以下代碼所示:

const payload = "The bigger the interface, the weaker the abstraction."

func TestScanner(t *testing.T)  {
    //服務端
    listener, err := net.Listen("tcp", "127.0.0.1:")
    if err != nil {
        t.Fatal(err)
    }
    go func() {
        conn, err := listener.Accept()
        if err != nil {
            t.Error(err)
            return
        }
        defer conn.Close()
        _, err = conn.Write([]byte(payload))
        if err != nil {
            t.Error(err)
        }
    }()
    //客戶端
    conn, err := net.Dial("tcp", listener.Addr().String())
    if err != nil {
        t.Fatal(err)
    }
    defer conn.Close()

    scanner := bufio.NewScanner(conn)
    scanner.Split(bufio.ScanWords)
    var words []string
    for scanner.Scan(){
        words = append(words, scanner.Text())
    }
    err = scanner.Err()
    if err != nil {
        t.Error(err)
    }
    expected := []string{"The", "bigger", "the", "interface,", "the",
        "weaker", "the", "abstraction."}
    if !reflect.DeepEqual(words, expected) {
        t.Fatal("inaccurate scanned word list")
    }
    t.Logf("Scanned words: %#v", words)
}

以上代碼 listener 部分很容易理解,目的是將通過網絡連接將 payload 發送給客戶端。使用 bufio.Scanner 讀取連接中的字符串,通過空格符分隔數據塊。客戶端,因爲知道正在讀取的是字符串,可以使用 bufio.Scanner 從網絡連接中讀取。默認情況,scanner 通過換行符('\n') 來分隔讀取到的字節流數據。相反,這裏選擇使用 bufio.ScanWords 以空格作爲分隔符,讀出字節流中的單詞。每當碰到一個空格符作爲讀取一部分數據的邊界直到碰到 io.EOF 結束。每次對 Scan 的調用都可能導致對網絡連接 Read 方法的多次調用,直到 scanner 找到它的分隔符或從連接中讀取錯誤爲止。它隱藏了從網絡連接中進行一次或多次讀取時搜索分隔符的複雜性,並返回結果消息。

調用 scanner 的 Text 方法會以字符串格式返回分隔出來的數據塊,本例中就是一個單詞和相鄰的標點符號。代碼通過 for 循環連續的讀取網絡連接中的字符串,直到 scanner 接收到 io.EOF 或者其他錯誤爲止。

運行以上測試用例:

go test -v -run=^TestScanner . 
=== RUN   TestScanner
    code_test.go:258: Scanned words: []string{"The", "bigger", "the", "interface,", "the", "weaker", "the", "abstraction."}
--- PASS: TestScanner (0.00s)
PASS
ok      awesomeProject  0.750s
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s?__biz=MzkyNTI1MzI5Nw==&amp;mid=2247484372&amp;idx=1&amp;sn=130e3392cda90bc6464fc5f0a272c19a&amp;chksm=c1c8280af6bfa11c803156b6507b21c5bd160e3641f407e974eae4518d5ae449a1c9608aaf77&amp;scene=178&amp;cur_album_id=2003145170782912517#rd