go语言网络编程解决粘包

go语言网络编程解决粘包

前言:

1、出现粘包的原因:

主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。

“粘包”可发生在发送端也可发生在接收端:

  1. 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
  2. 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

2、解决:

本质上是由于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作

封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

proto.go协议,拟定4个字节为包头,里面存储的是发送的数据的长度。
package proto

import (
	"bufio"
	"bytes"
	"encoding/binary"
)

// Encode 将消息编码
func Encode(message string) ([]byte, error) {
	// 读取消息的长度,转换成int32类型(占4个字节)
	var length = int32(len(message))
	var pkg = new(bytes.Buffer) // pkg为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
}

// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
	// 读取消息的长度
	lengthByte, _ := reader.Peek(4) // 返回缓存中的前4个字节的数据
	lengthBuff := bytes.NewBuffer(lengthByte)
	var length int32
	err := binary.Read(lengthBuff, binary.LittleEndian, &length)
	if err != nil {
		return "", err
	}
	// Buffered返回缓存中现有的可读取的字节数(数据长度)。
	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
}
客户端
func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("dial failed, err", err)
		return
	}
	defer conn.Close()
	for i := 0; i < 20; i++ {
		msg := `Hello, Hello. How are you?`
		data, err := proto.Encode(msg)
		if err != nil {
			fmt.Println("encode msg failed, err:", err)
			return
		}
		conn.Write(data)
	}
}
服务端
// socket_stick/server2/main.go

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		msg, err := proto.Decode(reader)
		if err == io.EOF {
			return
		}
		if err != nil {
			fmt.Println("decode msg failed, err:", err)
			return
		}
		fmt.Println("收到client发来的数据:", msg)
	}
}

func main() {

	listen, err := net.Listen("tcp", "127.0.0.1:30000")
	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)
	}
}

补充:

1、bufio包的用法

  • bufio 包实现了带缓存的 I/O 操作
  • 它封装一个 io.Reader 或 io.Writer 对象
  • 使其具有缓存和一些文本读写功能
1)NewReader方法
// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
	return NewReaderSize(rd, defaultBufSize)
}

NewReaderSize将rd封装成一个拥有size大小缓存的bufio.Reader对象,

如果 rd 的基类型就是 bufio.Reader 类型,而且拥有足够的缓存
// 则直接将 rd 转换为基类型并返回

func NewReaderSize(rd io.Reader, size int) *Reader
NewReader 相当于 NewReaderSize(rd, 4096)
func NewReader(rd io.Reader) *Reader
2)Peek()
func (b *Reader) Peek(n int) ([]byte, error) {}

Peek 返回缓存的一个切片,该切片引用缓存中前 n 字节数据
// 该操作不会将数据读出,只是引用
// 引用的数据在下一次读取操作之前是有效的
// 如果引用的数据长度小于 n,则返回一个错误信息
// 如果 n 大于缓存的总大小,则返回 ErrBufferFull
// 通过 Peek 的返回值,可以修改缓存中的数据
// 但是不能修改底层 io.Reader 中的数据

func main() {
s := strings.NewReader("ABCDEFG")
br := bufio.NewReader(s)

b, _ := br.Peek(5)
fmt.Printf("%s\n", b)
// ABCDE

b[0] = 'a'
b, _ = br.Peek(5)
fmt.Printf("%s\n", b)
// aBCDE
}
3)Read()
func (b *Reader) Read(p []byte) (n int, err error) {}
// Read 从 b 中读出数据到 p 中,返回读出的字节数
// 如果 p 的大小 >= 缓存的总大小,而且缓存不为空
// 则只能读出缓存中的数据,不会从底层 io.Reader 中提取数据
// 如果 p 的大小 >= 缓存的总大小,而且缓存为空
// 则直接从底层 io.Reader 向 p 中读出数据,不经过缓存
// 只有当 b 中无可读数据时,才返回 (0, io.EOF)
func main() {
s := strings.NewReader("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
br := bufio.NewReader(s)
b := make([]byte, 20)

n, err := br.Read(b)
fmt.Printf("%-20s %-2v %v\n", b[:n], n, err)
// ABCDEFGHIJKLMNOPQRST 20 <nil>

n, err = br.Read(b)
fmt.Printf("%-20s %-2v %v\n", b[:n], n, err)
// UVWXYZ1234567890 16 <nil>

n, err = br.Read(b)
fmt.Printf("%-20s %-2v %v\n", b[:n], n, err)
// 0 EOF
}

4)其他方法去参考https://studygolang.com/articles/4367

