程序员社区

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

前面一篇文章介绍Go使用分隔符的方法来拆解网络包时提到,还有另一种方式即根据协议来拆解包。客户端和服务端一般都会提前定义好收到的网络数据的字节排列方式。类似我们在学习计算机网络中的其他协议,包含协议头和实际发送数据两大部分。协议头里面定义根据需求会定义相应的协议字段。每个字段都会根据所占的字节数来读取。

本文将介绍如何使用Go来自定义简单的应用层协议。我们称该协议为TLV即(type-length-value)指的是收到的网络数据包,包含数据类型、数据长度和具体数据内容。TLV实现方式使用固定字节数来表示数据类型和数据长度,而发送的具体数据内容长度是不固定的。这里我们的实现使用5个字节的包头:1个字节表示数据类型和4个字节表示发送的数据长度。TLV实现方式允许您将数据作为字节序列发送到远程节点,并从远程节点上根据字节序列组合出相同的数据类型。如下代码所示:

package networking

import (
    "bytes"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "net"
    "reflect"
    "testing"
)

const (
    //使用一个字节无符号整数定义两种要发送的数据类型
    BinaryType uint8 = iota + 1  //1代表发送的数据是二进制类型数据
    StringType                   //2代表发送的是字符串

    MaxPayloadSize uint32 = 10 << 20 //10MB
)

var ErrMaxPayloadSize = errors.New("maximum payload size exceeded")

//定义一个解析数据包的接口
type Payload interface {
    fmt.Stringer
    io.ReaderFrom   //拆解网络数据包方法
    io.WriterTo     //封装网络数据包方法
    Bytes() []byte
}

上面代码创建常量定义两种数据包类型:BinaryType和StringType。如果你理解了每种类型的实现,你就可以根据自己的需求实现自己的协议。为了安全起见,你需要创建一个最大发送数据字节数,我们在后面会讨论的。

上面代码还定义了一个Payload接口,包含必须实现的方法。每种类型都要实现四个方法:Bytes、String、ReadFrom和WriteTo。其中io.ReadFrom和io.WriteTo方法分别从网络输入接口读取数据和写入数据到网络输出接口中。

接下来就可以定义TLV的数据类型了,如下所示:

//定义二进制字节数据类型并实现Payload接口
type Binary []byte

func (m Binary)Bytes() []byte { return m}
func (m Binary)String() string { return string(m)}

//封装二进制字节数据并发送到远程节点
func (m Binary)WriteTo(w io.Writer) (int64, error) {
    err := binary.Write(w, binary.BigEndian, BinaryType) //按高位顺序写入类型占1byte
    if err != nil {
        return 0, err
    }
    var n int64 = 1
    err = binary.Write(w, binary.BigEndian, uint32(len(m))) //负载字节数
    if err != nil{
        return n, err
    }
    n += 4
    o, err := w.Write(m)
    return n + int64(o), err
}

Binary类型是一个字节切片,因此Bytes方法直接返回自己。String方法将字节切片转换成字符串返回。WriteTo方法接收一个io.Writer参数以及返回写入数据的字节数和一个error接口。WriteTo方法首先写入1字节数据作为发送数据的类型。然后写入4字节表示发送的二进制切片长度。最后写入Binary数据,也就是要发送的数据内容。

//拆解二进制字节切片类型数据包
func (m *Binary)ReadFrom(r io.Reader) (int64, error) {
    var typ uint8
    err := binary.Read(r, binary.BigEndian, &typ) //读取高位1字节
    if err != nil{
        return 0, err
    }
    var n int64 = 1
    if typ != BinaryType {
        return n, errors.New("invalid Binary")
    }
    var size uint32
    err = binary.Read(r, binary.BigEndian, &size) //读取负载字节数
    if err != nil {
        return n, err
    }
    n += 4
    if size > MaxPayloadSize {
        return n, ErrMaxPayloadSize
    }
    *m = make([]byte, size)
    o, err := r.Read(*m) //负载
    return n + int64(o), err
}

ReadFrom方法从reader网络输入接口中读取1字节到typ变量中。接着验证是否为BinaryType类型才继续。然后读取后面4个字节数据到size变量,代码要接收到Binary字节切片的长度。最后,填充Binary字节切片。

注意检查最大负载大小。因为用4字节整数来表示负载大小最大值为4,294,967,295,表示发送的最大数据不能超过4GB。对于如此大的有效负载,恶意参与者很容易执行Dos攻击,从而耗尽计算机上所有可用的随机访问内存(RAM)。保持合理的最大有效负载可以提升内存耗尽攻击的难度。

下面的代码介绍了String类型,和Binary类型一样实现Payload接口。

//定义字符串类型并实现Payload接口
type String string

func (s String) String() string {return string(s)}

func (s String) Bytes() []byte {return []byte(s)}

//封装字符串类型的数据包并发送到远程节点
func (s String) WriteTo(w io.Writer) (n int64, err error) {
    err = binary.Write(w, binary.BigEndian, StringType) //高位写入1字节类型
    if err != nil {
        return 0, err
    }
    n = 1
    err = binary.Write(w, binary.BigEndian, uint32(len(s))) //负载字节数
    if err != nil {
        return n, err
    }
    n += 4
    o, err := w.Write([]byte(s))
    return n + int64(o), err
}

