pb 数据的使用 -- 游戏协议工具实战

前言:

其实之前已经用 python 实现过了(协议工具),最近又用 go 重构了一遍(刚入门 go)。所以本篇会分别从 go(详细讲)以及 py(粗略讲) 两方面讲叙。由于鄙人也是小小白,因此会讲的没那么高大上,尽可能俗一点,让其他刚进入游戏的老铁也能看个明白。

背景分析:

需求与方案制定:

至少需要实现:从客户端那边 log 输出中复制所有 “发送协议-->>XXX” 协议内容,通过工具可以实现并发去创建多个账号并执行这些协议内容。

为了应对协议内容的增删改变化,决定偷懒,不一一通过原有协议数据执行序列化与反序列化,而是全部通过 GM 协议执行。好处是可以偷懒,不需要采用其他方式(比如动态 import 去导入对应需要的 xx.pd.go),坏处是不能够对所有协议作反序列化。但是这个并不影响我的核心需求。

message CmdGMReqMsg {
    required string command = 1; 
}
message CmdGMRspMsg {
}

接下来请看代码实战。

GO 篇

HTTP 请求

"net/http"包使用

func Get(yoururl string, data map[string]string) map[string]interface{} {
    request, err := http.NewRequest("GET", yoururl, nil)
    if err != nil {
        log.Println("err->", err)
    }
    //加入get参数
    q := request.URL.Query()
    for key, value := range data {
        q.Add(key, value)
    }
    request.URL.RawQuery = q.Encode()
    resp, err := http.DefaultClient.Do(request)
    if err != nil {
        log.Println("err->", err)
    }
    defer resp.Body.Close()
    data1, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Println("err->", err)
    }
    rdata := string(data1)
    result := make(map[string]interface{})
    err1 := json.Unmarshal([]byte(rdata), &result)
    if err1 != nil {
        fmt.Println(err)
    }
    return result

proto 文件处理

最简单直接的办法,从官网直接拉 protoc.exe 文件,通过命令行转换即可。

举个栗子:

protoc --proto_path=..\\protos --python_out=..\\protos ..\\protos\XXX.proto

转换成 go 之后得到文件:XX.pb.go,接着如何根据协议内容创建结构体呢?稍微看一下官方例子就不难写出:

func Gmmsg(gm string) []byte {
    gm_mess := &pb.CmdGMReqMsg{
        Command: &gm,
    }
    //通过"google.golang.org/protobuf/proto"序列化
    out, _ := proto.Marshal(gm_mess)
    return out
}

我这边根据实际项目内容分析,协议最后都是嵌套作为 message ClientCmdData 中 data 值,那么也不难写出:

func Clientmsg(data []byte) []byte {
    clinetmsg := &pb.ClientCmdData{
        //部分字段省略
        Data:        data,
    }
    out, _ := proto.Marshal(clinetmsg)
    return out
}

websocket 连接

主要使用包:"github.com/gorilla/websocket"。这个去 github 学习一下,基本都能写出基础的 websocket 链接。

func connect() (c *websocket.Conn) {
    var addr string = "冬天的秘密"
    var u = url.URL{Scheme: "ws", Host: addr, Path: "冬天的秘密"}
    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    log.Printf("connecting to %s", u.String())
    return
}

发送与接收,以及反序列化尝试

func Send(t []byte, c *websocket.Conn) *pb.ServerCmdData {
    c.WriteMessage(websocket.BinaryMessage, t)
    time.Sleep(500 * time.Millisecond)
    _, message, _ := c.ReadMessage()
    serdata := &pb.ServerCmdData{}
    if err := proto.Unmarshal(message, serdata); err != nil {
        log.Fatalln("failed----------:", err)
    }
    //在我有需要的时候去掉注释,可以针对指定协议反序列化拿到服务器返回的数据
    // if *serdata.MessageId == int32(协议号) {
    //  temp := &pb.XXXXX{}
    //  if err := proto.Unmarshal(serdata.Data, temp); err != nil {
    //      log.Fatalln("failed----------:", err)
    //  }
    //  fmt.Printf("%+v#####", temp)
    // }
    return serdata
}

比如反序列化拿某个 message 的某个字段 id

另外可以根据需要,多加点实用的功能,比如知道服务器处理协议失败可以重发 10 次,超过 10 次不管;协议数据中最多会涉及到一个变化的 playerid,这个是需要根据实际注册账号得到的 playerid 的;还有其他小细节可以自己在使用过程中慢慢发挥。

协议 Log 内容处理

