Golang standard library binary

Keywords: Go Back-end

binary package implements a simple conversion between a sequence of numbers and bytes.

1,ByteOrder

ByteOrder specifies how to convert a sequence of bytes to a 16, 32, or 64 bit unsigned integer:

type ByteOrder interface {
	Uint16([]byte) uint16
	Uint32([]byte) uint32
	Uint64([]byte) uint64
	PutUint16([]byte, uint16)
	PutUint32([]byte, uint32)
	PutUint64([]byte, uint64)
	String() string
}

ByteOrder is an interface. In binary, there are two structures that implement the interface, namely littleEndian and bigEndian, that is, small end and large end. Large end and small end refer to how data is stored in memory. For example, storing low-order bytes in low address space and high-order bytes in high address space is the small end byte order; On the contrary, storing the low byte in the high address space and the high byte in the low address space is the large end byte order.

For example, the hexadecimal number 0X12345678 is stored in memory in small end and large end byte order as follows:

littleEndian:

Littleendian cannot be created in other packages, but a structure named littleendian has been created in binary, which can be used directly.

var LittleEndian littleEndian

type littleEndian struct{}

func (littleEndian) Uint16(b []byte) uint16 {
	_ = b[1] // Compiler boundary detection tips
	return uint16(b[0]) | uint16(b[1])<<8
}

func (littleEndian) PutUint16(b []byte, v uint16) {
	_ = b[1] // early bounds check to guarantee safety of writes below
	b[0] = byte(v)
	b[1] = byte(v >> 8)
}

func (littleEndian) Uint32(b []byte) uint32 {
	_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}

func (littleEndian) PutUint32(b []byte, v uint32) {
	_ = b[3] // early bounds check to guarantee safety of writes below
	b[0] = byte(v)
	b[1] = byte(v >> 8)
	b[2] = byte(v >> 16)
	b[3] = byte(v >> 24)
}

func (littleEndian) Uint64(b []byte) uint64 {
	_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
	return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
		uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}

func (littleEndian) PutUint64(b []byte, v uint64) {
	_ = b[7] // early bounds check to guarantee safety of writes below
	b[0] = byte(v)
	b[1] = byte(v >> 8)
	b[2] = byte(v >> 16)
	b[3] = byte(v >> 24)
	b[4] = byte(v >> 32)
	b[5] = byte(v >> 40)
	b[6] = byte(v >> 48)
	b[7] = byte(v >> 56)
}

func (littleEndian) String() string { return "LittleEndian" }

func (littleEndian) GoString() string { return "binary.LittleEndian" }

The method defined above is also relatively simple, that is, the conversion between byte sequence and unsigned number. For example, the uint16 method here is a small end byte order, so the low bytes are stored in the low address space. With the increase of the slice index, the address space also increases, so the space where b[1] is located is a high address. Therefore, after moving b[1] to the left and combining b[0] bits, you can get uint16 type data.

bigEndian:

The big end is opposite to the small end:

var BigEndian bigEndian

type bigEndian struct{}

func (bigEndian) Uint16(b []byte) uint16 {
	_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
	return uint16(b[1]) | uint16(b[0])<<8
}

func (bigEndian) PutUint16(b []byte, v uint16) {
	_ = b[1] // early bounds check to guarantee safety of writes below
	b[0] = byte(v >> 8)
	b[1] = byte(v)
}

func (bigEndian) Uint32(b []byte) uint32 {
	_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
	return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
}

func (bigEndian) PutUint32(b []byte, v uint32) {
	_ = b[3] // early bounds check to guarantee safety of writes below
	b[0] = byte(v >> 24)
	b[1] = byte(v >> 16)
	b[2] = byte(v >> 8)
	b[3] = byte(v)
}

func (bigEndian) Uint64(b []byte) uint64 {
	_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
	return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
		uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
}

func (bigEndian) PutUint64(b []byte, v uint64) {
	_ = b[7] // early bounds check to guarantee safety of writes below
	b[0] = byte(v >> 56)
	b[1] = byte(v >> 48)
	b[2] = byte(v >> 40)
	b[3] = byte(v >> 32)
	b[4] = byte(v >> 24)
	b[5] = byte(v >> 16)
	b[6] = byte(v >> 8)
	b[7] = byte(v)
}

func (bigEndian) String() string { return "BigEndian" }

func (bigEndian) GoString() string { return "binary.BigEndian" }

 

2,binary.Read

The Read method reads data from a reader into data. Data must be a pointer or a fixed size value or slice:

This method can also assign the data read in the reader to each field of the structure.

