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

在使用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下解决粘包的实例代码.

客户端:

#xiaorui.cc

//客户端发送封包
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)
    }
}

一些经典的问答:


在流传输中出现,UDP不会出现粘包,因为它有消息边界.

如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题. 

END.


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc

发表评论

邮箱地址不会被公开。 必填项已用*标注