String实现Bytes方法直接将字符串转为字节切片即可。String方法将String类型转为它的基础类型。WriteTo方法和Binary的writeTo方法类似,除了写入第一个字节是StringType和将字符串转为字节切片再写入网络输入接口writer中。

下面的代码完成了String类型的Payload的实现。

func (s *String) ReadFrom(r io.Reader) (n int64, err error) {
    var typ uint8
    err = binary.Read(r, binary.BigEndian, &typ) //高位顺序读取1字节类型
    if err != nil {
        return 0, err
    }
    n = 1
    if typ != StringType {
        return n, errors.New("invalid String")
    }
    var size uint32
    err = binary.Read(r, binary.BigEndian, &size)
    if err != nil {
        return n, err
    }
    n += 4
    buf := make([]byte, size)
    o, err := r.Read(buf)
    *s = String(buf[:o])
    return n + int64(o), err
}

这里ReadFrom和Binary的一样,除了两个地方。第一是先对比typ变量类型是StringType再继续。第二,将数据转为String类型返回。

剩下要实现的就是从网络连接读取任意数据并使用我们实现的两种类型来解析数据包。

//拆解任意类型的数据包
func decode(r io.Reader) (Payload, error) {
    var typ uint8
    err := binary.Read(r, binary.BigEndian, &typ)
    if err != nil {
        return nil, err
    }
    var payload Payload
    switch typ {
    case BinaryType:
        payload = new(Binary)
    case StringType:
        payload = new(String)
    default:
        return nil, errors.New("unknown type")
    }
    _, err = payload.ReadFrom(
        io.MultiReader(bytes.NewReader([]byte{typ}), r))
    if err != nil {
        return nil, err
    }
    return payload, nil
}

decode函数接收一个io.Reader参数并返回一个Payload接口实例和一个error。如果decode不能对读取到的数据解码为Bianry或StringType类型,将返回error和nil。

你必须从reader中读取1个字节才能判断是哪种数据类型,并创建payload变量来存储解码数据。如果从reader中读取的类型是已经定义的其中一种,然后定义对应的类型并赋值给payload变量。

知道数据的类型以后,就可以根据特定的类型来对网络中读取的数据进行解码。但是你不能简单的将reader传给ReadFrom方法。前面已经从reader中将第一个字节的类型数据读取出来了,而ReadFrom方法也需要读取第一个字节数据来判断数据类型。幸亏io包有一个函数可以使用:MultiReader。可以使用它来将已经读取的数据重写到Reader里面去。这样ReadFrom就可以继续按顺序读取数据并解析。

尽管io.MultiReader可以实现字节切片注入到reader中去,但并不是最好的方法。正确的解决方法是在ReadFrom中不用读取第一个字节。decode函数已经知道接收的数据类型了,可以直接调用对应的ReadFrom方法,解析剩下的数据即可。读者可以自行实现。

下面我们来测试下decode函数:

func TestPayloads(t *testing.T)  {
    //服务端
    b1 := Binary("Clear is better than clever.")
    b2 := Binary("Don't panic")
    s1 := String("Errors are values.")
    payloads := []Payload{&b1, &s1, &b2}
    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()
        for _, p := range payloads {
            _, err = p.WriteTo(conn)
            if err != nil {
                t.Error(err)
                break
            }
        }
    }()

测试代码先创建要发送的数据类型。这里我们创建了两个Binary类型和一个String类型的数据。然后创建一个Payload接口切片,并将创建的类型的地址添加到切片中。然后创建一个listener将接收网络连接将切片中的每种类型数据写进网络输入接口。

    //客户端
    conn, err := net.Dial("tcp", listener.Addr().String())
    if err != nil {
        t.Fatal(err)
    }
    defer conn.Close()
    for i := 0; i < len(payloads); i++{
        actual, err := decode(conn)
        if err != nil{
            t.Fatal(err)
        }
        if expected := payloads[i]; !reflect.DeepEqual(expected, actual) {
            t.Errorf("value mismatch: %v != %v", expected, actual)
            continue
        }
        t.Logf("[%T] %[1]q", actual)
    }
}

测试中你知道总共发送了多少中类型的数据,因此初始化一个连接到listener,然后对接收到的数据进行解码。最后比较你解码的类型和服务器发送的类型。如果发送的数据不一致测试就失败。

下面测试下发送最大数据负载情况:

func TestMaxPayloadSize(t *testing.T)  {
    buf := new(bytes.Buffer)
    err := buf.WriteByte(BinaryType)
    if err != nil {
        t.Fatal(err)
    }
    err = binary.Write(buf, binary.BigEndian, uint32(1 << 30)) //1GB
    if err != nil {
        t.Fatal(err)
    }
    var b Binary
    _, err = b.ReadFrom(buf)
    if err != ErrMaxPayloadSize {
        t.Fatalf("expected ErrMaxPayloadSize; actual: %v", err)
    }
}

该测试创建了一个bytes.Buffer,包含BinaryType类型和4字节无符号整数表示1GB的数据。如果发送的数据是1GB,已经超过我们定义的最大10MB限制了,虽然4字节可以最大表示4GB的数据,但是出于安全等原因一般不会发送这么大的数据包的。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » Go: 拆解网络数据包(2)

一个分享Java & Python知识的社区