当前位置:首页 > golang 与 cpp 之间的网络通信,了解网络字节码的编解

golang 与 cpp 之间的网络通信,了解网络字节码的编解

发布于 2018-06-29 阅读 558 次 Golang

字节序的了解

在几乎所有的机器上,多字节对象都被存储为连续的字节序列。例如在C语言中,一个类型为int的变量x地址为0x100,那么其对应地址表达式&x的值为0x100。且x的四个字

节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置。[1]

而存储地址内的排列则有两个通用规则。一个多位的整数将按照其存储地址的最低或最高字节排列。如果最低有效位在最高有效位的前面,则称小端序;
反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。

例如假设上述变量x类型为int,位于地址0x100处,它的十六进制为0x01234567,地址范围为0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..。而小端法将是:0x100: 67, 0x101: 45,..

序的大小端

大端序(Big-Endian)或称大尾序:高位字节在前,低位字节在后

小端序(Big-Endian)或称小尾序:低位字节在前,高位字节在后

下面将以小端序来理解网络字节码的编解

基本协议的定义

通过网络字节流的方式进行通的信基本格式:

header + body
//包头连接上包体,然后组成的字节流

header:为整个字节流的长度值
body: 是具体协议的内容(比如:请求的方法以及参数)

简单的画个图示:

所以整个字节流是一节一节的拼接起来的一段流体

举个栗子

构造一个简单的登陆请求,请求包参数如下:

Request struct {
    Header   uint32 //
    Username string //w2le.com
    Password string //123456
}
  • Password:规定为 hash md5,长度为 32
  • Username:定义用户名长度为20 字符长度

那么整个请求包的长度:

len(Username)+ len(Password) + len(Header)= 20 + 32 + 4 = 56

请求流处理,字节序的编码

import (
    "crypto/md5"
    "encoding/binary"
    "encoding/hex"
    "fmt"
)

func main() {

    //定义请求包的字节 buf
    //写入header
    buf := make([]byte, 4)
    //这里采用的小端序 LittleEndian
    binary.LittleEndian.PutUint32(buf, uint32(56))

    //定义Username, Password 占位buf 
    ubuf, pbuf := make([]byte, 20), make([]byte, 32)
    copy(ubuf, "w2le.com")
    buf = append(buf, ubuf...)

    copy(pbuf, Md5("123456"))
    buf = append(buf, pbuf...)

    fmt.Println(buf)
    //输出:[56 0 0 0 119 50 108 101 46 99 111 109 0 0 0 0 0 0 0 0 0 0 0 0 101 49 48 97 100 99 51 57 52 57 98 97 53 57 97 98 98 101 53 54 101 48 53 55 102 50 48 102 56 56 51 101]
}

//Md5 hash md5
func Md5(s string) string {
    hasher := md5.New()
    hasher.Write([]byte(s))

    return hex.EncodeToString(hasher.Sum(nil))
}

发送请求

func Send(buf []byte) {

    conn, err := net.Dial("tcp", "127.0.0.1")

    if err != nil {
        //handle error
    }

    _, err = conn.Write(buf)

    if err != nil {
        //handle error
    }
}

响应流的读取,字节序的解码

func handeResponse(conn io.Reader) {

    rd := bufio.NewReader(conn)

    //读取header头
    //获取这个请求的整个包的长度
    h, err := rd.Peek(4)

    tmp := bytes.NewBuffer(h)
    var hlen uint32
    binary.Read(tmp, binary.LittleEndian, &hlen)

    // 通过包头读取整个 数据流
    data = make([]byte, hlen)
    _, err = rd.Read(data)

    fmt.Println(data)
    //输出: [56 0 0 0 119 50 108 101 46 99 111 109 0 0 0 0 0 0 0 0 0 0 0 0 101 49 48 97 100 99 51 57 52 57 98 97 53 57 97 98 98 101 53 54 101 48 53 55 102 50 48 102 56 56 51 101]
}

解析:解析的时候,需要从头至尾依次读取解析

func scanBuf(data []byte) {
    r := &Request{}

    r.Header   = hlen
    r.Username = string(data[4:24])
    r.Password = string(data[24:])

    fmt.Println(r)
    //输出:&{56 w2le.com e10adc3949ba59abbe56e057f20f883e}
}

对于非字符串类型需要通过

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

binary.Read(r io.Reader, order ByteOrder, data interface{})

来解析,例如:

tmp := bytes.NewBuffer(h)
var hlen uint32
binary.Read(tmp, binary.LittleEndian, &hlen)

最后一些注意

  • 需要定义好每一段参数的长度。如果参数长度不够,需要用 0x00 补位。比较特殊的是字符串参数
    可以先通过 make([]byte, len) 定义好变量长度, 然后通过 copy复制
  • 解析的时候,需要从左自由依次读取解析。一旦解析错误会导致后面解析错乱