美文网首页
Go: 拆解网络数据包(1)

Go: 拆解网络数据包(1)

作者: Go语言由浅入深 | 来源:发表于2021-10-16 22:20 被阅读0次

    通常从网络连接中读取数据后,都需要对接收的数据进行处理,也就意味着你的代码需要理解接收到的数据内容。由于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
    

    相关文章

      网友评论

          本文标题:Go: 拆解网络数据包(1)

          本文链接:https://www.haomeiwen.com/subject/iwaboltx.html