Day4:Zinx 的多路由模式
之前我们已经为 Zinx 框架配置了路由,但存在的一个问题就是我们只为每一个 Server 绑定了一个单一的路由,而我们希望的是服务器可以根据不同的消息来进行不同的业务处理,绑定单一路由显然不能满足我们的需求,所以今天我们的第一项任务是实现 Zinx 的多路由模式。
创建消息管理模块
首先我们在 ziface 下创建 imsghandler.go
文件,并为 message handler 定义一个包含其指定方法的接口:
package ziface
type IMsgHandle interface {
DoMsgHandler(request IRequest) // 立即以非阻塞的方式处理消息
AddRouter(msgId uint32, router IRouter) // 为消息添加具体的处理逻辑
}
之后我们在 znet 下创建 msghandler.go
并实现接口。
首先让我们定义一个根据消息的属性绑定处理消息的具体业务的数据结构。
回忆之前我们定义的消息类,它由 Id、数据长度和具体的数据组成,但是 Id 好像我们并没有用到,实际上之前加入的 Id 就是为了在多路由模块实现消息与业务绑定的功能。具体来说,根据指定的 MsgId,我们调用与 Id 匹配的 Handler 进行业务处理。根据上述分析,我们应该使用 map 类型来保存消息与业务处理的映射关系。
MsgHandle 类型的定义如下,它实现了 IMsgHandle 接口:
type MsgHandle struct {
Apis map[uint32]ziface.IRouter // map 存放每个 MsgId 对应的处理方法
}
之后我们实现接口对应的方法,即“执行业务处理”以及“添加路由”:
// 立即以非阻塞的方式处理消息, 当然这里的非阻塞需要调用者手动使用 go MsgHandle.DoMsgHandler
func (mh *MsgHandle) DoMsgHandler(request ziface.IRequest) {
handler, ok := mh.Apis[request.GetMsgID()]
if !ok {
fmt.Println("api msgId = ", request.GetMsgID(), " is not FOUND!")
return
}
// 执行 Router 的 Handler
handler.PreHandle(request)
handler.Handle(request)
handler.PostHandle(request)
}
// 为某条消息添加具体的处理逻辑
func (mh *MsgHandle) AddRouter(msgId uint32, router ziface.IRouter) {
// 判断当前 msg 绑定的 API 处理方法是否已经存在
if _, ok := mh.Apis[msgId]; ok {
panic("repeated api, msgId = " + strconv.Itoa(int(msgId)))
}
// 添加 msg 与 api 的绑定关系
mh.Apis[msgId] = router
fmt.Println("Add api msgId = ", msgId)
}
最后为它添加一个工厂函数:
func NewMsgHandle() *MsgHandle {
return &MsgHandle{
Apis: make(map[uint32]ziface.IRouter),
}
}
Zinx-v0.6 代码实现
现在我们将多路由模式集成到 Zinx 框架当中,具体来说,我们需要首先对 Server 进行修改。回忆一下,之前我们在定义 Server 的结构时,显式地将 Router 作为属性硬编码到了 Server 结构当中,没有多路由模块对 router 进行管理,带来的后果就是一个 Server 只能显式地与一个 Router 相绑定,形成所谓的单路由模式。
首先,我们修改 Server 的接口 IServer,具体来说是修改 AddRouter 这个方法,使得 Server 在添加路由的时候可以将 MsgId 和 Router 绑定(其实也就是调用 MsgHandle 的 AddRouter 方法):
package ziface
// 定义服务器接口
type IServer interface {
Start() // Start 启动服务器方法
Stop() // Stop 停止服务器方法
Serve() // Serve 开启服务器方法
AddRouter(msgId uint32, router IRouter) // 路由功能: 给当前服务注册一个路由业务方法
}
现在我希望 Server 可以根据不同的 MsgId 执行不同的业务,因此将 Router 替换为 MsgHandle:
type Server struct {
Name string // Name 为服务器的名称
IPVersion string // IPVersion: IPv4 or other
IP string // IP: 服务器绑定的 IP 地址
Port int // Port: 服务器绑定的端口
msgHandler ziface.IMsgHandle // 将 Router 替换为 MsgHandler, 绑定 MsgId 与对应的处理方法
}
根据 Server 结构的修改,更正 Server 的工厂函数:
// NewServer 将创建一个服务器的 Handler
func NewServer() ziface.IServer {
s := &Server{
Name: settings.Conf.Name,
IPVersion: "tcp4",
IP: settings.Conf.Host,
Port: settings.Conf.Port,
msgHandler: NewMsgHandle(),
}
return s
}
重写 AddRouter 方法:
func (s *Server) AddRouter(msgId uint32, router ziface.IRouter) {
s.msgHandler.AddRouter(msgId, router)
fmt.Println("Add Router succ! msgId = ", msgId)
}
最后,修改 Start 方法的逻辑。具体来说,在 Start 方法当中,Server 会开启服务,并在指定的 IP + Port 监听连接请求,连接成功后,接收 Client 发送的消息。收到消息之后,Server 要做的就是实例化一个 Connection 结构,并在其中通过 Router 处理具体的业务逻辑,之前我们在实例化 Connection 对象时,进行的是:
... ... ...
dealConn := NewConnection(conn, cid, s.Router)
... ... ...
显然,一个 Client 可能发送过来多种 MsgId 的消息,应该把 MsgHandle 传给 Connection,让其在 StartReader 当中根据 MsgId 处理具体的业务。因此我们将上一行修改为:
... ... ...
dealConn := NewConnection(conn, cid, s.msgHandler)
... ... ...
现在我们来修改 Connection,先修改它的结构,Router 变为了 MsgHandler:
type Connection struct {
Conn *net.TCPConn // 当前连接的 socket TCP 套接字
ConnID uint32 // 当前连接的 ID, 也可称为 SessionID, 全局唯一
isClosed bool // 当前连接的开启/关闭状态
Msghandler ziface.IMsgHandle // 将 Router 替换为消息管理模块
ExitBuffChan chan bool // 告知该连接一经推出/停止的 channel
}
修改工厂函数:
func NewConnection(conn *net.TCPConn, connID uint32, msgHandler ziface.IMsgHandle) *Connection {
c := &Connection{
Conn: conn,
ConnID: connID,
isClosed: false,
Msghandler: msgHandler,
ExitBuffChan: make(chan bool, 1),
}
return c
}
最后修改 StartReader 即可,具体要修改的位置就是 StartReader 最后调用 Handle 的地方。回忆我们刚刚编写的 MsgHandle 的 DoMsgHandler 方法,它以 request 作为输入,而 request 结构中封装了 conn 和 msg,msg 当中包含 MsgId,所以根据输入我们可以直接解析出 MsgId 并找到对应的 Handler,因此直接在 StartReader 最后调用 MsgHandler 的 DoMsghandler 方法即可:
... ... ...
// 从路由 Routers 中找到注册绑定 Conn 的对应 Handle
go c.Msghandler.DoMsgHandler(&req)
... ... ...
使用 Zinx-v0.6 完成应用程序
首先启动 Server:
// zinx/server/main.go
package main
import (
"fmt"
"zinx/settings"
"zinx/ziface"
"zinx/znet"
)
// ping test 自定义路由
type PingRouter struct {
znet.BaseRouter
}
// Test Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
fmt.Println("Call PingRouter Handle")
//先读取客户端的数据,再回写ping...ping...ping
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
//回写数据
err := request.GetConnection().SendMsg(1, []byte("ping...ping...ping"))
if err != nil {
fmt.Println(err)
}
}
// HelloZinxRouter Handle
type HelloZinxRouter struct {
znet.BaseRouter
}
func (this *HelloZinxRouter) Handle(request ziface.IRequest) {
fmt.Println("Call HelloZinxRouter Handle")
//先读取客户端的数据,再回写ping...ping...ping
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
err := request.GetConnection().SendMsg(1, []byte("Hello Zinx Router V0.6"))
if err != nil {
fmt.Println(err)
}
}
func main() {
// 首先初始化
err := settings.Init()
if err != nil {
return
}
// 创建一个server句柄
s := znet.NewServer()
//配置路由
s.AddRouter(0, &PingRouter{})
s.AddRouter(1, &HelloZinxRouter{})
//开启服务
s.Serve()
}
我们设置两个路由来测试 Zinx 的多路由模式。
之后,我们启动两个 Client:
// zinx/client/main.go
package main
import (
"fmt"
"io"
"net"
"time"
"zinx/znet"
)
/*
模拟客户端
*/
func main() {
fmt.Println("Client Test ... start")
// 3 秒之后发起测试请求,给服务端开启服务的机会
time.Sleep(3 * time.Second)
conn, err := net.Dial("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("client start err, exit!")
return
}
for {
//发封包message消息
dp := znet.NewDataPack()
msg, _ := dp.Pack(znet.NewMsgPackage(0, []byte("Zinx V0.5 Client Test Message")))
_, err := conn.Write(msg)
if err != nil {
fmt.Println("write error err ", err)
return
}
//先读出流中的head部分
headData := make([]byte, dp.GetHeadLen())
_, err = io.ReadFull(conn, headData) //ReadFull 会把msg填充满为止
if err != nil {
fmt.Println("read head error")
break
}
//将headData字节流 拆包到msg中
msgHead, err := dp.Unpack(headData)
if err != nil {
fmt.Println("server unpack err:", err)
return
}
if msgHead.GetDataLen() > 0 {
//msg 是有data数据的,需要再次读取data数据
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())
//根据dataLen从io中读取字节流
_, err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println("server unpack data err:", err)
return
}
fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
}
time.Sleep(1 * time.Second)
}
}
// zinx/another_client/main.go
package main
import (
"fmt"
"io"
"net"
"time"
"zinx/znet"
)
/*
模拟客户端
*/
func main() {
fmt.Println("Client Test ... start")
//3秒之后发起测试请求,给服务端开启服务的机会
time.Sleep(3 * time.Second)
conn, err := net.Dial("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("client start err, exit!")
return
}
for {
//发封包message消息
dp := znet.NewDataPack()
msg, _ := dp.Pack(znet.NewMsgPackage(1, []byte("Zinx V0.6 Client1 Test Message")))
_, err := conn.Write(msg)
if err != nil {
fmt.Println("write error err ", err)
return
}
//先读出流中的head部分
headData := make([]byte, dp.GetHeadLen())
_, err = io.ReadFull(conn, headData) //ReadFull 会把msg填充满为止
if err != nil {
fmt.Println("read head error")
break
}
//将headData字节流 拆包到msg中
msgHead, err := dp.Unpack(headData)
if err != nil {
fmt.Println("server unpack err:", err)
return
}
if msgHead.GetDataLen() > 0 {
//msg 是有data数据的,需要再次读取data数据
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())
//根据dataLen从io中读取字节流
_, err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println("server unpack data err:", err)
return
}
fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
}
time.Sleep(1 * time.Second)
}
}
先启动 Server,再启动两个 client,Server 的 Terminal 显示的信息如下:
Add api msgId = 0
Add Router succ! msgId = 0
Add api msgId = 1
Add Router succ! msgId = 1
[START] Server listenner at IP: 127.0.0.1, Port 7777, is starting
start Zinx server zinx server succ, now listenning...
Reader Goroutine is running
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Reader Goroutine is running
Call HelloZinxRouter Handle
recv from client : msgId= 1 , data= Zinx V0.6 Client1 Test Message
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.5 Client Test Message
Call HelloZinxRouter Handle
recv from client : msgId= 1 , data= Zinx V0.6 Client1 Test Message
... ... ...
Client1:
Client Test ... start
==> Recv Msg: ID= 1 , len= 22 , data= Hello Zinx Router V0.6
==> Recv Msg: ID= 1 , len= 22 , data= Hello Zinx Router V0.6
==> Recv Msg: ID= 1 , len= 22 , data= Hello Zinx Router V0.6
==> Recv Msg: ID= 1 , len= 22 , data= Hello Zinx Router V0.6
==> Recv Msg: ID= 1 , len= 22 , data= Hello Zinx Router V0.6
... ... ...
Client2:
Client Test ... start
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 1 , len= 18 , data= ping...ping...ping
说明 Zinx 已经成功集成了多路由模式。