【转】解决golang开发socket服务时粘包半包bug

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

编程过程中遇到了粘包问题,看到这篇很详尽的就mark下来了,虽然看代码很简单,也没能解决我的粘包问题,但是对于自己了解粘包还是有用的么,是吧。
在使用golang做socket服务时,我想大多数人都会碰见粘包的问题。 以前用python做socket服务时就想写一篇关于tcp粘包的问题,后来因为单纯的tcp服务器开发功能实在烦杂,索性直接用http tornado进行通信了。
下面的资料有些是来自我个人的印象笔记,相关的参考引用链接早就找不到了。

该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。
http://xiaorui.cc/?p=2888

什么是半包 ?

接受方没有接受到完整的包,只接受了一部分。 由于发送方看到内容太大切分数据包进行发送,这样切包能提高传输效率,如果一个包太大,接受方并不能一次接受完。(在长连接和短连接中都会出现)。
注: 半包、粘包都可以用后面的方法解决.

什么是分包?
既然tcp的包产生了粘包,那么需要分开处理吧。 对,这就是分包 ! 分包的前提是客户端和服务端都提前定义一组结构,可以让你准确拆分粘包的结构。

什么时候需要考虑粘包的问题?

1: 类似 http的请求就不用考虑粘包的问题,因为服务端收到报文后, 就将缓冲区数据接收, 然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。

2:如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包

3:如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构: 1)”save it” 2)”delete it “ 这时候很不巧,发送方连续发送这个两个包出去,接收方一次接收可能会是”saveit delete it” 这样接收方就傻了,到底是要干嘛? 不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。 接着我们用伪代码来实现下tcp粘包的场景.
粘包问题就是TCP在传输数据时, 为了提高传输速度和效率, 把发送缓冲区中的数据拼为一个数据包发送到目的地 比如:
发送方:send(s, “abce”);send(s, “decfg”);
接收方:recv(s, buf); //buf = “abcedecfg”;
再废话下,用一段话来描述什么是tcp粘包:
出现粘包现象的原因既可能由发送方造成,也可能由接收方造成。
1 发送端需要等缓冲区满才发送出去,造成粘包
2 接收方没能及时地接收缓冲区的包,造成多个包接收

解决办法:
为了避免粘包现象,可采取以下几种措施。

  1. 对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
    缺点: 第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。
  2. 对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
    缺点: 第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。

最后解决tcp粘包的方法:
客户端会定义一个标示,比如数据的前4位是数据的长度,后面才是数据。那么客户端只需发送 ( 数据长度+数据 ) 的格式数据就可以了,接收方根据包头信息里的数据长度读取buffer.

下面直接说golang socket下解决粘包的实例代码.
客户端:

//客户端发送封包
package main
 
import (
    "fmt"
    "math/rand"
    "net"
    "os"
    "strconv"
    "strings"
    "time"
)
 
func main() {
 
    server := "127.0.0.1:5000"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", server)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
 
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
 
    defer conn.Close()
 
    for i := 0; i < 50; i++ {
        //msg := strconv.Itoa(i)
        msg := RandString(i)
        msgLen := fmt.Sprintf("%03s", strconv.Itoa(len(msg)))
        //fmt.Println(msg, msgLen)
        words := "aaaa" + msgLen + msg
        //words := append([]byte("aaaa"), []byte(msgLen), []byte(msg))
        fmt.Println(len(words), words)
        conn.Write([]byte(words))
    }
}
 
/**
*生成随机字符
**/
func RandString(length int) string {
    rand.Seed(time.Now().UnixNano())
    rs := make([]string, length)
    for start := 0; start < length; start++ {
        t := rand.Intn(3)
        if t == 0 {
            rs = append(rs, strconv.Itoa(rand.Intn(10)))
        } else if t == 1 {
            rs = append(rs, string(rand.Intn(26)+65))
        } else {
            rs = append(rs, string(rand.Intn(26)+97))
        }
    }
    return strings.Join(rs, "")
}

服务端:

package main
 
import (
    "fmt"
    "io"
    "net"
    "os"
    "strconv"
)
 
func main() {
    netListen, err := net.Listen("tcp", ":5000")
    CheckError(err)
 
    defer netListen.Close()
 
    for {
        conn, err := netListen.Accept()
        if err != nil {
            continue
        }
 
        go handleConnection(conn)
    }
}
 
func handleConnection(conn net.Conn) {
    allbuf := make([]byte, 0)
    buffer := make([]byte, 1024)
    for {
        readLen, err := conn.Read(buffer)
        //fmt.Println("readLen: ", readLen, len(allbuf))
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("read error")
            return
        }
 
        if len(allbuf) != 0 {
            allbuf = append(allbuf, buffer...)
        } else {
            allbuf = buffer[:]
        }
        var readP int = 0
        for {
            //fmt.Println("allbuf content:", string(allbuf))
 
            //buffer长度小于7
            if readLen-readP < 7 {
                allbuf = buffer[readP:]
                break
            }
 
            msgLen, _ := strconv.Atoi(string(allbuf[readP+4 : readP+7]))
            logLen := 7 + msgLen
            //fmt.Println(readP, readP+logLen)
            //buffer剩余长度>将处理的数据长度
            if len(allbuf[readP:]) >= logLen {
                //fmt.Println(string(allbuf[4:7]))
                fmt.Println(string(allbuf[readP : readP+logLen]))
                readP += logLen
                //fmt.Println(readP, readLen)
                if readP == readLen {
                    allbuf = nil
                    break
                }
            } else {
                allbuf = buffer[readP:]
                break
            }
        }
    }
}
 
func CheckError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

代码测试可以直接用


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

本文来自:简书

感谢作者:大雁儿

查看原文:【转】解决golang开发socket服务时粘包半包bug

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

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