引言
在该项目中有两个部分,一个是zinx,一个是mmo_game,zinx是一个tpc框架,而mmo_game是一个基于此框架设计的一个小游戏。
该项目的教学视频:https://www.bilibili.com/video/BV1wE411d7th?spm_id_from=333.337.search-card.all.click
项目的实现:https://github.com/1345414527/zinx
zinx
项目结构

server
server对外提供一个NewServer()方法,用于创建服务器,服务器的一些配置信息在zinx.json中配置,并由globalobj读取, 服务器创建的时候调用globalobj中的属性。
调用server()方法启动服务器,①首先会启动线程池 ②解析一个tcp的addr ③根据addr获取监听器 ④监听链接请求,当有客户端链接时,获取链接,判断链接是否超过最大个数,没超过则创建自定义链接,并启动链接,否则就关闭链接
server中管理着链接管理器,消息处理模块以及两个钩子函数。我们可以通过AddRouter()方法设置指定信息格式的处理方法,还可以设置链接建立后和销毁前的钩子函数。
1 | //iServer的接口实现,定义一个Server的服务器模块 |
1 | /** |
connection
当创建一个链接并启动后,会创建读和写协程,并调用链接创建的钩子函数。
读协程监听客户端发来的数据,获取数据对数据进行解封成一个message,因为使用的是TLV封包方法,因此先获取标志和长度,再通过长度长度获取数据封装进message中。然后将message封装到一个request中,交给线程池处理。
写协程会监听一个data channel和一个exit channel,当data channel有数据时,会写给客户端,当exit channel有数据时,则退出。
对外提供了一个SendMsg方法,当要发送数据给客户端时,调用该方法,对传入的msgid和数据组成一个message并进行封包,传入data channel中供写协程获取传输。
在构建我们的钩子函数时,可以设置一些property和该conn进行绑定。
1 | package znet |
msgHandler
主要用于对不同的消息进行处理。内部保存了一个Apis map集合,保存了不同消息的处理方法。
在启动服务器时会创建一个线程池,即创建一个消息队列和多个协程,协程监听消息队列,当某一个协程监听到request时,会根据request中message对应msgId获取不同的router,然后依次调用router中的preHandle、handle、postHandle进行处理。
1 | /** |
router
采用适配器模式,定义一个空的BaseRouter结构体,自定义Router可以继承该结构体,重写任意方法。
1 | type BaseRouter struct { |
message
我们的封包采用的是TLV的格式,因此对于每一个消息都包含三个属性。
- Id:用于指明当前消息的类型,对于不同的类型处理方法不同
- DataLen:用于指明当前消息数据的长度,用于防止粘包问题
- Data:用于保存具体的数据
1 | type Message struct { |
request
用于封装我们的链接和数据。每个request对应一个任务,放入线程池中供协程进行处理。
1 | type Request struct { |
connmanager
该模块用于管理链接。当创建链接后,将链接加入到链接管理器中,当销毁链接前,将链接移除。
主要用于控制该服务器中该链接的数量。当服务器监听到客户端的链接请求后,会判断当前链接数是否超过设置的最大链接数,若没有则创建,若超过了则拒绝。
1 | type ConnManager struct { |
datapack
该模块没有任何属性,主要用于进行message的封包和拆包。
1 | type DataPack struct { |
globalobj
主要用于初始化全局配置
1 | type GlobalObj struct { |
zinx.json:
1 | { |
服务器创建
在我们创建服务器时,可以自定义不同的Router、Hook函数和链接属性。
1 | //继承BaseRouter |
客户端创建
1 | func main() { |
流程分析

- 当服务器创建后,会一直监听客户端连接请求。当有请求时,且没超过最大连接数,则创建链接,并放入链接管理器中。
- 链接启动后会执行链接创建后的钩子函数,并创建读、写协程。读协程监听的链接请求后,会对数据进行拆封成message,因为使用的是TLV封包方法,因此先获取标志和长度,再通过长度长度获取数据封装进message中。然后将message封装到一个request中,交给线程池处理。
- request进入channel中,被其中一个协程获取,根据msgId获取相应的router进行处理,如果要返回给客户端信息,则调用connection的sendMsg方法,根据msgId和data进行封包交给写协程,再传给客户端。
MMO
游戏介绍
该游戏主要基于AOI算法和zinx进行设计,客户端和服务器的消息通过protobuf格式进行传输。主要实现:
- 玩家的上线、下线。
- 世界聊天系统
- 多人位置同步
- 移动位置同步
服务器要发送消息,流程:数据 =>编码成protobuf格式=>将msgId和编码后的数据组成message,并进行封包,再传递给写协程进程发送
玩家的上线
玩家上线,就是和服务器建立了一个链接,在链接建立后的钩子函数中进行初始化操作:
- 创建一个player对象,会随机初始化玩家的位置
- 向客户端发送一个消息,同步当前玩家的id;
- 向客户端发送一个消息,同步当前玩家的位置;
- 在WorldManager中添加当前玩家,并在当前格子中添加玩家
- 通过当前玩家的位置获取九宫格,将当前玩家的位置发送给九宫格里的玩家,其它玩家就可以看到当前玩家;将九宫格里的玩家的位置发送给当前玩家,当前玩家就可以看到其他玩家。
世界聊天系统
当客户端发送来数据,读协程监听的链接请求后,会对数据进行拆封成message,然后将message封装到一个request中,交给线程池处理。request进入channel中,被其中一个协程获取,根据msgId获取相应的router进行处理。此router将request中的protobuf格式的数据解码,并通过connection获取pid属性,再通过pid属性在WorldManager中获取当前player对象,通过该对象将数据发送给所有的玩家。
多人位置同步
就是在玩家上线的时候,通过当前玩家的位置获取九宫格,将当前玩家的位置发送给九宫格里的玩家,其它玩家就可以看到当前玩家;将九宫格里的玩家的位置发送给当前玩家,当前玩家就可以看到其他玩家。
移动位置同步
当客户端发送来数据,读协程监听的链接请求后,会对数据进行拆封成message,然后将message封装到一个request中,交给线程池处理。request进入channel中,被其中一个协程获取,根据msgId获取相应的router进行处理。此router将request中的protobuf格式的数据解码,获取到当前玩家的位置,然后将当前的位置信息发送给九宫格里的其他玩家。
玩家下线
玩家下线,就是和服务器断开链接,在链接销毁后的钩子函数中进行后序操作:
向九宫格内的玩家广播玩家下线消息
在当前格子grid中删除当前玩家
在WorldManager中删除一个玩家
项目结构

main
在main中,会创建游戏服务器。设置两个Hook函数用于玩家的上线和下线,两个router用于处理玩家的世界聊天和移动位置的同步。
1 | /** |
aoi
aoi主要用来初始化并管理格子grid,并定义地图的边界和两个维度的格子数量。可以通过玩家坐标获取当前所在格子,也可以获取当前格子周围的九宫格。
1 | /* |
grid
存储每个格子的信息,保存了当前格子中在线玩家
1 | /** |
player
存储每个玩家的信息,进行了上、下线处理,发送数据,同步玩家位置,广播数据等操作都是在这里进行处理。
1 | type Player struct { |
worldmanager
世界管理模块,在引用该包的时候就会初始化。存储了当前游戏的AOI管理模块以及所有的在线玩家。
1 | type WorldManager struct { |
router
move
主要用于服务器对玩家位置变更信息的处理
1 | type MoveApi struct { |
world_chat
主要用于世界聊天的处理
1 | type WorldChatApi struct { |
msg.proto
1 | syntax = "proto3"; //指定版本信息,不指定会报错 |