MENU

开发联飞服务时的一些思考 ② 中间件和插件

July 16, 2021 • 个人作品

接上篇文章:https://blog.dextercai.com/2021-01-27-51.html

咕了半年才润色完,删了很多废话,尽量精简语言吧。

RumService是一款从2018年末开始开发的一款模拟飞行联飞软件,至今已经迭代了三个大版本(0,1,2),正式开始在生产环境使用的为RumService2.0。在本系列第二篇文章内,简要聊聊关于游戏服务器的中间件、插件机制的设计。

在前一篇文章中已经提到了,其实连飞服务并不是一个非常典型的游戏服务。在FSD协议中玩家会话的生命周期范围和套接字的生命周期相同,并不像基于RPC协议现代化的协议。FSD的协议从数据密度角度来说,应该是介于帧同步服务器和RPC服务器之间的协议。因为对于协议的实时性要求不那么高。套用一个网关去处理也是比较常见的一种处理手法。

回看FSD3.000 draft9这份代码,其实整体协议实现除了要求套接字持久连接之外,没有太多其他的过多要求。所以在设计Rum2.0的分服机制过程中,我选择的是将接入层和处理层进行分离,其中协议可以采用基于RPC接口,并简易地提供上下文。这样一来,处理层可以比较方便的扩容,接入层保持住连接即可,处理层和接入层之间对于一个套接字的描述可以采用比较自定义化的方式。

讲的云里雾里的,说白了就是对于接入层的套接字进行标记,方便抽象化地处理,由于RPC通讯可以提供上下文并且接入层和处理层的低耦合,所以编写过程中的感受也没差多少。

RumService的分服机制是这种架构设计下产生的一种运行模式,这里离不开中间件和插件。这里的中间件更多指的是面向消息的中间件,缩写MOM(Message Oriented Middleware),而不是传统意义上的中间件(eg.数据库中间件)。RumService的中间件设计如同一个定向导流的车道,让来自特定源头的消息去往指定的地方,其中只需要基于流量数据进行判断数据走向,并管理好数据结构即可。

从单体应用的常见分服机制来说,这种中间件机制通常会和两个部分密切关联,解析数据模块以及用户池模块。而在实际RumService的实践中(Rum并不是一个很典型的单体应用),中间件实现了对实际用户池操作的解耦,但解析数据上还逃不掉对于数据的简单处理。

事后想一想,应该是可以设计一个首包处理模块,由模块决定走向,这样减少后续长连接流量上不必要的处理(分服决定后一般不需要切换),中间件被首包处理模块直接控制,也能介绍很大程度的耦合。

最后要说的是RumService新加入的插件机制,插件机制采用了Lua作为脚本引擎的支持,在关键函数上进行Hook,可以帮助开发过程中的调试,或者是统计功能。

这个模块设计了文件监视机制,用于及时同步内部Lua虚拟机的状态。算是弥补一点Golang应用在不停机更新上的缺陷吧。

直接放代码感觉直观一点。


package plugin

import (
    "dextercai.com/Rum2/config"
    "dextercai.com/Rum2/define"
    "github.com/fsnotify/fsnotify"
    libs "github.com/vadv/gopher-lua-libs"
    lua "github.com/yuin/gopher-lua"
    "log"
    "sync"
)

type LuaVm struct {
    state *lua.LState
    pLock sync.RWMutex
}
var LuaVmService *LuaVm
var Watcher *fsnotify.Watcher

func StopPluginVm(){
    if Watcher != nil {
        Watcher.Close()
    }
}
func InitPluginVm() {
    if Watcher != nil {
        Watcher.Close()
    }
    LuaVmService = new(LuaVm)
    LuaVmService.InitLuaVmEnv()
    LuaVmService.ReloadLuaVmEnv()
    Watcher, _ := fsnotify.NewWatcher()
    go func() {
        for {
            select {
            case event, ok := <-Watcher.Events:
                if !ok {
                    log.Println(" [Plugin]: 监视器意外退出")
                    return
                }
                if event.Op & fsnotify.Write == fsnotify.Write {
                    if event.Name == config.RumConfig.PluginScript {
                        LuaVmService.ReloadLuaVmEnv()
                    }
                }
            case err, ok := <-Watcher.Errors:
                if !ok {
                    log.Println(" [Plugin]: 监视器意外退出")
                    return
                }
                log.Println(" [Plugin]: 监视器发生错误", err.Error())
            }
        }

    }()
    err := Watcher.Add(config.RumConfig.PluginScript)
    if err != nil{
        log.Println(" [Plugin]: 添加文件监视器时发生错误,", err.Error())
    }
}
func (this *LuaVm) InitLuaVmEnv(){
    this.state = new(lua.LState)
    this.state = lua.NewState()
    libs.Preload(this.state)
}
func (this *LuaVm) ReloadLuaVmEnv()  {
    if this.state != nil && !this.state.IsClosed() {
        this.state.Close()
        this.InitLuaVmEnv()
    }
    err := this.state.DoFile(config.RumConfig.PluginScript)
    if err != nil{
        log.Println(" [Plugin]: 重载插件时发生错误,", err.Error())
    }else {
        log.Println(" [Plugin]: 重载插件成功")
    }
}

func (this *LuaVm) SomeFunc(buf string) string{ //一个函数示例
    err := this.state.CallByParam(lua.P{
        Fn:      this.state.GetGlobal("SomeFunc"),
        NRet:    1,
        Protect: true,
    }, lua.LString(buf))
    if err != nil {
        return ""
    }
    ret := this.state.Get(-1)
    // 从堆栈中扔掉返回结果
    this.state.Pop(1)
    res, ok := ret.(lua.LString)
    if ok && string(res) != ""{
        return string(res)
    } else {
        return ""
    }
}
Last Modified: July 18, 2021
Archives QR Code
QR Code for this page
Tipping QR Code