func Read(r io.Reader, order ByteOrder, data interface{}) error {
	// Fast path for basic types and slices.
	if n := intDataSize(data); n != 0 {
		bs := make([]byte, n)
		if _, err := io.ReadFull(r, bs); err != nil {
			return err
		}
		switch data := data.(type) {
		case *bool:
			*data = bs[0] != 0
		case *int8:
			*data = int8(bs[0])
		case *uint8:
			*data = bs[0]
		case *int16:
			*data = int16(order.Uint16(bs))
		case *uint16:
			*data = order.Uint16(bs)
		case *int32:
			*data = int32(order.Uint32(bs))
		case *uint32:
			*data = order.Uint32(bs)
		case *int64:
			*data = int64(order.Uint64(bs))
		case *uint64:
			*data = order.Uint64(bs)
		case *float32:
			*data = math.Float32frombits(order.Uint32(bs))
		case *float64:
			*data = math.Float64frombits(order.Uint64(bs))
		case []bool:
			for i, x := range bs { // Easier to loop over the input for 8-bit values.
				data[i] = x != 0
			}
		case []int8:
			for i, x := range bs {
				data[i] = int8(x)
			}
		case []uint8:
			copy(data, bs)
		case []int16:
			for i := range data {
				data[i] = int16(order.Uint16(bs[2*i:]))
			}
		case []uint16:
			for i := range data {
				data[i] = order.Uint16(bs[2*i:])
			}
		case []int32:
			for i := range data {
				data[i] = int32(order.Uint32(bs[4*i:]))
			}
		case []uint32:
			for i := range data {
				data[i] = order.Uint32(bs[4*i:])
			}
		case []int64:
			for i := range data {
				data[i] = int64(order.Uint64(bs[8*i:]))
			}
		case []uint64:
			for i := range data {
				data[i] = order.Uint64(bs[8*i:])
			}
		case []float32:
			for i := range data {
				data[i] = math.Float32frombits(order.Uint32(bs[4*i:]))
			}
		case []float64:
			for i := range data {
				data[i] = math.Float64frombits(order.Uint64(bs[8*i:]))
			}
		default:
			n = 0 // fast path doesn't apply
		}
		if n != 0 {
			return nil
		}
	}

	// Fallback to reflect-based decoding.
	v := reflect.ValueOf(data)
	size := -1
	switch v.Kind() {
	case reflect.Ptr:
		v = v.Elem()
		size = dataSize(v)
	case reflect.Slice:
		size = dataSize(v)
	}
	if size < 0 {
		return errors.New("binary.Read: invalid type " + reflect.TypeOf(data).String())
	}
	d := &decoder{order: order, buf: make([]byte, size)}
	if _, err := io.ReadFull(r, d.buf); err != nil {
		return err
	}
	d.value(v)
	return nil
}

 

3,binary.Write

The Write method writes the binary data into a Writer. The data must be a fixed value or a slice or a pointer to this kind of data:

func Write(w io.Writer, order ByteOrder, data interface{}) error {
	// Fast path for basic types and slices.
	if n := intDataSize(data); n != 0 {
		bs := make([]byte, n)
		switch v := data.(type) {
		case *bool:
			if *v {
				bs[0] = 1
			} else {
				bs[0] = 0
			}
		case bool:
			if v {
				bs[0] = 1
			} else {
				bs[0] = 0
			}
		case []bool:
			for i, x := range v {
				if x {
					bs[i] = 1
				} else {
					bs[i] = 0
				}
			}
		case *int8:
			bs[0] = byte(*v)
		case int8:
			bs[0] = byte(v)
		case []int8:
			for i, x := range v {
				bs[i] = byte(x)
			}
		case *uint8:
			bs[0] = *v
		case uint8:
			bs[0] = v
		case []uint8:
			bs = v
		case *int16:
			order.PutUint16(bs, uint16(*v))
		case int16:
			order.PutUint16(bs, uint16(v))
		case []int16:
			for i, x := range v {
				order.PutUint16(bs[2*i:], uint16(x))
			}
		case *uint16:
			order.PutUint16(bs, *v)
		case uint16:
			order.PutUint16(bs, v)
		case []uint16:
			for i, x := range v {
				order.PutUint16(bs[2*i:], x)
			}
		case *int32:
			order.PutUint32(bs, uint32(*v))
		case int32:
			order.PutUint32(bs, uint32(v))
		case []int32:
			for i, x := range v {
				order.PutUint32(bs[4*i:], uint32(x))
			}
		case *uint32:
			order.PutUint32(bs, *v)
		case uint32:
			order.PutUint32(bs, v)
		case []uint32:
			for i, x := range v {
				order.PutUint32(bs[4*i:], x)
			}
		case *int64:
			order.PutUint64(bs, uint64(*v))
		case int64:
			order.PutUint64(bs, uint64(v))
		case []int64:
			for i, x := range v {
				order.PutUint64(bs[8*i:], uint64(x))
			}
		case *uint64:
			order.PutUint64(bs, *v)
		case uint64:
			order.PutUint64(bs, v)
		case []uint64:
			for i, x := range v {
				order.PutUint64(bs[8*i:], x)
			}
		case *float32:
			order.PutUint32(bs, math.Float32bits(*v))
		case float32:
			order.PutUint32(bs, math.Float32bits(v))
		case []float32:
			for i, x := range v {
				order.PutUint32(bs[4*i:], math.Float32bits(x))
			}
		case *float64:
			order.PutUint64(bs, math.Float64bits(*v))
		case float64:
			order.PutUint64(bs, math.Float64bits(v))
		case []float64:
			for i, x := range v {
				order.PutUint64(bs[8*i:], math.Float64bits(x))
			}
		}
		_, err := w.Write(bs)
		return err
	}

	// Fallback to reflect-based encoding.
	v := reflect.Indirect(reflect.ValueOf(data))
	size := dataSize(v)
	if size < 0 {
		return errors.New("binary.Write: invalid type " + reflect.TypeOf(data).String())
	}
	buf := make([]byte, size)
	e := &encoder{order: order, buf: buf}
	e.value(v)
	_, err := w.Write(buf)
	return err
}

 

