Go: 拆解網(wǎng)絡(luò)數(shù)據(jù)包(1)

通常從網(wǎng)絡(luò)連接中讀取數(shù)據(jù)后,都需要對接收的數(shù)據(jù)進行處理,也就意味著你的代碼需要理解接收到的數(shù)據(jù)內(nèi)容。由于TCP是面向流的協(xié)議,客戶端可以接收多個數(shù)據(jù)包的字節(jié)流。與我們能理解的普通語句不同,二進制數(shù)據(jù)不包括固有的標(biāo)點符號,不能告訴你一條信息從哪里開始和在哪里結(jié)束。例如下面的方式讀取的數(shù)據(jù)只能是一堆字節(jié)流,無法理解具體內(nèi)容。

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)
    }

再舉個例子,如果你寫代碼要從一個服務(wù)器上面讀取一封電子郵件,你的代碼必須檢查每個字節(jié),并通過分隔符來判斷信息流的分界?;蛘?,客戶端可能已經(jīng)與服務(wù)器建立了協(xié)議,服務(wù)器發(fā)送固定數(shù)量的字節(jié),以指示服務(wù)器接下來將發(fā)送的有效負載大小。你的代碼可以根據(jù)這個字節(jié)數(shù)來為負載創(chuàng)建合適的讀取緩沖區(qū)。我們將在第二部分中通過例子說明。

如果你選擇使用分隔符來表示一個消息的結(jié)尾和另一個消息的開始的話,處理邊界的代碼不會很簡單。例如,你可能從網(wǎng)絡(luò)連接中讀取了1KB的數(shù)據(jù)但是發(fā)現(xiàn)內(nèi)容中包含兩個分隔符。這表示你有兩個完整的消息,但是,關(guān)于第二個分隔符后面的數(shù)據(jù)塊,您沒有足夠的信息來知道它是否也是一個完整的消息。如果你再讀取1KB的數(shù)據(jù)而且沒發(fā)現(xiàn)分隔符,你可以得出這1KB的數(shù)據(jù)塊是和前面1KB是連續(xù)的。如果你讀取到1KB到分隔符怎么處理呢?

以上內(nèi)容看起來有些復(fù)雜,這是因為你必須考慮多個Read調(diào)用之間的數(shù)據(jù),并在此過程中處理任何錯誤。每當(dāng)你想用自己的方法來解決這個問題時,查看下標(biāo)準(zhǔn)庫是否有現(xiàn)成可用的實現(xiàn)。剛說的字節(jié)流分隔的問題,可以使用標(biāo)準(zhǔn)庫中的bufio.Scanner來實現(xiàn),它實現(xiàn)了對讀取的流數(shù)據(jù)的分隔。bufio.Scanner是Go標(biāo)準(zhǔn)庫中的結(jié)構(gòu),可以讀取帶分隔符的數(shù)據(jù)。Scanner接收一個io.Reader對象作為參數(shù)。因為net.Conn實現(xiàn)了Read方法,也就實現(xiàn)了io.Reader接口,你可以使用Scanner輕松地讀取網(wǎng)絡(luò)連接中帶分隔符的數(shù)據(jù)。如以下代碼所示:

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

func TestScanner(t *testing.T)  {
    //服務(wù)端
    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部分很容易理解,目的是將通過網(wǎng)絡(luò)連接將payload發(fā)送給客戶端。使用bufio.Scanner讀取連接中的字符串,通過空格符分隔數(shù)據(jù)塊??蛻舳耍驗橹勒谧x取的是字符串,可以使用bufio.Scanner從網(wǎng)絡(luò)連接中讀取。默認情況,scanner通過換行符('\n')來分隔讀取到的字節(jié)流數(shù)據(jù)。相反,這里選擇使用bufio.ScanWords以空格作為分隔符,讀出字節(jié)流中的單詞。每當(dāng)碰到一個空格符作為讀取一部分數(shù)據(jù)的邊界直到碰到io.EOF結(jié)束。每次對Scan的調(diào)用都可能導(dǎo)致對網(wǎng)絡(luò)連接Read方法的多次調(diào)用,直到scanner找到它的分隔符或從連接中讀取錯誤為止。它隱藏了從網(wǎng)絡(luò)連接中進行一次或多次讀取時搜索分隔符的復(fù)雜性,并返回結(jié)果消息。

調(diào)用scanner的Text方法會以字符串格式返回分隔出來的數(shù)據(jù)塊,本例中就是一個單詞和相鄰的標(biāo)點符號。代碼通過for循環(huán)連續(xù)的讀取網(wǎng)絡(luò)連接中的字符串,直到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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容