天天看點

自定義頭和protobuf解決沾包問題

文章目錄

    • 緣由
    • 定義協定
    • .proto檔案
    • Server示例代碼
    • Client示例代碼
    • 知識點
      • bufio.Scan的使用

緣由

最近在研究一款遊戲的源碼。發現裡面的通信協定是protobuf定義的,但還是自己定義了協定頭部,類似 head + body。先解析頭部裡所存儲的整個包的資料長度,然後再解析包剩餘的資料,這樣做的目的是為了防止沾包。我在想都用了protobuf了為啥不直接用grpc呢。一時想不出,于是我用golnag + protobuf 做了一個簡單的client + server

定義協定

一個包資料組成:

  • 標頭
    • size :表示body長度,int32,占位4位元組
    • cmd : 表示指令,int32,占位 4位元組
  • body:表示protobuf資料。

.proto檔案

// msg.proto
syntax = "proto3";
option go_package = ".;GCToLs";

package GCToLS;

enum MsgID
{
  eMsgToLSFromGC_Unknow = 0;
  eMsgToLSFromGC_Begin = 40960;
  eMsgToLSFromGC_AskLogin = 40961;
  eMsgToLSFromGC_End = 40970;
}

message AskLogin
{
  MsgID msgid = 1;
  uint32 platform = 2;
  string uin = 3;
  string sessionid = 4;
}

           

執行指令,生成 msg.pb.go檔案

protoc ./*.proto --go_out=protos -I ./
           

Server示例代碼

package main

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"fmt"
	"github.com/golang/protobuf/proto"
	pb "m1/protos"
	"net"
)
func doServerStuf(conn net.Conn)  {
	defer conn.Close()
	for {
		scanner := bufio.NewScanner(conn)
		scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
			if !atEOF {
				if len(data) > 8  {  //4位元組資料包長度  4位元組指令
					length := int32(0)
					binary.Read(bytes.NewReader(data[0:4]),binary.BigEndian, &length)
					if length <= 0 {
						return 0, nil, fmt.Errorf("length is 0")
					}
					fmt.Printf("len_data %d; length: %d\n", len(data), length)
					if int(length) + 8 <= len(data) {
						return int(length), data[8:int(length) + 8], nil
					}
				}
			}
			return
		})

		for scanner.Scan() {
			fmt.Println("scanner msg: ", string(scanner.Bytes()))
			msg := new(pb.AskLogin)
			err := proto.Unmarshal(scanner.Bytes(), msg)
			if err != nil {
				fmt.Printf("proto Unmarshal err: %s\n", err.Error())
				return
			}
			fmt.Printf("msg.msgid: %d, msg.session: %s\n", msg.Msgid, msg.Sessionid)
		}

		if err := scanner.Err(); err != nil {
			fmt.Println("無資料包!")
			return
		}
	}
}

func main() {
	errs := make(chan error)

	l, err := net.Listen("tcp",":9091")
	if err != nil {
		errs <- err
	}
	fmt.Println("Accept...9001")
	go func() {
		conn ,err := l.Accept()
		if err != nil {
			errs <- err
		}
		go doServerStuf(conn)
	}()
	fmt.Errorf("%s\n", <- errs)
}
           

Client示例代碼

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"github.com/golang/protobuf/proto"
	pb "m1/protos"
	"net"
)

func main() {
	conn, err := net.Dial("tcp","127.0.0.1:9091")
	if err != nil {
		fmt.Errorf("%s\n", err.Error())
		return
	}
	msg := &pb.AskLogin{
		Msgid: 1,
		Platform: 1,
		Uin: "1",
		Sessionid: "1001",
	}

	msgbyte,err := proto.Marshal(msg)
	if err != nil {
		fmt.Errorf("msg Marshal error %s\n", err.Error())
		return
	}

	buf := &bytes.Buffer{}
	var head []byte
	head = make([]byte, 8)
	binary.BigEndian.PutUint32(head[0:4], uint32(bytes.Count(msgbyte,nil) -1))
	binary.BigEndian.PutUint32(head[4:8], uint32(pb.MsgID_eMsgToLSFromGC_AskLogin))
	buf.Write(head[:8])
	buf.Write(msgbyte)
	fmt.Printf("%v\n", string(buf.Bytes()))

	conn.Write(buf.Bytes())
	defer conn.Close()
}
           

知識點

一、golang的net網路庫的使用

二、golang的goroutine

三、golang的channel使用

四、bufio.Scan可以很好的分割一個資料包。

bufio.Scan的使用

//生成一個 scanner
scanner := bufio.NewScanner(conn)
//自定義解包方法
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {})
//擷取解析出的body
for bufio.Scan{
        fmt.printf("%s\n", string(scanner.Bytes()));
}
           

標頭是8位元組,表示body長度的是 head[0:4]。表示指令的是head[4:8], 解析出body如下:

func doServerStuf(conn net.Conn)  {
	defer conn.Close()
	for {
		scanner := bufio.NewScanner(conn)
		scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
			if !atEOF {
				if len(data) > 8  {  //4位元組資料包長度  4位元組指令
					length := int32(0)
					binary.Read(bytes.NewReader(data[0:4]),binary.BigEndian, &length)
					if length <= 0 {
						return 0, nil, fmt.Errorf("length is 0")
					}
					fmt.Printf("len_data %d; length: %d\n", len(data), length)
					if int(length) + 8 <= len(data) {
						return int(length), data[8:int(length) + 8], nil
					}
				}
			}
			return
		})

		for scanner.Scan() {
			fmt.Println("scanner msg: ", string(scanner.Bytes()))
			msg := new(pb.AskLogin)
			err := proto.Unmarshal(scanner.Bytes(), msg)
			if err != nil {
				fmt.Printf("proto Unmarshal err: %s\n", err.Error())
				return
			}
			fmt.Printf("msg.msgid: %d, msg.session: %s\n", msg.Msgid, msg.Sessionid)
		}

		if err := scanner.Err(); err != nil {
			fmt.Println("無資料包!")
			return
		}
	}
}