go protobuf tcp 粘包处理 demo
1.安装相关包
设置代理
set GOPROXY=https://goproxy.cn
安装 golang
的proto工具包go get -u github.com/golang/protobuf/proto
安装 goalng
的proto编译支持go get -u github.com/golang/protobuf/protoc-gen-go
查看 protoc 版本
protoc --version
2.创建项目
模拟微服务开发,在某个空文件夹下建立两个文件夹
分别为 client
和 server
3.创建 proto 文件
proto 文件是微服务交互的基本
proto的语法见官方:https://developers.google.com/protocol-buffers/docs/proto3?hl=zh-cn
这里简单写一个示例(people.proto)
syntax = "proto3"; // 协议为proto3
package model; // 包名
option go_package ="./model"; //protoc --go_out=./ ./people.proto
// 定义消息, people 是类名
message people {
string name = 1;
int32 age = 2;
}
4.生成 .bp.go
文件
protoc --go_out=./ ./people.proto
运行后会在spider目录下生成 people.pb.go
文件
该文件是 server 和 client 的通信协议,勿动
5.编写 server 端
编写 server.go
文件
package prob
import (
"bufio"
"fmt"
"github.com/golang/protobuf/proto"
"net"
"os"
"tes/prob/model"
)
func Server() {
//监听
listener, err := net.Listen("tcp", "localhost:6600")
if err != nil {
panic(err)
}
for {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
fmt.Println("new connect", conn.RemoteAddr())
go readMessage(conn)
}
}
//接收消息
func readMessage(conn net.Conn) {
defer conn.Close()
reader :=bufio.NewReader(conn)
//读消息
for {
data,err := Decode(reader)
if err ==nil && len(data)>0{
p := &model.People{}
err := proto.Unmarshal(data, p)
if err != nil {
panic(err)
}
//buf= make([]byte, 10240,10240)//前两个字节表示本次请求body为长度
fmt.Printf("receive %s %+v \n", conn.RemoteAddr(), p)
if p.Name == "stop" {
os.Exit(1)
}
}
if err!=nil{
panic(err)
}
}
}
6.编写 client 端
编写 client.go
文件
package prob
import (
"fmt"
"github.com/golang/protobuf/proto"
"net"
"strconv"
"tes/prob/model"
"time"
)
func Client() {
strIP := "localhost:6600"
var conn net.Conn
var err error
//连接服务器
if conn, err = net.Dial("tcp", strIP); err != nil {
panic(err)
}
fmt.Println("connect", strIP, "success")
defer conn.Close()
//发送消息
//sender := bufio.NewScanner(os.Stdin)
for i :=0; i<10000;i++ {
stSend := &model.People{
Name: "lishang "+ strconv.Itoa(i),
Age: *proto.Int(i),
}
//protobuf编码
pData, err := proto.Marshal(stSend)
if err != nil {
panic(err)
}
data,err := Encode(pData)
if err==nil{
conn.Write(data)
fmt.Printf("%d send %d \n" ,i,uint16(len(pData)))
}
/*if sender.Text() == "stop" {
return
}*/
}
for{
time.Sleep(time.Millisecond*10)
/*stSend := &model.People{
Name: "lishang "+ strconv.Itoa(22222222),
Age: *proto.Int(22222222),
}
//protobuf编码
pData, err := proto.Marshal(stSend)
if err != nil {
panic(err)
}
b := make([]byte,2,2)
binary.BigEndian.PutUint16(b,uint16(len(pData)))
//发送
conn.Write(append(b,pData...))*/
}
}
7.粘包处理
编写 protol.go
文件
package prob
import (
"bufio"
"bytes"
"encoding/binary"
)
//将数据包编码(即加上包头再转为二进制)
func Encode(mes []byte) ([]byte, error) {
//获取发送数据的长度,并转换为四个字节的长度,即int32
len := uint16(len(mes))
//创建数据包
dataPackage := new(bytes.Buffer) //使用字节缓冲区,一步步写入性能更高
//先向缓冲区写入包头
//大小端口诀:大端:尾端在高位,小端:尾端在低位
//编码用小端写入,解码也要从小端读取,要保持一致
err := binary.Write(dataPackage, binary.LittleEndian, len) //往存储空间小端写入数据
if err != nil {
return nil, err
}
//写入消息
err = binary.Write(dataPackage, binary.LittleEndian, mes)
if err != nil {
return nil, err
}
return dataPackage.Bytes(), nil
}
//解码数据包
func Decode(reader *bufio.Reader) ([]byte, error) {
//读取数据包的长度(从包头获取)
lenByte, err := reader.Peek(2) //读取前四个字节的数据
if err!=nil{
return []byte{}, err
}
//转成Buffer对象,设置为从小端读取
buff := bytes.NewBuffer(lenByte)
var len uint16 //读取的数据大小,初始化为0
err = binary.Read(buff, binary.LittleEndian, &len) //从小端读取
if err != nil {
return []byte{}, err
}
//读取消息
pkg := make([]byte, int(len)+2)
//Buffered返回缓冲区中现有的可读取的字节数
if reader.Buffered() < int(len)+2 { //如果读取的包头的数据大小和读取到的不符合
hr := 0
for hr < int(len)+2 {
l, err := reader.Read(pkg[hr:])
if err != nil {
return []byte{}, err
}
hr+=l
}
}else{
_, err := reader.Read(pkg)
if err != nil {
return []byte{}, err
}
}
return pkg[2:], nil
}
8.运行
编写 main.go
文件
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"tes/prob"
)
func main(){
go prob.Server()
go prob.Client()
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
fmt.Printf("job get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
fmt.Printf("job exit")
return
case syscall.SIGHUP:
default:
return
}
}
}
我们运行后可看到结果已经正常返回并打印