31

Golang 处理TCP分包、合包——固定包头+包体方式

 4 years ago
source link: https://studygolang.com/articles/25278
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

TCP分包、合包的场景

TCP通信是流式的,在发送一个大数据包时,可能会被拆分成多个数据包进行发送,同时,多次发送数据包,也可能会被底层合并成一个数据包进行发送。

  1. 分包:接收一个数据包,需要对数据包进行拆分;
  2. 合包:接收多个数据包,需要对数据包进行合并;

因此,TCP通信时需要设定通信协议来正确处理收到的数据,如我们常见的HTTP、FTP协议等。

固定包头+包体方式

在该协议下,一个数据包总是有一个定长的包头加一个包体构成,其中包头中会有一个字段说明包体或者整个包的长度。服务器收到数据后就可以按序解析出包头 > 包体/包长度 > 包体。

程序默认使用包头最后一个字节描述包体长度,下面来看看具体的代码实现:

启动服务程序,读取并打印接收的TCP数据

package main

import (
    "fmt"
    "net"
)

const MaxBufferSize = 128    //单次读取最大长度
const HeaderLen = 5    //包头长度

func main() {
    tcpAddr := "192.168.0.111:15000"
    listener, err := net.Listen("tcp4", tcpAddr)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer listener.Close()
    //启动客户端程序
    go send()
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }
        go handle(conn)
    }
}

//处理从通道接收的数据
func accept(acceptData chan string) {
    for {
        value, isOk := <- acceptData
        if !isOk {
            break
        }
        parse([]byte(value))
    }
}

func parse(data []byte) {
    //将四字节的包头转为string
    head := string(data[:4])
    //将一字节的包体长度转为十进制整形
    size := int(data[4])
    //将包体转为string
    body := string(data[5:])
    fmt.Println(fmt.Sprintf("接收的数据:包头为:%s,包体大小%d字节,值为:%s", head, size, body))
}

//处理tcp请求
func handle(conn net.Conn) {
    defer conn.Close()
    //创建接收数据的通道
    acceptData := make(chan string, 10)
    defer close(acceptData)
    go accept(acceptData)

    reader := Reader{
        Conn:  conn,
        Buff:  make([]byte, MaxBufferSize),
        Start: 0,
        End:   0,
        BuffLen: MaxBufferSize,
        HeaderLen: HeaderLen,
    }
    err := reader.Read(acceptData)
    if err != nil {
        fmt.Println("read data error:", err)
    }
}

读取单条完整数据并通过通道传递数据

package main

import (
    "fmt"
    "net"
)

type Reader struct {
    Conn net.Conn
    Buff []byte
    Start int    //数据读取开始位置
    End int    //数据读取结束位置
    BuffLen int    //数据接收缓冲区大小
    HeaderLen int    //包头长度
    BodySizeOffset int    //标识包体长度所在包头位置
}

//读取tcp数据流
func (reader *Reader)Read(acceptData chan string) error {
    for {
        reader.Move()
        if reader.End == reader.BuffLen {
            //缓冲区的宽度容纳不了一条消息的长度
            return fmt.Errorf("one message is too large:%v", reader)
        }
        len, err := reader.Conn.Read(reader.Buff[reader.End:])
        if err != nil {
            return err
        }
        reader.End += len
        reader.GetData(acceptData)
    }
}

//前移上一次未处理完的数据
func (reader *Reader)Move() {
    if reader.Start == 0 {
        return
    }
    copy(reader.Buff, reader.Buff[reader.Start:reader.End])
    reader.End -= reader.Start
    reader.Start = 0
}

//读取buff中的单条数据
func (reader *Reader)GetData(acceptData chan string) error {
    if reader.End - reader.Start < reader.HeaderLen {
        //包头的长度不够,继续接收
        return nil
    }
    //读取包头数据
    headerData := reader.Buff[reader.Start:(reader.Start + reader.HeaderLen)]
    if reader.End - reader.Start - reader.HeaderLen < int(headerData[reader.HeaderLen - 1]) {
        //包体的长度不够,继续接收
        return nil
    }
    //读取包体数据
    bodyData := reader.Buff[(reader.Start + reader.HeaderLen):reader.Start + reader.HeaderLen + int(headerData[reader.HeaderLen - 1])]
    //把完整的包用通道传递出去
    acceptData <- string(append(headerData, bodyData...))
    //每读完一次数据 start 后移
    reader.Start += reader.HeaderLen + int(headerData[reader.HeaderLen - 1])
    return reader.GetData(acceptData)
}

客户端发消息

package main

import (
    "fmt"
    "net"
    "time"
)

func send() {
    addr := "192.168.0.111:15000"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", addr)
    if err != nil {
        fmt.Println(err)
        return
    }
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        fmt.Println(err)
        return
    }
    head := []byte("head")
    bodies := []string{
        "I am not afraid of tomorrow for I have seen yesterday and I love today.",
        "If you want to understand today, you have to search yesterday.",
        "You never know what you can do till you try.",
        "A good name keeps its luster in the dark.",
    }
    for {
        var messages []byte
        for _, b := range bodies{
            body := []byte(b)
            oneMessage := append(head, byte(len(body)))
            oneMessage = append(oneMessage, body...)
            messages = append(messages, oneMessage...)
        }
        _, err := conn.Write(messages)
        if err != nil {
            fmt.Println(err)
            return
        }
        time.Sleep(5*time.Second)
    }
}

完整项目放在 Github 上,欢迎给Star~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK