大家好,我是你的好朋友思创斯。今天说一说go tcp粘包_tcp粘包处理,希望您对编程的造诣更进一步.
- tcp粘包:
- tcp是流式协议:发送包的时候一次可能没有发完可能给了下一次 nagle算法导致客户端发送的粘包,本意是为了改善客户端网络
- nagle算法
该算法要求一个tcp连接上最多只能有一个未被确认的未完成的小分组,在该分组ack到达之前不能发送其他的小分组,tcp需要收集这些少量的分组,并在ack到来时以一个分组的方式发送出去;其中小分组的定义是小于mss的任何分组; 该算法的优越之处在于它是自适应的,确认到达的越快,数据也就发哦送的越快;而在希望减少微小分组数目的低速广域网上,则会发送更少的分组;
- nagle算法是以他的发明人john nagle的名字命名的,它用于自动连接许多的小缓冲器消息;这一过程(称为nagling)通过减少必须发送包的个数来增加网络软件系统的效率。
if there is new data to send #有数据要发送
# 发送窗口缓冲区和队列数据 >=mss,队列数据(available data)为原有的队列数据加上新到来的数据
# 也就是说缓冲区数据超过mss大小,nagle算法尽可能发送足够大的数据包
if the window size >= mss and available data is >= mss
send complete mss segment now # 立即发送
else
if there is unconfirmed data still in the pipe # 前一次发送的包没有收到ack
# 将该包数据放入队列中,直到收到一个ack再发送缓冲区数据
enqueue data in the buffer until an acknowledge is received
else
send data immediately # 立即发送
end if
end if
end if
是否还在为ide开发工具频繁失效而烦恼,来吧关注以下公众号获取最新激活方式。亲测可用!
【正版授权,激活自己账号】:
【官方授权 正版激活】:
https://baike.baidu.com/item/nagle算法
服务端代码
package main
import (
"fmt"
"net"
)
func main() {
// 本地端口启动服务
listener, err := net.listen("tcp", "localhost:20000")
if err != nil {
fmt.println("服务器启动失败....", err)
return
}
fmt.println("监听成功...")
// 等待连接
for {
conn, err := listener.accept()
if err != nil {
fmt.println("连接建立失败...", err)
break
}
fmt.println("连接成功...")
go process(conn)
}
// 通信
}
func process(conn net.conn) {
var temp [128]byte
for {
n, err := conn.read(temp[:])
if err != nil {
fmt.println("服务端读取数据失败....")
return
}
fmt.println("读取数据成功:")
fmt.println(string(temp[:n]))
}
}
客户端代码
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.println("连接失败,err:", err)
return
}
defer conn.close()
msg := "hello socket"
for i := 0; i < 20; i {
conn.write([]byte(msg))
}
}
粘包现象
- 可以看到每次收数据会收到多个“hello socket” 而不是单独的一个,也就是说客户端把多个小包合并一起发了
基础知识
大端小端
- 对于 0x123456
- 要存入内中之中存在两种情况:
或者
高位写在右边,则在读取的时候应该从右往左读,反之亦然
高位写在内存低地址:大端
高位写在内存高地址:小端
解决思路
- 出现“粘包”问题的关键在于接收方不知道你发送的数据包的实际大小,也就是说我们在发送数据的时候可以指定数据包中的一个位置来存放当前数据包的实际大小。
- 封包:实际上就是给一段数据加上包头,这样一来数据包就分为包头和包体两个部分(过滤非法包的时候会加上包尾部分)。包头的长度是固定的,并且存储了包体的长度,更具包头的长度固定以及其中包含的长度 变量就能正确的拆分出一个完整的数据包。
- 我们可以自定义一个小协议,在数据包的前4个字节存储数据的长度
协议代码
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
func encode(message string) ([]byte, error) {
// 读取消息的长度转换成int32类型(4字节)
var length = int32(len(message))
var pkg = new(bytes.buffer)
// 写入消息头
err := binary.write(pkg, binary.littleendian, length)
if err != nil {
return nil, err
}
// 写入包体
err = binary.write(pkg, binary.littleendian, []byte(message))
if err != nil {
return nil, err
}
return pkg.bytes(), nil
}
// 解码
func decode(reader *bufio.reader) (string, error) {
// 读消息长度
lengthbyte, _ := reader.peek(4)
lengthbuff := bytes.newbuffer(lengthbyte)
var length int32
err := binary.read(lengthbuff, binary.littleendian, &length)
if err != nil {
return "", err
}
// buffer返回缓冲中现有的可读的字节数
if int32(reader.buffered()) < length4 {
return "", err
}
// 读取真正的数据
pack := make([]byte, int(4length))
_, err = reader.read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
服务端代码
package main
import (
"20_tcp/03_粘包/proto"
"bufio"
"fmt"
"io"
"net"
)
func main() {
// 本地端口启动服务
listener, err := net.listen("tcp", "localhost:20000")
if err != nil {
fmt.println("服务器启动失败....", err)
return
}
fmt.println("监听成功...")
// 等待连接
for {
conn, err := listener.accept()
if err != nil {
fmt.println("连接建立失败...", err)
break
}
fmt.println("连接成功...")
go process(conn)
}
// 通信
}
func process(conn net.conn) {
defer conn.close()
reader := bufio.newreader(conn)
for {
msg, err := proto.decode(reader)
fmt.println("收到消息:", msg)
if err == io.eof {
return
}
if err != nil {
fmt.println("decode失败,err:", err)
}
}
}
客户端代码
package main
import (
"20_tcp/03_粘包/proto"
"fmt"
"net"
)
func main() {
conn, err := net.dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.println("连接失败,err:", err)
return
}
defer conn.close()
msg := "hello socket"
for i := 0; i < 20; i {
// 调用协议编码协议
b, err := proto.encode(msg)
if err != nil {
fmt.println("encode失败,err:", err)
}
conn.write(b)
fmt.println("发送成功...,msg:", b)
}
}
结果
文章由思创斯整理,转载请注明出处:https://ispacesoft.com/121273.html