mango(二):架构

  • Post author:
  • Post category:其他




项目地址

mango:

https://github.com/GoldBaby5511/mango.git


mango-client:

https://github.com/GoldBaby5511/mango-client.git


mango-user-center:

https://github.com/GoldBaby5511/mango-user-center.git


mango-admin:

https://github.com/GoldBaby5511/mango-admin.git


mango-admin-ui:

https://github.com/GoldBaby5511/mango-admin-ui.git



开发环境

  • Windows10专业版
  • Goland2021.2
  • go1.15.2



使用

  • 执行script\statup.bat脚本,会依次编译并启动logger.exe、center.exe、config.exe、gateway.exe、login.exe
  • 解压client,双击执行client.exe,单击“微信登录”,选择Test001



设计

​ 采用网状结构,除center服务于logger服务之外,其他所有服务两两互联。服务启动时先向center发送注册,注册成功后center将服务信息广播至所有已注册服务,同时将已注册服务基础信息下发至新注册服务,所有服务收到注册信息后进行两两互联,具体结构及流程如下。



服务连接关系

连接关系图



服务分类

  • Gateway:网关,与Client交互,转发Client消息
  • Center:中心服,负责服务注册与管理
  • Config:配置中心,所有服务配置,支持本地JSON文件以及携程Apollo配置中心(推荐该方式),配置修改实时下发至相关订阅服务
  • lobby:登陆服务,登陆验证,维护在线用户信息
  • property:财富服务,用户财富操作,如分数加减等
  • List:房间列表服务
  • Room:房间服务
  • Table:桌子服务,具体游戏实现
  • daemon:守护服务,执行管理端命令,开启/守护相关服务



基础属性

属性名 属性值
AppName 服务名称
AppType 服务类型
AppID 服务ID(全局唯一)
CenterAddr 中心服务地址
ListenOnAddr 本服务监听地址



目录结构

├─api

│ ├─center

│ ├─client

│ ├─config

│ ├─gameddz

│ ├─gate

│ ├─logger

│ ├─proto

│ ├─room

│ ├─table

│ └─types

├─build

│ └─package

├─cmd

│ ├─center

│ │ └─business

│ ├─config

│ │ ├─business

│ │ └─conf

│ ├─gateway

│ │ └─business

│ ├─logger

│ │ └─business

│ ├─login

│ │ └─business

│ ├─room

│ │ └─business

│ │ └─player

│ └─table

│ └─business

│ ├─player

│ └─table

│ └─ddz

├─configs

│ ├─center

│ ├─config

│ ├─gateway

│ ├─logger

│ └─login

├─examples

│ └─client

├─pkg

│ ├─chanrpc

│ ├─conf

│ │ └─apollo

│ ├─gate

│ ├─go

│ ├─log

│ ├─module

│ ├─network

│ │ ├─json

│ │ └─protobuf

│ ├─timer

│ └─util

├─scripts

└─third_party

└─agollo

/api : 所使用的proto目录

/build/package : Docker镜像脚本

/cmd :程序主干服务入口、业务实现、如center、gateway等等

/configs : 所有服务json配置文件

/examples : 一个示例客户端,unity编写

/pkg : 封装了所有服务共用基础框架

/scripts : 启动脚本,目前只有windows下

/third_party/agollo : 协程apollo配置中心,第三方包




启动流程

程序入口(以center为例)为gate包内start方法,需传入服务名称;导入主业务包并初始化,如下

package main

import (
	_ "xlddz/cmd/center/business"
	"xlddz/pkg/gate"
)

func main() {
	gate.Start("center")
}

business包init(),主要注册该服务需要处理的消息以及事件映射

import _ "xlddz/cmd/center/business"

func init() {
    //注册消息
	g.MsgRegister(&center.RegisterAppReq{}, n.CMDCenter, uint16(center.CMDID_Center_IDAppRegReq), handleRegisterAppReq)
	g.MsgRegister(&center.AppStateNotify{}, n.CMDCenter, uint16(center.CMDID_Center_IDAppState), handleAppStateNotify)
	g.MsgRegister(&center.AppPulseNotify{}, n.CMDCenter, uint16(center.CMDID_Center_IDPulseNotify), handleAppPulseNotify)
	g.MsgRegister(&center.AppOfflineReq{}, n.CMDCenter, uint16(center.CMDID_Center_IDAppOfflineReq), handleAppOfflineReq)
	g.MsgRegister(&center.AppUpdateReq{}, n.CMDCenter, uint16(center.CMDID_Center_IDAppUpdateReq), handleAppUpdateReq)
    //注册事件
	g.EventRegister(g.ConnectSuccess, connectSuccess)
	g.EventRegister(g.Disconnect, disconnect)
}

