6.设计和实施Zinx多路由器模式
#go #zinx

<1.Building Basic Services with Zinx Framework>
<2. Zinx-V0.2 Simple Connection Encapsulation and Binding with Business>
<3.Design and Implementation of the Zinx Framework's Routing Module>
<4.Zinx Global Configuration>
<5.Zinx Message Encapsulation Module Design and Implementation>
<6.Design and Implementation of Zinx Multi-Router Mode>
<7. Building Zinx's Read-Write Separation Model>


在Zinx V0.5中,已配置了路由模式功能,但它只能绑定单个路由处理程序方法。显然,这无法满足服务器的基本要求。现在,我们需要根据先前的实现将多路由器模式添加到Zinx。

多路由器模式是什么?这意味着我们需要将MSGID与其相应的处理逻辑相关联。该关联需要存储在地图数据结构中,定义如下:

Apis map[uint32] ziface.IRouter

地图的密钥是UINT32类型的关键,并存储每种类型的消息的ID。该值是IRouter路由业务的抽象层,该层应为用户覆盖的手柄方法。重要的是要注意,仍然不建议在此处存储特定的实现层路由器类型。原因是设计模块仍然基于抽象层设计。该地图的名称是API。

6.1创建消息管理模块

在本节中,我们将定义一个消息管理模块,以维护API地图中的绑定关系。

6.1.1为消息管理模块创建抽象类

zinx/ziface目录中创建一个名为imsghandler.go的文件。该文件定义了用于消息管理的抽象层接口。消息管理的接口命名为IMsgHandle,定义如下:

//zinx/ziface/imsghandler.go

package ziface

/*
   Abstract layer for message management
*/
type IMsgHandle interface {
   DoMsgHandler(request IRequest)             // Process messages immediately in a non-blocking manner
   AddRouter(msgId uint32, router IRouter)    // Add specific handling logic for a message
}

该接口内有两种方法。 AddRouter()用于在API地图上添加MSGID和路由关系。 DoMsgHandler()是一个接口,该接口调用路由器中的特定句柄()方法。该参数是类型IRequest的,因为zinx已经将所有客户端消息请求放入具有所有相关消息属性的IRequest中。

使用原始的Markdown格式翻译成英文。

6.1.2消息管理模块的实现

在zinx/znet目录中创建一个名为msghandler.go的文件。该文件包含IMSGHANDLE接口的实现代码。具体实现如下:

//zinx/znet/msghandler.go

package znet

import (
    "fmt"
    "strconv"
    "zinx/ziface"
)

type MsgHandle struct {
    Apis map[uint32]ziface.IRouter // Map to store the handler methods for each MsgID
}

MsgHandle struct具有一个Apis属性,该属性是将MSGID绑定到路由器的地图。接下来,我们为MsgHandle提供构造方法。实现代码如下:

//zinx/znet/msghandler.go

func NewMsgHandle() *MsgHandle {
    return &MsgHandle{
        Apis: make(map[uint32]ziface.IRouter),
    }
}

在Golang中,初始化地图需要使用make关键字来分配空间。请注意。 MsgHandle结构需要实现IMSGHANDLE:DoMsgHandler()AddRouter()的两个接口方法。具体实现如下:

//zinx/znet/msghandler.go

// Process messages in a non-blocking manner
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
    }

    // Execute the corresponding handler methods
    handler.PreHandle(request)
    handler.Handle(request)
    handler.PostHandle(request)
}

// Add specific handling logic for a message
func (mh *MsgHandle) AddRouter(msgId uint32, router ziface.IRouter) {
    // 1. Check if the current msg's API handler method already exists
    if _, ok := mh.Apis[msgId]; ok {
        panic("repeated api, msgId = " + strconv.Itoa(int(msgId)))
    }
    // 2. Add the binding relationship between msg and api
    mh.Apis[msgId] = router
    fmt.Println("Add api msgId =", msgId)
}

DoMsgHandler()方法由两个步骤组成。首先,它从输入参数request检索msgid,并使用API​​映射获取相应的路由器。如果找不到匹配项,则指示未识别的消息,并且开发人员需要为此类型的消息注册回调业务路由器。其次,在获得路由器后,它在模板顺序中顺序执行注册的PreHandle()Handle()PostHandle()方法。执行这三个方法后,消息处理完成。

现在已经设计了消息管理模块。下一步是将此模块集成到Zinx框架中,并将其升级到Zinx V0.6。

使用原始的Markdown格式翻译成英文。

6.2 ZINX-V0.6代码的实现

首先,需要修改IServer抽象层中的AddRouter()接口以包括MsgID参数,因为现在我们添加了MSGID差异化。修改如下:

//zinx/ziface/iserver.go

