代码

本人代码仓库

官方代码仓库

官方教程

官方文档

引言

Golang目前在服务器的应用框架很多,但是应用在游戏领域或者其他长链接的领域的轻量级企业框架甚少

TCP服务器

Zinx-V0.1-基础Server

服务端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//启动服务器方法
Start()
//停止服务器方法
Stop()
//开启业务服务方法 
//TODO Server.Serve() 是否在启动服务的时候 还要处理其他的事情呢 可以在这个方法里添加添加
Serve()

服务器获取客户端的连接步骤
//1 获取一个TCP的Addr
addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
//2 监听服务器地址
listenner, err:= net.ListenTCP(s.IPVersion, addr)
//3.1 阻塞等待客户端建立连接请求
conn, err := listenner.AcceptTCP()

客户端

1
2
//发送一个请求
conn,err := net.Dial("tcp", "127.0.0.1:7777")

交互

1
2
3
4
5
6
7
8
9
//客户端
_, err := conn.Write([]byte("hello ZINX"))
buf :=make([]byte, 512)
cnt, err := conn.Read(buf)

//服务端
buf := make([]byte, 512)
cnt, err := conn.Read(buf)
err := conn.Write(buf[:cnt])

链接建立成功后,客户端写了又读,服务端读了又写,闭环

Zinx-V0.2-简单的连接封装与业务绑定

Zinx-V0.1我们只知道连接写读数据,不知道客户端的地址,连接没有id不便管理,于是我们需要把连接封装起来

并给连接绑定一个api方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//定义连接接口
type IConnection interface {
	//启动连接,让当前连接开始工作
	Start()
	//停止连接,结束当前连接状态M
	Stop()
	//从当前连接获取原始的socket TCPConn
	GetTCPConnection() *net.TCPConn
	//获取当前连接ID
	GetConnID() uint32
	//获取远程客户端地址信息
	RemoteAddr() net.Addr
}

//定义一个统一处理链接业务的接口
type HandFunc func(*net.TCPConn, []byte, int) error


type Connection struct {
	//当前连接的socket TCP套接字
	Conn *net.TCPConn
	//当前连接的ID 也可以称作为SessionID,ID全局唯一
	ConnID uint32
	//当前连接的关闭状态
	isClosed bool

	//该连接的处理方法api
	handleAPI ziface.HandFunc

	//告知该链接已经退出/停止的channel
	ExitBuffChan chan bool
}

//3.3 处理该新连接请求的 业务 方法, 此时应该有 handler 和 conn是绑定的
dealConn := NewConntion(conn, cid, CallBackToClient)
cid ++

//3.4 启动当前链接的处理业务
go dealConn.Start()

总来来说就是一个api方法和连接绑定,一旦有客户端连接,就新建一个连接、api和这个客户端绑定,并启动这个连接来干业务

Zinx-V0.3-集成简单路由功能

底层再往上面跑一点,把客户端请求的连接信息 和 请求的数据,放在一个叫Request的请求类里,这样的好处是我们可以从Request里得到全部客户端的请求信息

这里面有个非常好的设计模式,使用基类,并且基类的方法都为空,然后子类继承这个基类,子类就只需要重写需要的方法。

.\Server.go:44:14: cannot use &PingRouter{} (value of type *PingRouter) as type ziface.IRouter in argument to s.AddRouter: *PingRouter does not implement ziface.IRouter (missing PreHandle method)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
	路由接口, 这里面路由是 使用框架者给该链接自定的 处理业务方法
	路由里的IRequest 则包含用该链接的链接信息和该链接的请求数据信息
*/
type IRouter interface{
	PreHandle(request IRequest)  //在处理conn业务之前的钩子方法
	Handle(request IRequest)	 //处理conn业务的方法
	PostHandle(request IRequest) //处理conn业务之后的钩子方法
}

//实现router时,先嵌入这个基类,然后根据需要对这个基类的方法进行重写
type BaseRouter struct {}

//这里之所以BaseRouter的方法都为空,
// 是因为有的Router不希望有PreHandle或PostHandle
// 所以Router全部继承BaseRouter的好处是,不需要实现PreHandle和PostHandle也可以实例化
func (br *BaseRouter)PreHandle(req ziface.IRequest){}
func (br *BaseRouter)Handle(req ziface.IRequest){}
func (br *BaseRouter)PostHandle(req ziface.IRequest){}

