缘由

最近在研究一款游戏的源码。发现里面的通信协议是protobuf定义的,但还是自己定义了协议头部,类似 head + body。先解析头部里所存储的整个包的数据长度,然后再解析包剩余的数据,这样做的目的是为了防止沾包。我在想都用了protobuf了为啥不直接用grpc呢。一时想不出,于是我用golnag + protobuf 做了一个简单的client + server

定义协议

一个包数据组成:

  • 包头
    • size :表示body长度,int32,占位4字节
    • cmd : 表示指令,int32,占位 4字节

.proto文件

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示例代码

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示例代码

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如下:

golang
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
		}
	}
}

--完--