package ziface

// Server interface definition
type IServer interface {
    // Start the server
    Start()
    // Stop the server
    Stop()
    // Start the business service
    Serve()
    // Route function: Register a route business method for the current server to handle client connections
    AddRouter(msgId uint32, router IRouter)
}

第二,在Server类的Server结构中,以前的路由器成员,代表唯一消息处理业务方法的MsgHandler成员。修改后,应该看起来像这样:

//zinx/znet/server.go

type Server struct {
    // Server name
    Name string
    // IP version, e.g., "tcp4" or "tcp6"
    IPVersion string
    // IP address the server is bound to
    IP string
    // Port the server is bound to
    Port int
    // Message handler module of the current server, used to bind MsgIDs with corresponding handling methods
    msgHandler ziface.IMsgHandle
}

还需要修改用于初始化Server的构造函数函数,以包括msgHandler对象的初始化:

//zinx/znet/server.go

/*
  Create a server handler
 */
func NewServer () ziface.IServer {
    utils.GlobalObject.Reload()

    s := &Server {
        Name:       utils.GlobalObject.Name,
        IPVersion:  "tcp4",
        IP:         utils.GlobalObject.Host,
        Port:       utils.GlobalObject.TcpPort,
        msgHandler: NewMsgHandle(), // Initialize msgHandler
    }
    return s
}

Server处理连接请求时,连接的创建还需要将msgHandler作为参数传递给Connection对象。相关代码修改如下:

//zinx/znet/server.go

// ... (omitted code)

dealConn := NewConntion(conn, cid, s.msgHandler)

// ... (omitted code)

接下来,让我们继续转到Connection对象。 Connection对象应具有一个MsgHandler成员来查找消息的回调路由方法。修改的代码如下:

//zinx/znet/connection.go

type Connection struct {
    // Current socket TCP connection
    Conn *net.TCPConn
    // Current connection ID, also known as SessionID, which is globally unique
    ConnID uint32
    // Current connection closed state
    isClosed bool
    // Message handler, which manages MsgIDs and corresponding handling methods
    MsgHandler ziface.IMsgHandle
    // Channel to notify that the connection has exited/stopped
    ExitBuffChan chan bool
}

创建连接的构造方法方法(NewConntion())还需要通过MsgHandler作为参数将其分配给成员:

//zinx/znet/connection.go

func NewConntion(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
}

最后,在将conn的完整消息数据封装在Request中时,当有必要调用路由业务时,我们只需要调用connDoMsgHandler()方法的DoMsgHandler()方法。相关代码修改如下:

//zinx/znet/connection.go

func (c *Connection) StartReader() {

    // ... (omitted code)

    for  {
        // ... (omitted code)

        // Get the Request data for the current client request
        req := Request{
            conn: c,
            msg:  msg,
        }

        // Execute the corresponding Handle method from the bound message and its handling method
        go c.MsgHandler.DoMsgHandler(&req)
    }
}

通过启动新的goroutine来处理domsghandler()方法,带有不同msgids的消息将匹配不同的处理业务流。

使用原始的Markdown格式翻译成英文。

6.3使用ZINX-V0.6开发应用程序

要使用ZINX-V0.6测试开发,我们将创建一个服务器端应用程序。代码如下:

// Server.go
package main

import (
    "fmt"
    "zinx/ziface"
    "zinx/znet"
)

// Ping test custom router
type PingRouter struct {
    znet.BaseRouter
}

// Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
    fmt.Println("Call PingRouter Handle")
    // Read client data first, then write back ping...ping...ping
    fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))

    err := request.GetConnection().SendMsg(0, []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")
    // Read client data first, then write back 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() {
    // Create a server handle
    s := znet.NewServer()

    // Configure routers
    s.AddRouter(0, &PingRouter{})
    s.AddRouter(1, &HelloZinxRouter{})

    // Start the server
    s.Serve()
}

服务器设置了两个路由器:一个用于MSGID 0的消息,它将执行PingRouter{}中覆盖的handle()方法,另一个用于使用MSGID 1的消息,该消息将执行HelloZinxRouter{}中的handle()方法。<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< /p>

接下来,我们将创建两个客户端,这些客户端将以MSGID 0和MSGID 1发送消息,以测试Zinx是否可以处理这些不同的消息类型。

第一个客户端将在Client0.go中实现,并具有以下代码:

//Client0.go
package main

import (
    "fmt"
    "io"
    "net"
    "time"
    "zinx/znet"
)

