Skip to content

duanhf2012/origin

Repository files navigation

origin 游戏服务器引擎简介

origin 是一个由 Go 语言(golang)编写的分布式开源游戏服务器引擎。origin适用于各类游戏服务器的开发,包括 H5(HTML5)游戏服务器。

origin 解决的问题:

  • origin总体设计如go语言设计一样,总是尽可能的提供简洁和易用的模式,快速开发。
  • 能够根据业务需求快速并灵活的制定服务器架构。
  • 利用多核优势,将不同的service配置到不同的node,并能高效的协同工作。
  • 将整个引擎抽象三大对象,node,service,module。通过统一的组合模型管理游戏中各功能模块的关系。
  • 有丰富并健壮的工具库。

Hello world!

下面我们来一步步的建立origin服务器,建议使用origin v2版本,内容如下:

package main

import (
    "github.com/duanhf2012/origin/v2/node"
)

func main() {
    node.Start()
}

以上只是基础代码,具体运行参数和配置请参照第一章节。

一个origin进程需要创建一个node对象,Start开始运行。您也可以直接下载origin引擎示例:

git clone https://github.com/duanhf2012/originserver_v2.git

本文所有的说明都是基于该示例为主。

我们也提供了示例项目,包含网络模块(支持tcp,kcp,websocket)、角色加载登录流程,存档,配置读取等功能。请参照:

git clone https://github.com/duanhf2012/origingame.git

origin引擎三大对象关系

  • Node: 可以认为每一个Node代表着一个origin进程
  • Service:一个独立的服务可以认为是一个大的功能模块,他是Node的子集,创建完成并安装Node对象中。服务可以支持对外部RPC等功能。
  • Module: 这是origin最小对象单元,强烈建议所有的业务模块都划分成各个小的Module组合,origin引擎将监控所有服务与Module运行状态,例如可以监控它们的慢处理和死循环函数。Module可以建立树状关系。Service本身也是Module的类型。

origin配置说明

origin的配置文件以json格式,主要包含Discovery、RpcMode、NodeList、Service部分,具体格式如下:

{
    "Discovery":{},
    "RpcMode":{},
    "NodeList":[],
    "Service":{},
    "Global": {}
}

Discovery部分

origin目前支持etcd与origin自带的服务发现类型。

Etcd方式示例:

  "Discovery": {
    "Etcd": {
      "TTLSecond": 10,
      "DialTimeoutMillisecond": 3000,
      "EtcdList": [
        {
          "NetworkName": ["network1"],
          "Endpoints": ["http://192.168.13.24:12379"]
        }
      ]
    }
  }

TTLSecond:表示健康检查TTL失效时间10秒

DialTimeoutMillisecond: 与etcd连接超时时间

EtcdList:Etcd列表,可以多个Etcd服务器连接

NetworkName:所在的网络名称,可以配置多个。node会往对应的网络名称中注册、监听发现Service。NetworkName也起到发现隔离的作用。

Endpoints:Etcd服务器地址

Origin方式示例:

{
  "Discovery": {
    "Origin":{
      "TTLSecond": 10,
      "MasterNodeList": [
        {
          "NodeId": "test_1",
          "ListenAddr": "127.0.0.1:8801"
        }
      ]
    }
  }
}

TTLSecond:表示健康检查TTL失效时间10秒

MasterNodeList:指定哪些Node为服务发现Master结点,需要配置NodeId与ListenAddr,注意它们要与实际的Node配置一致。

RpcMode部分

默认模式

{
  "RpcMode":{
      "Type": "Default"
  }
}

默认模式下,origin的node之前通过tcp连接组网。

Nats模式

{
  "RpcMode":{
      "Type": "Nats",
      "remark": "support Default or Nats",
      "Nats": {
          "NatsUrl":"127.0.0.1:4222",
          "NoRandomize": true
      }
  }
}

NatsUrl:Nats连接url串

NoRandomize:在多连接集群模式下,连接nats节点是否顺序策略。false表示随机连接,true表示顺序连接。

NodeList部分