gate.Start方法内依次完成下列操作

  • 初始化logger
  • 初始化基础配置
  • 启动业务gogroutine
  • 启动网络IO协程
gate.Start("center")

func Start(appName string) {
	conf.AppInfo.AppName = appName
	// logger
	l, err := log.New(conf.AppInfo.AppName)
	if err != nil {
		panic(err)
	}
	log.Export(l)
	defer l.Close()

	//baseConfig
	conf.LoadBaseConfig()

	if conf.AppInfo.AppType == n.AppCenter {
		apollo.RegisterConfig("", conf.AppInfo.AppType, conf.AppInfo.AppID, nil)
	}

	wg.Add(2)
	go func() {
		Skeleton.Run()
		wg.Done()
	}()

	go func() {
		Run()
		wg.Done()
	}()

	// close
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, os.Kill)
	sig := <-c
	log.Info("主流程", "服务器关闭 (signal: %v)", sig)
	Stop()
}

gate.Run()会根据配置启动监听,并连接Center

func Run() {
	...
    //本服务监听地址
	var tcpServer *n.TCPServer
	if conf.AppInfo.ListenOnAddress != "" {
		tcpServer = new(n.TCPServer)
		tcpServer.Addr = conf.AppInfo.ListenOnAddress
		tcpServer.MaxConnNum = MaxConnNum
		tcpServer.PendingWriteNum = PendingWriteNum
		tcpServer.LenMsgLen = LenMsgLen
		tcpServer.MaxMsgLen = MaxMsgLen
		tcpServer.GetConfig = apollo.GetConfigAsInt64
		tcpServer.NewAgent = func(conn *n.TCPConn, agentId uint64) n.AgentClient {
			a := &agentClient{id: agentId, conn: conn, info: n.BaseAgentInfo{AgentType: n.NormalUser}}
			if agentChanRPC != nil {
				agentChanRPC.Go(ConnectSuccess, a, agentId)
			}
			return a
		}
	}

    //连接Center
	if conf.AppInfo.CenterAddr != "" && conf.AppInfo.AppType != n.AppCenter {
		newServerItem(n.BaseAgentInfo{AgentType: n.CommonServer, AppName: "center", AppType: n.AppCenter, ListenOnAddress: conf.AppInfo.CenterAddr}, true, PendingWriteNum)
	}
	...
}

与Center连接成功后发送注册,注册成功后center将服务信息广播至所有已注册服务,同时将已注册服务基础信息下发至新注册服务,所有服务收到注册信息后进行两两互联

func newServerItem(info n.BaseAgentInfo, autoReconnect bool, pendingWriteNum int) {
	...
	tcpClient.NewAgent = func(conn *n.TCPConn) n.AgentServer {
		...
		sendRegAppReq(a)
		...
		return a
	}
    ...
}

func (a *agentServer) Run() {
	for {
		...
		switch bm.Cmd.SubCmdID {
		case uint16(center.CMDID_Center_IDAppRegRsp):
			var m center.RegisterAppRsp
			_ = proto.Unmarshal(msgData, &m)

			if m.GetRegResult() == 0 {
				...
				if !(conf.AppInfo.AppType == m.GetAppType() && conf.AppInfo.AppID == m.GetAppId()) && !ok {
					if m.GetAppAddress() != "" {
                        	...
                        	//与其他服务进行两两互联
						newServerItem(info, false, 0)
					} else {
						log.Warning("agentServer", "没有地址?,%v,%v,%v,%v",
							m.GetAppName(), m.GetAppType(), m.GetAppId(), m.GetAppAddress())
					}
				}
                ...
			} else {
				log.Warning("agentServer", "注册失败,RouterId=%v,原因=%v", m.GetCenterId(), m.GetReregToken())
			}
			...
		}
	}
}

当与配置中心互联成功后向配置中心注册并获取本服务所有配置

func newServerItem(info n.BaseAgentInfo, autoReconnect bool, pendingWriteNum int) {
	...
	tcpClient.NewAgent = func(conn *n.TCPConn) n.AgentServer {
		...
        //在这里注册本服务的配置请求
		if n.AppConfig == info.AppType {
			apollo.SetNetAgent(a)
			apollo.RegisterConfig("", conf.AppInfo.AppType, conf.AppInfo.AppID, nil)
		}
		...
		return a
	}
    ...
}
func RegisterConfig(key string, reqAppType, reqAppId uint32, cb cbNotify) {
	...
	SendSubscribeReq(nsKey, false)
}

