GoMQTT服务器 tcp处理粘包问题
第一章 tcp处理粘包问题
写的不好,请指出,目前还在学习当中
文章目录
前言
提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。
提示:以下是本篇文章正文内容,下面案例可供参考
一、问题显示
1.服务器代码
// goTcp/server/main.go
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buf [1024]byte
for {
n, err := reader.Read(buf[:])
if err == io.EOF {
break
}
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client发来的数据:", recvStr)
}
}
func main() {
// 监听端口
listen, err := net.Listen("tcp", "127.0.0.1:8088")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
// 等待客户端连接
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 协程事件处理
}
}
2.客户端代码
// goTcp/client/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8088")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. HelloWord?`
conn.Write([]byte(msg))
}
}
先启动服务端,再启动客户端
3.问题
收到client发来的数据: Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?
收到client发来的数据: Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?
收到client发来的数据: Hello, Hello. HelloWord?Hello, Hello. HelloWord?
收到client发来的数据: Hello, Hello. HelloWord?Hello, Hello. HelloWord?Hello, Hello. HelloWord?
收到client发来的数据: Hello, Hello. HelloWord?Hello, Hello. HelloWord?
客户端分20次发送的数据,在服务端并没有成功的输出20次,而是多条数据“粘”到了一起。
二、分析
1.为什么会出现粘包
主要原因就是tcp数据传输模式是流模式,在保持长连接的时候可以进行多次的收和发。
“粘包”可发生在发送端也可发生在接收端:主要有以下情况:
- 由Nagle算法造成的发送端的粘包
Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
- 接收端接收不及时造成的接收端粘包
TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层不能及时的把TCP的数据取出,就会造成TCP缓冲区中存放了几段数据。
2.解决办法
出现”粘包”的关键在于接收方不确定将要传输来的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度,后面数据域长度的内容为实际传输的消息。
| 包头| 包体|
编解码
因此,可以写一个编解码器,参考了Java netty
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
/**
消息编码
*/
func Encode(message *string)([]byte,error ) {
//读取消息长度,转化为int32类型(占4字节)
var lengrh = int32(len(*message))
var pkg = new(bytes.Buffer)
//写入消息头
err := binary.Write(pkg,binary.LittleEndian,lengrh)
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) {
// 读取消息长度
// 读取前4个字节的数据
lengthByte, _ := reader.Peek(4)
lengthBuffer := bytes.NewBuffer(lengthByte)
var length int32
// 求数据长度
err := binary.Read(lengthBuffer,binary.LittleEndian,&length)
if err != nil{
return "", err
}
// Buffered返回缓冲区中现有可读的字节数,不够就不去取数据
// 下面加4,是因为长度字段占4字节(int32)
if int32(reader.Buffered()) < length + 4{
return "",err
}
//读取真正的消息数据
pack := make([]byte, int(4+length))
_, err = reader.Read(pack)
if err != nil{
return "", err
}
// 返回数据就不要长度字段了
return string(pack[4:]),nil
}
修改服务端代码
package main
import (
"bufio"
"fmt"
"io"
"net"
"study_one_day/gotcp/proto"
)
func main() {
listen, err := net.Listen("tcp","127.0.0.1:8088")
if err != nil{
fmt.Println("listen failed, err:",err)
}
fmt.Println("begin listen...")
for{
conn, err := listen.Accept()
if err != nil{
fmt.Println("accept failed, err:",err)
}
//go process(conn)
go processPlus(conn)
}
}
func processPlus(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for{
msg, err := proto.Decode(reader)
if err == io.EOF{
fmt.Println("client close connect")
return
}
if err != nil{
fmt.Println("decode msg failed, err : ",err)
return
}
fmt.Println("收到client发来的数据: ",msg)
}
}
修改客户端代码
package main
import (
"bufio"
"fmt"
"net"
"strings"
"study_one_day/gotcp/proto"
)
func main() {
conn, err := net.Dial("tcp","127.0.0.1:8088")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := "Hello Hello HelloWorld ?"
data, err := proto.Encode(&msg)
if err != nil{
fmt.Println("encode msg failed, err : ",err)
return
}
conn.Write(data)
}
}
服务端接收数据效果
begin listen...
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
收到client发来的数据: Hello Hello HelloWorld ?
client close connect
可见,效果不错