4. binary.Read and binary.Write applications

When we use tcp to transmit data, we often encounter the phenomenon of sticking packets. Therefore, in order to solve the problem of sticking packets, we need to tell each other the size of the packets we send. Generally, TLV type data protocols are used, including type, len and Value. Type and Len are data headers, and these two fields can be fixed to four bytes. When reading data, first read out the type and len, and then read the remaining data according to len:

For example, we use the client to send data to a server:

client:

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
)

// Encode data
func Encode(id uint32, msg []byte) []byte {
	var dataLen uint32 = uint32(len(msg))

	// *Buffer implements Writer
	buffer := bytes.NewBuffer([]byte{})
    // Write id to byte slice
	if err := binary.Write(buffer, binary.LittleEndian, &id); err != nil {
		fmt.Println("Write to buffer error:", err)
	}
	// Write data length to byte slice
	if err := binary.Write(buffer, binary.LittleEndian, &dataLen); err != nil {
		fmt.Println("Write to buffer error:", err)
	}
	
    // Finally, add the data to the back
	msg = append(buffer.Bytes(), msg...)

	return msg
}

func main() {
	dial, err := net.Dial("tcp4", "127.0.0.1:6666")
	if err != nil {
		fmt.Println("Dial tcp error:", err)
	}
	
    // Send hello,world to the server!
	msg := []byte("hello,world!")
	var id uint32 = 1

	data := Encode(id, msg)
	dial.Write(data)

	dial.Close()
}

// Operation results:
Receive Data, Type:1, Len:12, Message:hello,world!
Connection has been closed by client

server:

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"net"
)

// Decode and obtain id and len from byte slice
func Decode(encoded []byte) (id uint32, l uint32) {
	buffer := bytes.NewBuffer(encoded)
	if err := binary.Read(buffer, binary.LittleEndian, &id); err != nil {
		fmt.Println("Read from buffer error:", err)
	}

	if err := binary.Read(buffer, binary.LittleEndian, &l); err != nil {
		fmt.Println("Read from buffer error:", err)
	}

	return id, l
}

const MAX_PACKAGE = 4096

func DealConn(conn net.Conn) {
	defer conn.Close()

	head := make([]byte, 8)
	for {
        // First read the header of 8 bytes, that is, id and dataLen
		_, err := io.ReadFull(conn, head)
		if err != nil {
			if err == io.EOF {
				fmt.Println("Connection has been closed by client")
			} else {
				fmt.Println("Read error:", err)
			}
			return
		}

		id, l := Decode(head)
		if l > MAX_PACKAGE {
			fmt.Println("Received data grater than MAX_PACKAGE")
			return
		}
		
        // Then read the remaining data
		data := make([]byte, l)
		_, err = io.ReadFull(conn, data)
		if err != nil {
			if err == io.EOF {
				fmt.Println("Connection has been closed by client")
			} else {
				fmt.Println("Read error:", err)
			}
			return
		}

        // Print received data
		fmt.Printf("Receive Data, Type:%d, Len:%d, Message:%s\n",
			id, l, string(data))
	}

}

func main() {

	listener, err := net.Listen("tcp", "127.0.0.1:6666")
	if err != nil {
		fmt.Println("Listen tcp error:", err)
		return
	}


	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Accept error:", err)
            break
		}

		// Start a coroutine processing client
		go DealConn(conn)

	}

}
Operation results:
Receive Data, Type:1, Len:12, Message:hello,world!
Connection has been closed by client

Posted by dewed on Sat, 20 Nov 2021 04:43:34 -0800