本人代码仓库
官方代码仓库
官方教程
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游戏#
大型多人在线游戏