处理从客户端复制下来的一大批协议内容:发送协议:> XXXXXXX

这块主要涉及 go 文件处理

func Handfile() (result []string) {
    file := "冬天的秘密.log"
    f, err := os.Open(file)
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        if err = f.Close(); err != nil {
            log.Fatal(err)
        }
    }()
    s := bufio.NewScanner(f)
    for s.Scan() {
        result = append(result, s.Text())
    }
    err = s.Err()
    if err != nil {
        log.Fatal(err)
    }
    return
}

接收 log 内容,做一定处理(根据需要)然后发送;接收 nickname,根据 nickname 注册并创建 nickname 游戏角色

func Handmess(nickname string, mess []string) {
    //涉及协议内容格式,不敢放太多
    con := connect()
    loginmsg := Clientmsg(1001, Loginmessage(token, accountid))
    Resend(loginmsg, con)
    registmsg := Clientmsg(1000, Regist(nickname, token, accountid))
    serverrsg := Resend(registmsg, con)
    if !*serverrsg.Result {
        log.Printf("%s注册失败,该账号可能已经注册并在游戏中创建了账号(或者账号非法)", nickname)
        return
    }
    //省略mess处理与发送
    time.Sleep(1 * time.Second)
    con.Close()

go 的并发(太香了)

type Glimit struct {
    n int
    c chan struct{}
}

func New(n int) *Glimit {
    return &Glimit{
        n: n,
        c: make(chan struct{}, n),
    }
}
func (g *Glimit) Run(f func()) {
    g.c <- struct{}{}
    go func() {
        f()
        <-g.c
    }()
}
g := New(20)
log.Printf("start-------------")
filemess := websockets_test.Handfile()
var wg sync.WaitGroup
for i := 100; i < 200; i++ {
    wg.Add(1)
    value := strconv.Itoa(i)
    gofunc := func() {
        websockets_test.Handmess("newnick"+value, filemess)
        log.Printf("end-------------%s", "newnick"+value)
        wg.Done()
    }
    g.Run(gofunc)
}
wg.Wait()
log.Printf("end-------------")

实战结果

Python 篇

相信大伙的 python 都比我厉害,因此 Python 篇会简略一点。

    # asyncio
    async def run():
        semaphore = asyncio.Semaphore(15)
        to_get = [main_logic(i, semaphore) for i in range(num)]
        await asyncio.wait(to_get)
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
    # websockets
    async with websockets.connect(_server_ws_url) as ws:
        # ws.close_timeout = 200
        await send_message(XXXXXX)
    # pd数据的引用
    def clientmsg(self, messageid, msg=None):
        cmsg = Cmd_pb2.ClientCmdData()
        cmsg.messageId = messageid
        cmsg.clientIndex = 0
        if msg:
            cmsg.data = msg
        return cmsg

[config]
# 服务器id
serverid = 冬天的秘密
_server_ws_url = 冬天的秘密
# 平台id
platformId = 冬天的秘密
# 你想要登录的账号,如果不存在会自动注册
nickname = nihao
start = 39
num = 1
# playerid(自增id),进阶功能,可以将协议数据中所有用到playerid替换为该player的正确playerid
playerid = 冬天的秘密
log = 高级账号.log

另外还有一套用于 jenkins 配置

class config(object):
    def __init__(self):
        server = {
            "A服": '冬天的秘密',
            "B网": '冬天的秘密',
        }
        serverid = {
            "A服": XXX,
            "B网": XX,
        }
        platformId = {
            "A服": XX,
            "B网": XX,
        }
        log = {
            "高级账号": "D:/高级账号.log",
            "创建新号": "D://创建新号.log",
            "第一章": "D://什么都不做只通关第一章.log",
        }
        if not os.getenv("choice_server"):
            print("你没有选择服务器!!!")
            sys.exit(0)
        if not os.getenv("可选协议") and not os.getenv("protos"):
            print("你没有填写任何协议内容!!!")
            sys.exit(0)
        self.serverid = serverid[os.getenv("choice_server")]
        self.platformId = platformId[os.getenv("choice_server")]
        gamename = re.split(r'\n',os.getenv("gamename"))
        self.nickname = gamename[0]
        self.num = gamename[2]
        self.start = gamename[1]
        if os.getenv("可选协议"):
            self.log = linecache.getlines(log[os.getenv("可选协议")])
        else:
            self.log = re.split(r'\n', os.getenv("protos"))
        self._server_ws_url = server[os.getenv("choice_server")]


↙↙↙阅读原文可查看相关链接,并与作者交流