func SendSubscribeReq(k ConfKey, cancel bool) {
	...
	cmd := network.TCPCommand{MainCmdID: uint16(network.AppConfig), SubCmdID: uint16(config.CMDID_Config_IDApolloCfgReq)}
	bm := network.BaseMessage{MyMessage: &req, Cmd: cmd}
	netAgent.SendMessage(bm)
}

config服务受到配置请求后会查找已加载的配置,相应请求,相关逻辑处理在 /cmd/config/business/business.go内

func handleApolloCfgReq(args []interface{}) {
	b := args[n.DataIndex].(n.BaseMessage)
	m := (b.MyMessage).(*config.ApolloCfgReq)

	log.Debug("配置", "收到配置请求,AppType=%v,AppId=%v,KeyName=%v,SubAppType=%v,SubAppId=%v,Subscribe=%v",
		m.GetAppType(), m.GetAppId(), m.GetKeyName(), m.GetSubAppType(), m.GetSubAppId(), m.GetSubscribe())

	listerIndex := getListenerIndex(m.GetSubAppType(), m.GetSubAppId())
	if listerIndex < 0 {
		log.Warning("配置", "配置不存在,NameSpace=%v,KeyName=%v", m.GetNameSpace(), m.GetKeyName())
		return
	}

	listenerList[listerIndex].addSubscriptionItem(m.GetAppType(), m.GetAppId(), m.GetSubAppType(), m.GetSubAppId(), m.GetKeyName())
	if m.GetSubscribe()&uint32(config.ApolloCfgReq_NEED_RSP) != 0 {
		listenerList[listerIndex].notifySubscriptionList(m.GetKeyName())
	}
}

gate包在init的时候会注册一个配置中心相应,处理方法在handleApolloCfgRsp内

func init() {
	...
	MsgRegister(&config.ApolloCfgRsp{}, n.CMDConfig, uint16(config.CMDID_Config_IDApolloCfgRsp), handleApolloCfgRsp)
}

当请求配置消息得到响应后,会通过gate在init内的注册将相应路由到handleApolloCfgRsp内

func (a *agentClient) Run() {
	for {
		...
		//解析并路由消息
		cmd, msg, err = processor.Unmarshal(unmarshalCmd.MainCmdID, unmarshalCmd.SubCmdID, msgData)
		if err != nil {
			log.Error("agentClient", "unmarshal message,headCmd=%v,error: %v", bm.Cmd, err)
			continue
		}
		err = processor.Route(n.BaseMessage{MyMessage: msg, TraceId: bm.TraceId}, a, cmd, dataReq)
		if err != nil {
			log.Error("agentClient", "client agentClient route message error: %v,cmd=%v", err, cmd)
			continue
		}
	}
}

func handleApolloCfgRsp(args []interface{}) {
    //将消息丢到apollo包内,存储相关配置
	apollo.ProcessConfigRsp(args[n.DataIndex].(n.BaseMessage).MyMessage.(*config.ApolloCfgRsp))

    //获取日志服地址进行连接
	logAddr := apollo.GetConfig("日志服务器地址", "")
	if logAddr != "" && tcpLog != nil && !tcpLog.IsRunning() {
		ConnectLogServer(logAddr)
	}

    //若业务层需要则将配置抛到业务层
	go func() {
		if agentChanRPC != nil {
			agentChanRPC.Call0(ConfigChangeNotify)
		}
	}()
}

一般服务当与日志服连接成功后,则认为服务启动流程就结束了,但是这个没有统一标准,比如一些服务是需要根据配置响应做一些业务上的初始化后才算完成,比如room、table之类就需要获取桌子或初始化桌子等

func ConnectLogServer(logAddr string) {
	...
	if conf.AppInfo.AppType != n.AppLogger && logAddr != "" && tcpLog != nil && !tcpLog.IsRunning() {
		...
		tcpLog.NewAgent = func(conn *n.TCPConn) n.AgentServer {
			...
			log.Info("gate", "日志服务器连接成功,服务启动完成,阔以开始了... ...")
			...
			return a
		}

		tcpLog.Start()
	}
}

项目地址:

https://github.com/GoldBaby5511/mango.git

,由于本人能力有限,如有好建议请不吝赐教,若觉得有参考价值还望不吝点亮小星星。

相关博客

mango(一):杂谈项目由来:

https://blog.csdn.net/weixin_42780662/article/details/122006434




参考引用

  • leaf:https://github.com/name5566/leaf.git
  • agollo:https://github.com/apolloconfig/agollo.git
  • fsnotify:https://github.com/fsnotify/fsnotify.git
  • proto:https://github.com/protocolbuffers/protobuf.git
  • project-layout:https://github.com/golang-standards/project-layout.git



交流群



版权声明:本文为weixin_42780662原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。