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

match · · 2395 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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    //包头长度
}

//读取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~


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:match

查看原文:Golang 处理TCP分包、合包——固定包头+包体方式

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2395 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传