2、bytes包的用法

bytes 包实现了用于操作 []byte 的函数,类似于 strings 包中的函数

// Buffer 实现了带缓存的输入输出操作
// 缓存的容量会根据需要自动扩展
// 如果缓存太大,无法继续扩展,则会引发 panic(ErrTooLarge)
type Buffer struct {
// 私有字段
}

1) 通过 []byte 或 string 创建 bytes.Buffer 对象

func NewBuffer(buf []byte) *Buffer
func NewBufferString(s string) *Buffer

2) 返回 b 中数据的切片

func (b *Buffer) Bytes() []byte

3) 返回 b 中数据的长度

func (b *Buffer) Len() int

其他方法参考:https://studygolang.com/articles/5887

3、binary学习

binary 包,简单的实现了数字和byte之间的转化varints之间的编码和解码。简单说一下,varints是一种使用一个或多个字节表示整型数据的方法。其中数值本身越小,其所占用的字节数越少。

1)数字与byte互相转化

通过读入和写入固定长度的值来转化数字。固定长度的值,既可以是指定长度的值,也可以是指定长度数组,也可以是包含指定长度结构体。数字类型必须申明为,bool, int8, uint8, int16, float32, complex64, …,具体可以去看源码

package main
import (
"bytes"
"encoding/binary"
"fmt"
)
func main() {
    by := []byte{0x00, 0x00, 0x03, 0xe8}
    var num int32
    bytetoint(by, &num)
    fmt.Println(int(num), num)

    // 测试 int -> byte
    by2 := []byte{}
    var num2 int32
    num2 = 333
    by2 = inttobyte(&num2)
    fmt.Println(by2)

    // 测试 byte -> int
    var num3 int32
    bytetoint(by2,&num3)
    fmt.Println(num3)
}
// byte 转化 int
func bytetoint(by []byte, num *int32)  {
    b_buf := bytes.NewBuffer(by)
    binary.Read(b_buf, binary.BigEndian, num)
}
// 数字 转化 byte
func inttobyte(num *int32) []byte {
    b_buf := new(bytes.Buffer)
    binary.Write(b_buf, binary.BigEndian,num)
    return b_buf.Bytes()
}

2)func Read

func Read(r io.Reader, order ByteOrder, data interface{}) error
`参数列表:
1)r  可以读出字节流的数据源
2)order  特殊字节序,包中提供大端字节序和小端字节序
3)data  需要解码成的数据
返回值:error  返回错误
功能说明:Read从r中读出字节数据并反序列化成结构数据。data必须是固定长的数据值或固定长数据的slice。从r中读出的数据可以使用特殊的 字节序来解码,并顺序写入value的字段。当填充结构体时,使用(_)名的字段将被跳过。`

案例

package main

import (
    "fmt"
    "log"
    "bytes"
    "encoding/binary"
)

func main() {
    var pi float64
    b := []byte{0x18,0x2d,0x44,0x54,0xfb,0x21,0x09,0x40}
    buf := bytes.NewBuffer(b)
    err := binary.Read(buf, binary.LittleEndian, &pi)
    if err != nil {
        log.Fatalln("binary.Read failed:", err)
    }
    fmt.Println(pi)
}

3)Write

func Write(w io.Writer, order ByteOrder, data interface{}) error
`参数列表:
1)w  可写入字节流的数据
2)order  特殊字节序,包中提供大端字节序和小端字节序
3)data  需要解码的数据
返回值:error  返回错误
功能说明:
Write讲data序列化成字节流写入w中。data必须是固定长度的数据值或固定长数据的slice,或指向此类数据的指针。写入w的字节流可用特殊的字节序来编码。另外,结构体中的(_)名的字段讲忽略。`

4)其他方法参考:

https://www.jianshu.com/p/ec461b39bf43

相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页