编程过程中遇到了粘包问题,看到这篇很详尽的就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 接收方没能及时地接收缓冲区的包,造成多个包接收
解决办法:
为了避免粘包现象,可采取以下几种措施。
- 对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
缺点: 第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。 - 对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
缺点: 第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。
最后解决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)
}
}
代码测试可以直接用