通常從網(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