//3.3 处理该新连接请求的 业务 方法, 此时应该有 handler 和 conn是绑定的
dealConn := NewConntion(conn, cid, s.Router)
cid ++

//3.4 启动当前链接的处理业务
go dealConn.Start()

总来来说就是一个路由和连接绑定,一旦有客户端连接,就新建一个连接、路由和这个客户端绑定,并启动这个连接来干业务

Zinx-V0.4增添全局配置代码实现

定义一个全局json文件,定义一个结构体来接受json。简单

Zinx-V0.5消息封装

我们把服务器的全部数据都放在一个Request里,很明显,现在是用一个[]byte来接受全部数据,又没有长度,又没有消息类型,这不科学。怎么办呢?我们现在就要自定义一种消息类型,把全部的消息都放在这种消息类型里。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type IMessage interface {
	GetDataLen() uint32	//获取消息数据段长度
	GetMsgId() uint32	//获取消息ID
	GetData() []byte	//获取消息内容

	SetMsgId(uint32)	//设计消息ID
	SetData([]byte)		//设计消息内容
	SetDataLen(uint32)	//设置消息数据段长度
}
type Message struct {
	Id      uint32 //消息的ID
	DataLen uint32 //消息的长度
	Data    []byte //消息的内容
}

我们这里就是采用经典的TLV(Type-Len-Value)封包格式来解决TCP粘包问题吧。

1
2
//类型断言 接口转为
msg := msgHead.(*znet.Message) 

https://vimsky.com/examples/usage/golang_encoding_binary_Write.html

https://vimsky.com/examples/usage/golang_encoding_binary_Read.html

Zinx的多路由模式

之前的Zinx好像只能绑定一个路由的处理业务方法。显然这是无法满足基本的服务器需求的,那么现在我们要在之前的基础上,给Zinx添加多路由的方式。

既然是多路由的模式,我们这里就需要给MsgId和对应的处理逻辑进行捆绑。所以我们需要一个Map。

1
2
3
4
5
6
7
Apis map[uint32] ziface.IRouter

//路由功能:给当前服务注册一个路由业务方法,供客户端链接处理使用
AddRouter(router IRouter)  ---->  AddRouter(msgId uint32, router IRouter)

//int 转string
strconv.Itoa(int(msgId)))

增加了MsgHandle

就是发送什么消息类型,就用什么路由处理

为了实现这个功能,首先加了一个IMessage接口来实现消息

IDataPack来实现封包和拆包

IMsgHandle来使消息和路由绑定

Zinx的读写分离模型

好了,接下来我们就要对Zinx做一个小小的改变,就是与客户端进修数据交互的Gouroutine由一个变成两个,一个专门负责从客户端读取数据,一个专门负责向客户端写数据。这么设计有什么好处,当然是目的就是高内聚,模块的功能单一,对于我们今后扩展功能更加方便。

读和写分别用一个go协程

Zinx的消息队列及多任务机制

我们可以通过worker的数量来限定处理业务的固定goroutine数量,而不是无限制的开辟Goroutine,虽然我们知道go的调度算法已经做的很极致了,但是大数量的Goroutine依然会带来一些不必要的环境切换成本,这些本应该是服务器应该节省掉的成本。我们可以用消息队列来缓冲worker工作的数据。

之前是没来一个conn 就go c.MsgHandler.DoMsgHandler(&req)

现在是c.MsgHandler.SendMsgToTaskQueue(&req)将请求交给消息队列处理。

Zinx的链接管理

现在我们要为Zinx框架增加链接个数的限定,如果超过一定量的客户端个数,Zinx为了保证后端的及时响应,而拒绝链接请求。

我们之前给Connection提供了一个发消息的方法SendMsg(),这个是将数据发送到一个无缓冲的channel中msgChan。但是如果客户端链接比较多的话,如果对方处理不及时,可能会出现短暂的阻塞现象,我们可以做一个提供一定缓冲的发消息方法,做一些非阻塞的发送体验。

MMO游戏

大型多人在线游戏