/*
   Simulate client
*/
func main() {

    fmt.Println("Client Test ... start")
    // Wait for 3 seconds before sending the test request to give the server a chance to start
    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 {
        // Pack the message
        dp := znet.NewDataPack()
        msg, _ := dp.Pack(znet.NewMsgPackage(0, []byte("Zinx V0.6 Client0 Test Message")))
        _, err := conn.Write(msg)
        if err != nil {
            fmt.Println("write error err ", err)
            return
        }

        // Read the head part from the stream
        headData := make([]byte, dp.GetHeadLen())
        _, err = io.ReadFull(conn, headData) // ReadFull fills the buffer until it's full
        if err != nil {
            fmt.Println("read head error")
            break
        }

        // Unpack the headData into a message
        msgHead, err := dp.Unpack(headData)
        if err != nil {
            fmt.Println("server unpack err:", err)
            return
        }

        if msgHead.GetDataLen() > 0 {
            // The message has data, so we need to read the data part
            msg := msgHead.(*znet.Message)
            msg.Data = make([]byte, msg.GetDataLen())

            // Read the data bytes from the stream based on the dataLen
            _, 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)
    }
}

客户端0发送带有MSGID 0的消息和内容“ Zinx v0.6客户端测试消息”。

第二个客户端将在Client1.go中实现,并具有以下代码:

//Client1.go
package main

import (
    "fmt"
    "io"
    "net"
    "time"
    "zinx/znet"
)

/*
   Simulate client
*/
func main() {

    fmt.Println("Client Test ... start")
    // Wait for 3 seconds before sending the test request to give the server a chance to start
    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 {
        // Pack the 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
        }

        // Read the head part from the stream
        headData := make([]byte, dp.GetHeadLen())
        _, err = io.ReadFull(conn, headData) // ReadFull fills the buffer until it's full
        if err != nil {
            fmt.Println("read head error")
            break
        }

        // Unpack the headData into a message
        msgHead, err := dp.Unpack(headData)
        if err != nil {
            fmt.Println("server unpack err:", err)
            return
        }

        if msgHead.GetDataLen() > 0 {
            // The message has data, so we need to read the data part
            msg := msgHead.(*znet.Message)
            msg.Data = make([]byte, msg.GetDataLen())

            // Read the data bytes from the stream based on the dataLen
            _, 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)
    }
}

Client1发送带有MSGID 1的消息和内容“ Zinx v0.6客户端测试消息”。

运行服务器和两个客户端,在三个不同的终端中执行以下命令:

$ go run Server.go
$ go run Client0.go
$ go run Client1.go

服务器输出将如下:

$ go run Server.go 
Add api msgId =  0
Add api msgId =  1
[START] Server name: zinx v-0.6 demoApp, listenner at IP: 127.0.0.1, Port 7777 is starting
[Zinx] Version: V0.4, MaxConn: 3, MaxPacketSize: 4096
start Zinx server zinx v-0.6 demoApp succ, now listenning...
Reader Goroutine is running
Call PingRouter Handle
recv from client: msgId= 0, data= Zinx V0.6 Client0 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.6 Client0 Test Message
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.6 Client0 Test Message
Call HelloZinxRouter Handle
recv from client: msgId= 1, data= Zinx V0.6 Client1 Test Message
// ...

客户端0输出将如下:

$ go run Client0.go 
Client Test ... start
==> Recv Msg: ID= 0, len= 18, data= ping...ping...ping
==> Recv Msg: ID= 0, len= 18, data= ping...ping...ping
==> Recv Msg: ID= 0, len= 18, data= ping...ping...ping
// ...

客户端1输出将如下:

$ go run Client1.go 
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
// ...

从结果中可以看出,服务器代码现在能够处理不同的消息类型并根据消息ID执行不同的逻辑。 Client0仅收到“ ping ... ping ... ping”作为答复,而client1仅收到“你好zinx路由器v0.6”作为答复。

7.4摘要

总而言之,直到Zinx V0.6,可以根据不同的消息ID处理不同的业务逻辑。 Zinx为服务器端网络通信提供了一个基本框架,使开发人员能够定义消息类型,根据消息类型注册不同的句柄功能,然后使用AddRouter()将它们添加到服务器服务对象,从而启用服务器端业务开发功能。下一步将是进一步升级Zinx中的内部模块结构。


<1.Building Basic Services with Zinx Framework>
<2. Zinx-V0.2 Simple Connection Encapsulation and Binding with Business>
<3.Design and Implementation of the Zinx Framework's Routing Module>
<4.Zinx Global Configuration>
<5.Zinx Message Encapsulation Module Design and Implementation>
<6.Design and Implementation of Zinx Multi-Router Mode>
<7. Building Zinx's Read-Write Separation Model>

未完待续...

作者:
不和谐:https://discord.gg/xQ8Xxfyfcz
zinx:https://github.com/aceld/zinx
github:https://github.com/aceld
Aceld的家:https://yuque.com/aceld