{
    "NodeList":[
        {
          "NodeId": "node_1",
          "Private": false,
          "ListenAddr":"127.0.0.1:8001",
          "MaxRpcParamLen": 409600,
          "CompressBytesLen": 20480,
          "remark":"//以_打头的,表示只在本机进程,不对整个子网公开",
          "ServiceList": ["TestService1","TestService2","TestServiceCall","GateService","_TcpService","HttpService","WSService"]
        },
        {
          "NodeId": "node_2",
          "Private": false,
          "ListenAddr":"127.0.0.1:8002",
          "MaxRpcParamLen": 409600,
          "CompressBytesLen": 20480,
          "remark":"//以_打头的,表示只在本机进程,不对整个子网公开",
          "ServiceList": ["TestService1","TestService2","TestServiceCall","GateService","TcpService","HttpService","WSService"]
        }
    ]

以上配置了两个结点服务器程序:

  • NodeId: 表示origin程序的结点Id标识,同一个服务发现网络中不允许重复。
  • Private: 是否私有结点,如果为true,表示其他结点不会发现它,但可以自我运行。
  • ListenAddr:Rpc通信服务的监听地址
  • MaxRpcParamLen:Rpc参数数据包最大长度,该参数可以缺省,默认一次Rpc调用支持最大4294967295byte长度数据。
  • CompressBytesLen:Rpc网络数据压缩,当数据>=20480byte时将被压缩。该参数可以缺省或者填0时不进行压缩。
  • remark:备注,可选项
  • ServiceList:该Node拥有的服务列表,注意:origin按配置的顺序进行安装初始化。但停止服务的顺序是相反。

在启动程序命令originserver -start nodeid="node_1"中nodeid就是根据该配置装载服务。 更多参数使用,请使用originserver -help查看。

Service 部分

service.json如下:

{
  "Service":{
      "HttpService":{
        "ListenAddr":"0.0.0.0:9402",
        "ReadTimeout":10000,
        "WriteTimeout":10000,
        "ProcessTimeout":10000,
        "ManualStart": false,
        "CAFile":[
        {
          "Certfile":"",
            "Keyfile":""
        }]
  
      },
      "TcpService":{
        "ListenAddr":"0.0.0.0:9030",
        "MaxConnNum":3000,
        "PendingWriteNum":10000,
        "LittleEndian":false,
        "MinMsgLen":4,
        "MaxMsgLen":65535
      },
      "WSService":{
        "ListenAddr":"0.0.0.0:9031",
        "MaxConnNum":3000,
        "PendingWriteNum":10000,
        "MaxMsgLen":65535
      }  
  },
  "NodeService":[
   {
      "NodeId":1,
      "TcpService":{
        "ListenAddr":"0.0.0.0:9830",
        "MaxConnNum":3000,
        "PendingWriteNum":10000,
        "LittleEndian":false,
        "MinMsgLen":4,
        "MaxMsgLen":65535
      },
      "WSService":{
        "ListenAddr":"0.0.0.0:9031",
        "MaxConnNum":3000,
        "PendingWriteNum":10000,
        "MaxMsgLen":65535
      }  
   },
   {
      "NodeId":2,
      "TcpService":{
        "ListenAddr":"0.0.0.0:9030",
        "MaxConnNum":3000,
        "PendingWriteNum":10000,
        "LittleEndian":false,
        "MinMsgLen":4,
        "MaxMsgLen":65535
      },
      "WSService":{
        "ListenAddr":"0.0.0.0:9031",
        "MaxConnNum":3000,
        "PendingWriteNum":10000,
        "MaxMsgLen":65535
      }  
   }
  ]
 
}

以上配置分为两个部分:Global,Service与NodeService。Global是全局配置,在任何服务中都可以通过cluster.GetCluster().GetGlobalCfg()获取,NodeService中配置的对应结点中服务的配置,如果启动程序中根据nodeid查找该域的对应的服务,如果找不到时,从Service公共部分查找。

HttpService配置

  • ListenAddr:Http监听地址
  • ReadTimeout:读网络超时毫秒
  • WriteTimeout:写网络超时毫秒
  • ProcessTimeout: 处理超时毫秒
  • ManualStart: 是否手动控制开始监听,如果true,需要手动调用StartListen()函数
  • CAFile: 证书文件,如果您的服务器通过web服务器代理配置https可以忽略该配置

TcpService配置

  • ListenAddr: 监听地址
  • MaxConnNum: 允许最大连接数
  • PendingWriteNum:发送网络队列最大数量
  • LittleEndian:是否小端
  • MinMsgLen:包最小长度
  • MaxMsgLen:包最大长度

WSService配置

  • ListenAddr: 监听地址
  • MaxConnNum: 允许最大连接数
  • PendingWriteNum:发送网络队列最大数量
  • MaxMsgLen:包最大长度

Global部分

{
  "Global": {
    "AreaId": 1
  }
}

这部分,在所有的服务中都可以类似以下代码获取:

globalCfg := cluster.GetCluster().GetGlobalCfg()
mapGlobal, ok := globalCfg.(map[string]interface{})
if ok == false {
    return fmt.Errorf("Canot find Global from config.")
}

areaId, ok := mapGlobal["AreaId"]

第一章:origin基础:

查看github.com/duanhf2012/originserver_v2中的simple_service中新建两个服务,分别是TestService1.go与CTestService2.go。

simple_service/TestService1.go如下:

package simple_service

import (
    "github.com/duanhf2012/origin/v2/node"
    "github.com/duanhf2012/origin/v2/service"
)

//模块加载时自动安装TestService1服务
func init(){
    node.Setup(&TestService1{})
}

//新建自定义服务TestService1
type TestService1 struct {
    //所有的自定义服务必需加入service.Service基服务
    //那么该自定义服务将有各种功能特性
    //例如: Rpc,事件驱动,定时器等
    service.Service
}

//服务初始化函数,在安装服务时,服务将自动调用OnInit函数
func (slf *TestService1) OnInit() error {
    return nil
}


simple_service/TestService2.go如下:

import (
    "github.com/duanhf2012/origin/v2/node"
    "github.com/duanhf2012/origin/v2/service"
)

func init(){
    node.Setup(&TestService2{})
}

type TestService2 struct {
    service.Service
}

func (slf *TestService2) OnInit() error {
    return nil
}


  • main.go运行代码
package main

import (
    "github.com/duanhf2012/origin/v2/node"
    //导入simple_service模块
    _"orginserver/simple_service"
)

func main(){
    node.Start()
}
  • config/cluster/cluster.json如下:
{
    "NodeList":[
        {
          "NodeId": "nodeid_1",
          "Private": false,
          "ListenAddr":"127.0.0.1:8001",
          "remark":"//以_打头的,表示只在本机进程,不对整个子网开发",
          "ServiceList": ["TestService1","TestService2"]
        }
    ]
}

编译后运行结果如下:

#originserver -start nodeid="nodeid_1"
TestService1 OnInit.
TestService2 OnInit.

第二章:Service中常用功能:

定时器:

在开发中最常用的功能有定时任务,origin提供两种定时方式:

一种AfterFunc函数,可以间隔一定时间触发回调,参照simple_service/TestService2.go,实现如下:

func (slf *TestService2) OnInit() error {
    fmt.Printf("TestService2 OnInit.\n")
    slf.AfterFunc(time.Second*1,slf.OnSecondTick)
    return nil
}

func (slf *TestService2) OnSecondTick(){
    fmt.Printf("tick.\n")
    slf.AfterFunc(time.Second*1,slf.OnSecondTick)
}

此时日志可以看到每隔1秒钟会print一次"tick.",如果下次还需要触发,需要重新设置定时器

另一种方式是类似Linux系统的crontab命令,使用如下:


func (slf *TestService2) OnInit() error {
    fmt.Printf("TestService2 OnInit.\n")

    //crontab模式定时触发
    //NewCronExpr的参数分别代表:Seconds Minutes Hours DayOfMonth Month DayOfWeek
    //以下为每换分钟时触发
    cron,_:=timer.NewCronExpr("0 * * * * *")
    slf.CronFunc(cron,slf.OnCron)
    return nil
}


func (slf *TestService2) OnCron(cron *timer.Cron){
    fmt.Printf(":A minute passed!\n")
}

以上运行结果每换分钟时打印:A minute passed!

打开多协程模式:

在origin引擎设计中,所有的服务是单协程模式,这样在编写逻辑代码时,不用考虑线程安全问题。极大的减少开发难度,但某些开发场景下不用考虑这个问题,而且需要并发执行的情况,比如,某服务只处理数据库操作控制,而数据库处理中发生阻塞等待的问题,因为一个协程,该服务接受的数据库操作只能是一个 一个的排队处理,效率过低。于是可以打开此模式指定处理协程数,代码如下:

func (slf *TestService1) OnInit() error {
    fmt.Printf("TestService1 OnInit.\n")

    //打开多线程处理模式,10个协程并发处理
    slf.SetGoRoutineNum(10)
    return nil
}

性能监控功能:

我们在开发一个大型的系统时,经常由于一些代码质量的原因,产生处理过慢或者死循环的产生,该功能可以被监测到。使用方法如下:

func (slf *TestService1) OnInit() error {
    fmt.Printf("TestService1 OnInit.\n")
    //打开性能分析工具
    slf.OpenProfiler()
    //监控超过1秒的慢处理
    slf.GetProfiler().SetOverTime(time.Second*1)
    //监控超过10秒的超慢处理,您可以用它来定位是否存在死循环
    //比如以下设置10秒,我的应用中是不会发生超过10秒的一次函数调用
    //所以设置为10秒。
    slf.GetProfiler().SetMaxOverTime(time.Second*10)

    slf.AfterFunc(time.Second*2,slf.Loop)
    //打开多线程处理模式,10个协程并发处理
    //slf.SetGoRoutineNum(10)
    return nil
}

func (slf *TestService1) Loop(){
    for {
        time.Sleep(time.Second*1)
    }
}


func main(){
    //打开性能分析报告功能,并设置10秒汇报一次
    node.OpenProfilerReport(time.Second*10)
    node.Start()
}

上面通过GetProfiler().SetOverTime与slf.GetProfiler().SetMaxOverTimer设置监控时间 并在main.go中,打开了性能报告器,以每10秒汇报一次,因为上面的例子中,定时器是有死循环,所以可以得到以下报告:

2020/04/22 17:53:30 profiler.go:179: [release] Profiler report tag TestService1: process count 0,take time 0 Milliseconds,average 0 Milliseconds/per. too slow process:Timer_orginserver/simple_service.(*TestService1).Loop-fm is take 38003 Milliseconds 直接帮助找到TestService1服务中的Loop函数

结点连接和断开事件监听:

在有些业务中需要关注某结点是否断开连接,可以注册回调如下:

func (ts *TestService) OnInit() error{
    ts.RegRpcListener(ts)

    return nil
}

func (ts *TestService) OnNodeConnected(nodeId int){
}

func (ts *TestService) OnNodeDisconnect(nodeId int){
}

第三章:Module使用:

Module创建与销毁:

可以认为Service就是一种Module,它有Module所有的功能。在示例代码中可以参考originserver_v2/simple_module/TestService3.go。

package simple_module

import (
    "fmt"
    "github.com/duanhf2012/origin/v2/node"
    "github.com/duanhf2012/origin/v2/service"
)

func init(){
    node.Setup(&TestService3{})
}

type TestService3 struct {
    service.Service
}

type Module1 struct {
    service.Module
}

type Module2 struct {
    service.Module
}

func (slf *Module1) OnInit()error{
    fmt.Printf("Module1 OnInit.\n")
    return nil
}

func (slf *Module1) OnRelease(){
    fmt.Printf("Module1 Release.\n")
}

func (slf *Module2) OnInit()error{
    fmt.Printf("Module2 OnInit.\n")
    return nil
}

func (slf *Module2) OnRelease(){
    fmt.Printf("Module2 Release.\n")
}


func (slf *TestService3) OnInit() error {
    //新建两个Module对象
    module1 := &Module1{}
    module2 := &Module2{}
    //将module1添加到服务中
    module1Id,_ := slf.AddModule(module1)
    //在module1中添加module2模块
    module1.AddModule(module2)
    fmt.Printf("module1 id is %d, module2 id is %d",module1Id,module2.GetModuleId())

    //释放模块module1
    slf.ReleaseModule(module1Id)
    fmt.Printf("xxxxxxxxxxx")
    return nil
}

在OnInit中创建了一条线型的模块关系TestService3->module1->module2,调用AddModule后会返回Module的Id,自动生成的Id从10e17开始,内部的id,您可以自己设置Id。当调用ReleaseModule释放时module1时,同样会将module2释放。会自动调用OnRelease函数,日志顺序如下:

Module1 OnInit.
Module2 OnInit.
module1 id is 100000000000000001, module2 id is 100000000000000002
Module2 Release.
Module1 Release.

在Module中同样可以使用定时器功能,请参照第二章节的定时器部分。

第四章:事件使用

事件是origin中一个重要的组成部分,可以在服务与各module之间进行事件通知。它也是一个典型的观察者设计模型。在event中有两个类型的interface,一个是event.IEventProcessor它提供注册与卸载功能,另一个是event.IEventHandler提供消息广播等功能。

在目录simple_event/TestService5.go中

package simple_event

import (
	"fmt"
	"github.com/duanhf2012/origin/v2/event"
	"github.com/duanhf2012/origin/v2/node"
	"github.com/duanhf2012/origin/v2/service"
	"github.com/duanhf2012/origin/v2/util/timer"
	"time"
)

func init() {
	node.Setup(&TestService5{})
}

const (
	//自定义事件类型,必需从event.Sys_Event_User_Define开始
	//event.Sys_Event_User_Define以内给系统预留
	EVENT1 event.EventType = event.Sys_Event_User_Define + 1
)

type TestService5 struct {
	service.Service
}

type TestModule struct {
	service.Module
}

func (slf *TestModule) OnInit() error {
	//在TestModule中注册监听EVENT1事件
	slf.GetEventProcessor().RegEventReceiverFunc(EVENT1, slf.GetEventHandler(), slf.OnModuleEvent)

	return nil
}

// OnModuleEvent 模块监听事件回调
func (slf *TestModule) OnModuleEvent(ev event.IEvent) {
	event := ev.(*event.Event)
	fmt.Printf("OnModuleEvent type :%d data:%+v\n", event.GetEventType(), event.Data)
}

// OnInit 服务初始化函数,在安装服务时,服务将自动调用OnInit函数
func (slf *TestService5) OnInit() error {
	//在服务中注册监听EVENT1类型事件
	slf.RegEventReceiverFunc(EVENT1, slf.GetEventHandler(), slf.OnServiceEvent)
	slf.AddModule(&TestModule{})

	slf.AfterFunc(time.Second*10, slf.TriggerEvent)
	return nil
}

// OnServiceEvent 服务监听事件回调
func (slf *TestService5) OnServiceEvent(ev event.IEvent) {
	event := ev.(*event.Event)
	fmt.Printf("OnServiceEvent type :%d data:%+v\n", event.Type, event.Data)
}

func (slf *TestService5) TriggerEvent(t *timer.Timer) {
	//广播事件,传入event.Event对象,类型为EVENT1,Data可以自定义任何数据
	//这样,所有监听者都可以收到该事件
	slf.GetEventHandler().NotifyEvent(&event.Event{
		Type: EVENT1,
		Data: "event data.",
	})
}


程序运行10秒后,调用slf.TriggerEvent函数广播事件,于是在TestService5中会收到

OnServiceEvent type :2 data:event data.
OnModuleEvent type :2 data:event data.

在上面的TestModule中监听的事情,当这个Module被Release时监听会自动卸载。

第五章:RPC使用

RPC是service与service间通信的重要方式,它允许跨进程node互相访问,当然也可以指定nodeid进行调用。如下示例:

simple_rpc/TestService6.go文件如下:

package simple_rpc

import (
    "github.com/duanhf2012/origin/v2/node"
    "github.com/duanhf2012/origin/v2/service"
)

func init(){
    node.Setup(&TestService6{})
}

type TestService6 struct {
    service.Service
}

func (slf *TestService6) OnInit() error {
    return nil
}

type InputData struct {
    A int
    B int
}

// 注意RPC函数名的格式必需为RPC_FunctionName或者是RPCFunctionName,如下的RPC_Sum也可以写成RPCSum
func (slf *TestService6) RPC_Sum(input *InputData,output *int) error{
    *output = input.A+input.B
    return nil
}

simple_rpc/TestService7.go文件如下:

package simple_rpc

import (
    "fmt"
    "github.com/duanhf2012/origin/v2/node"
    "github.com/duanhf2012/origin/v2/service"
    "time"
)

func init(){
    node.Setup(&TestService7{})
}

type TestService7 struct {
    service.Service
}

func (slf *TestService7) OnInit() error {
    slf.AfterFunc(time.Second*2,slf.CallTest)
    slf.AfterFunc(time.Second*2,slf.AsyncCallTest)
    slf.AfterFunc(time.Second*2,slf.GoTest)
    return nil
}

func (slf *TestService7) CallTest(){
    var input InputData
    input.A = 300
    input.B = 600
    var output int

    //同步调用其他服务的rpc,input为传入的rpc,output为输出参数
    err := slf.Call("TestService6.RPC_Sum",&input,&output)
    if err != nil {
        fmt.Printf("Call error :%+v\n",err)
    }else{
        fmt.Printf("Call output %d\n",output)
    }
  
  
    //自定义超时,默认rpc超时时间为15s,以下设置1秒钟超过
    err = slf.CallWithTimeout(time.Second*1, "TestService6.RPC_Sum", &input, &output)
    if err != nil {
        fmt.Printf("Call error :%+v\n", err)
    } else {
        fmt.Printf("Call output %d\n", output)
    }
}


func (slf *TestService7) AsyncCallTest(){
    var input InputData
    input.A = 300
    input.B = 600
    /*slf.AsyncCallNode(1,"TestService6.RPC_Sum",&input,func(output *int,err error){
    })*/
    //异步调用,在数据返回时,会回调传入函数
    //注意函数的第一个参数一定是RPC_Sum函数的第二个参数,err error为RPC_Sum返回值
    err := slf.AsyncCall("TestService6.RPC_Sum", &input, func(output *int, err error) {
        if err != nil {
            fmt.Printf("AsyncCall error :%+v\n", err)
        } else {
            fmt.Printf("AsyncCall output %d\n", *output)
        }
    })
    fmt.Println(err)

    //自定义超时,返回一个cancel函数,可以在业务需要时取消rpc调用
    rpcCancel, err := slf.AsyncCallWithTimeout(time.Second*1, "TestService6.RPC_Sum", &input, func(output *int, err error) {
        //如果下面注释的rpcCancel()函数被调用,这里可能将不再返回
        if err != nil {
            fmt.Printf("AsyncCall error :%+v\n", err)
        } else {
            fmt.Printf("AsyncCall output %d\n", *output)
        }
    })
    //rpcCancel()
    fmt.Println(err, rpcCancel)
  
}

func (slf *TestService7) GoTest(){
    var input InputData
    input.A = 300
    input.B = 600

    //在某些应用场景下不需要数据返回可以使用Go,它是不阻塞的,只需要填入输入参数
    err := slf.Go("TestService6.RPC_Sum",&input)
    if err != nil {
        fmt.Printf("Go error :%+v\n",err)
    }

    //以下是广播方式,如果在同一个子网中有多个同名的服务名,CastGo将会广播给所有的node
    //slf.CastGo("TestService6.RPC_Sum",&input)
}

您可以把TestService6配置到其他的Node中,比如NodeId为2中。只要在一个子网,origin引擎可以无差别调用。开发者只需要关注Service关系。同样它也是您服务器架构设计的核心需要思考的部分。

第六章:并发函数调用

在开发中经常会有将某些任务放到其他协程中并发执行,执行完成后,将服务的工作线程去回调。使用方式很简单,先打开该功能如下代码:

    //以下通过cpu数量来定开启协程并发数量,建议:(1)cpu密集型计算使用1.0  (2)i/o密集型使用2.0或者更高
    slf.OpenConcurrentByNumCPU(1.0)
  
    //以下通过函数打开并发协程数,以下协程数最小5,最大10,任务管道的cap数量1000000
    //origin会根据任务的数量在最小与最大协程数间动态伸缩
    //slf.OpenConcurrent(5, 10, 1000000)

使用示例如下:


func (slf *TestService13) testAsyncDo() {
    var context struct {
        data int64
    }

    //1.示例普通使用
    //参数一的函数在其他协程池中执行完成,将执行完成事件放入服务工作协程,
    //参数二的函数在服务协程中执行,是协程安全的。
    slf.AsyncDo(func() bool {
        //该函数回调在协程池中执行
        context.data = 100
        return true
    }, func(err error) {
        //函数将在服务协程中执行
        fmt.Print(context.data) //显示100
    })

    //2.示例按队列顺序
    //参数一传入队列Id,同一个队列Id将在协程池中被排队执行
    //以下进行两次调用,因为两次都传入参数queueId都为1,所以它们会都进入queueId为1的排队执行
    queueId := int64(1)
    for i := 0; i < 2; i++ {
        slf.AsyncDoByQueue(queueId, func() bool {
            //该函数会被2次调用,但是会排队执行
            return true
        }, func(err error) {
            //函数将在服务协程中执行
        })
    }

    //3.函数参数可以某中一个为空
    //参数二函数将被延迟执行
    slf.AsyncDo(nil, func(err error) {
        //将在下
    })

    //参数一函数在协程池中执行,但没有在服务协程中回调
    slf.AsyncDo(func() bool {
        return true
    }, nil)

    //4.函数返回值控制不进行回调
    slf.AsyncDo(func() bool {
        //返回false时,参数二函数将不会被执行; 为true时,则会被执行
        return false
    }, func(err error) {
        //该函数将不会被执行
    })
}

第七章:服务发现

origin引擎默认使用读取所有结点配置的进行确认结点有哪些Service。引擎也支持动态服务发现的方式,支持etcd与origin类型,具体请参照【配置说明】部分,Node结点可以配置只发现某些服务,如下示例:

{
    "NodeList": [{
        "NodeId": "nodeid_test",
        "ListenAddr": "127.0.0.1:8801",
        "MaxRpcParamLen": 409600,
        "Private": false,
        "remark": "//以_打头的,表示只在本机进程,不对整个子网开发",
        "ServiceList": ["_TestService1", "TestService9", "TestService10"],
        "DiscoveryService": [
            {
                "MasterNodeId": "nodeid_1",
                "NetworkName":"networkname1"
                "DiscoveryService": ["TestService8"]
            }
        ]
    }]
}

DiscoveryService:在当前nodeid为nodeid_test的结点中,只发现 MasterNodeId为nodeid_1或NetworkName为networkname1网络中的TestService8服务。

注意:MasterNodeId与NetworkName只配置一个,分别在模式为origin或者etcd服务发现类型时。

第八章:HttpService使用

HttpService是origin引擎中系统实现的http服务,http接口中常用的GET,POST以及url路由处理。

simple_http/TestHttpService.go文件如下:

package simple_http

import (
    "fmt"
    "github.com/duanhf2012/origin/v2/node"
    "github.com/duanhf2012/origin/v2/service"
    "github.com/duanhf2012/origin/v2/sysservice"
    "net/http"
)

func init(){
    node.Setup(&sysservice.HttpService{})
    node.Setup(&TestHttpService{})
}

//新建自定义服务TestService1
type TestHttpService struct {
    service.Service
}

func (slf *TestHttpService) OnInit() error {
    //获取系统httpservice服务
    httpservice := node.GetService("HttpService").(*sysservice.HttpService)

    //新建并设置路由对象
    httpRouter := sysservice.NewHttpHttpRouter()
    httpservice.SetHttpRouter(httpRouter,slf.GetEventHandler())

    //GET方法,请求url:http://127.0.0.1:9402/get/query?nickname=boyce
    //并header中新增key为uid,value为1000的头,则用postman测试返回结果为:
    //head uid:1000, nickname:boyce
    httpRouter.GET("/get/query", slf.HttpGet)

    //POST方法 请求url:http://127.0.0.1:9402/post/query
    //返回结果为:{"msg":"hello world"}
    httpRouter.POST("/post/query", slf.HttpPost)

    //GET方式获取目录下的资源,http://127.0.0.1:port/img/head/a.jpg
    httpRouter.SetServeFile(sysservice.METHOD_GET,"/img/head/","d:/img")

    //如果配置"ManualStart": true配置为true,则使用以下方法进行开启http监听
    //httpservice.StartListen()
    return nil
}

func (slf *TestHttpService) HttpGet(session *sysservice.HttpSession){
    //从头中获取key为uid对应的值
    uid := session.GetHeader("uid")
    //从url参数中获取key为nickname对应的值
    nickname,_ := session.Query("nickname")
    //向body部分写入数据
    session.Write([]byte(fmt.Sprintf("head uid:%s, nickname:%s",uid,nickname)))
    //写入http状态
    session.WriteStatusCode(http.StatusOK)
    //完成返回
    session.Done()
}

type HttpRespone struct {
    Msg string `json:"msg"`
}

func (slf *TestHttpService) HttpPost(session *sysservice.HttpSession){
    //也可以采用直接返回数据对象方式,如下:
    session.WriteJsonDone(http.StatusOK,&HttpRespone{Msg: "hello world"})
}

注意,要在main.go中加入import _ "orginserver/simple_service",并且在config/cluster/cluster.json中的ServiceList加入服务。

第九章:TcpService服务使用

TcpService是origin引擎中系统实现的Tcp服务,可以支持自定义消息格式处理器。只要重新实现network.Processor接口。目前内置已经实现最常用的protobuf处理器。

simple_tcp/TestTcpService.go文件如下:

package simple_tcp

import (
    "fmt"
    "github.com/duanhf2012/origin/v2/network/processor"
    "github.com/duanhf2012/origin/v2/node"
    "github.com/duanhf2012/origin/v2/service"
    "github.com/duanhf2012/origin/v2/sysservice"
    "github.com/golang/protobuf/proto"
    "orginserver/simple_tcp/msgpb"
)

func init(){
    node.Setup(&sysservice.TcpService{})
    node.Setup(&TestTcpService{})
}

//新建自定义服务TestService1
type TestTcpService struct {
    service.Service
    processor *processor.PBProcessor
    tcpService *sysservice.TcpService
}

func (slf *TestTcpService) OnInit() error {
    //获取安装好了的TcpService对象
    slf.tcpService =  node.GetService("TcpService").(*sysservice.TcpService)

    //新建内置的protobuf处理器,您也可以自定义路由器,比如json,后续会补充
    slf.processor = processor.NewPBProcessor()

    //注册监听客户连接断开事件
    slf.processor.RegisterDisConnected(slf.OnDisconnected)
    //注册监听客户连接事件
    slf.processor.RegisterConnected(slf.OnConnected)
    //注册监听消息类型MsgType_MsgReq,并注册回调
    slf.processor.Register(uint16(msgpb.MsgType_MsgReq),&msgpb.Req{},slf.OnRequest)
    //将protobuf消息处理器设置到TcpService服务中
    slf.tcpService.SetProcessor(slf.processor,slf.GetEventHandler())

    return nil
}


func (slf *TestTcpService) OnConnected(clientid string){
    fmt.Printf("client id %s connected\n",clientid)
}


func (slf *TestTcpService) OnDisconnected(clientid string){
    fmt.Printf("client id %s disconnected\n",clientid)
}

func (slf *TestTcpService) OnRequest (clientid string,msg proto.Message){
    //解析客户端发过来的数据
    pReq := msg.(*msgpb.Req)
    //发送数据给客户端
    err := slf.tcpService.SendMsg(clientid,&msgpb.Req{
        Msg: proto.String(pReq.GetMsg()),
    })
    if err != nil {
        fmt.Printf("send msg is fail %+v!",err)
    }
}

第十章:其他系统模块介绍

  • sysservice/wsservice/:支持了WebSocket协议,使用方法与TcpService类似
  • sysservice/messagequeueservice/:自定义的消息队列
  • sysservice/rankservice/:排行榜服务,采用跳表数据结构实现
  • sysmodule/mysqlmodule/:对mysql数据库操作
  • sysmodule/redismodule/:对Redis数据进行操作
  • sysmodule/httpclientmodule/:Http客户端请求封装
  • sysmodule/ginmodule/:对gin模块的封装,支持服务协程处理
  • sysmodule/kafkamodule/:对kafka的封装
  • log/log.go:日志的封装,可以使用它构建对象记录业务文件日志
  • util:在该目录下,有常用的uuid,hash,md5,协程封装等工具库
  • https://github.com/duanhf2012/originservice: 其他扩展支持的服务可以在该工程上看到,目前支持firebase推送的封装。
  • https://github.com/duanhf2012/origingame: 基础游戏服务器的框架
  • etcd与nats开发环境搭建可以从https://github.com/duanhf2012/originserver_v2下的docker-compose获取

备注:

感觉不错请star, 谢谢!

欢迎加入origin服务器开发QQ交流群:168306674,有任何疑问我都会及时解答

提交bug及特性: https://github.com/duanhf2012/origin/issues

因服务器是由个人维护,如果这个项目对您有帮助,您可以点我进行捐赠,感谢! 特别感谢以下赞助网友:

咕咕兽
_
死磕代码
bp-li
阿正
大头
Now'C