DEV Community

Cover image for 6.Design and Implementation of Zinx Multi-Router Mode
Aceld
Aceld

Posted on • Updated on

6.Design and Implementation of Zinx Multi-Router Mode

[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>
<8.Zinx Message Queue and Task Worker Pool Design and Implementation>
<9. Zinx Connection Management and Property Setting>

[Zinx Application - MMO Game Case Study]

<10. Application Case Study using the Zinx Framework>
<11. MMO Online Game AOI Algorithm>
<12.Data Transmission Protocol: Protocol Buffers>
<13. MMO Game Server Application Protocol>
<14. Building the Project and User Login>
<15. World Chat System Implementation>
<16. Online Location Information Synchronization>
<17. Moving position and non-crossing grid AOI broadcasting>
<18. Player Logout>
<19. Movement and AOI Broadcast Across Grids>


source code

https://github.com/aceld/zinx/blob/master/examples/zinx_release/zinx-v0.6.tar.gz


In Zinx v0.5, the routing mode functionality has been configured, but it can only bind a single routing handler method. Obviously, this cannot meet the basic requirements of a server. Now, we need to add a multi-router mode to Zinx based on the previous implementation.

What is the multi-router mode? It means that we need to associate MsgIDs with their corresponding processing logic. This association needs to be stored in a map data structure, defined as follows:

Apis map[uint32] ziface.IRouter
Enter fullscreen mode Exit fullscreen mode

The key of the map is of type uint32 and stores the IDs of each type of message. The value is an abstraction layer of the IRouter routing business, which should be the Handle method overridden by the user. It is important to note that it is still not recommended to store specific implementation layer Router types here. The reason is that the design module is still based on abstract layer design. The name of this map is Apis.

6.1 Creating the Message Management Module

In this section, we will define a message management module to maintain the binding relationship in the Apis map.

6.1.1 Creating the Abstract Class for the Message Management Module

Create a file named imsghandler.go in the zinx/ziface directory. This file defines the abstract layer interface for message management. The interface for message management is named IMsgHandle and is defined as follows:

//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
}
Enter fullscreen mode Exit fullscreen mode

There are two methods inside this interface. AddRouter() is used to add a MsgID and a routing relationship to the Apis map. DoMsgHandler() is an interface that calls the specific Handle() method in the Router. The parameter is of type IRequest because Zinx has already put all client message requests into an IRequest with all the relevant message properties.

Translated into English, using the original Markdown format.

6.1.2 Implementation of the Message Management Module

Create a file named msghandler.go in the zinx/znet directory. This file contains the implementation code for the IMsgHandle interface. The specific implementation is as follows:

//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
}
Enter fullscreen mode Exit fullscreen mode

The MsgHandle struct has an Apis attribute, which is a map that binds MsgIDs to Routers. Next, we provide the constructor method for MsgHandle. The implementation code is as follows:

//zinx/znet/msghandler.go

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

In Golang, initializing a map requires using the make keyword to allocate space. Please note this. The MsgHandle struct needs to implement the two interface methods of IMsgHandle: DoMsgHandler() and AddRouter(). The specific implementation is as follows:

//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)
}
Enter fullscreen mode Exit fullscreen mode

The DoMsgHandler() method consists of two steps. First, it retrieves the MsgID from the input parameter request and uses the Apis map to get the corresponding Router. If it cannot find a match, it indicates an unrecognized message, and the developer needs to register the callback business Router for that type of message in advance. Second, after obtaining the Router, it sequentially executes the registered PreHandle(), Handle(), and PostHandle() methods in the template order. Once these three methods are executed, the message processing is completed.

The message management module has now been designed. The next step is to integrate this module into the Zinx framework and upgrade it to Zinx v0.6.

Translated into English, using the original Markdown format.

6.2 Implementation of Zinx-V0.6 Code

First, the AddRouter() interface in the IServer abstract layer needs to be modified to include the MsgID parameter, as now we have added MsgID differentiation. The modification is as follows:

//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)
}
Enter fullscreen mode Exit fullscreen mode

Second, in the Server struct of the Server class, the previous Router member, representing the unique message handling business method, should be replaced with the MsgHandler member. After the modification, it should look like this:

//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
}
Enter fullscreen mode Exit fullscreen mode

The constructor function for initializing the Server also needs to be modified to include the initialization of the msgHandler object:

//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
}
Enter fullscreen mode Exit fullscreen mode

When the Server is handling connection requests, the creation of a connection also needs to pass the msgHandler as a parameter to the Connection object. The relevant code modification is as follows:

//zinx/znet/server.go

// ... (omitted code)

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

// ... (omitted code)
Enter fullscreen mode Exit fullscreen mode

Next, let's move on to the Connection object. The Connection object should have a MsgHandler member to look up the callback route method for a message. The modified code is as follows:

//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
}
Enter fullscreen mode Exit fullscreen mode

The constructor method for creating a connection (NewConntion()) also needs to pass the MsgHandler as a parameter to assign it to the member:

//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
}
Enter fullscreen mode Exit fullscreen mode

Finally, after reading the complete Message data from the conn, when encapsulating it in a Request, and when it's necessary to invoke the routing business, we only need to call the DoMsgHandler() method of MsgHandler from the conn. The relevant code modification is as follows:

//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)
    }
}
Enter fullscreen mode Exit fullscreen mode

By starting a new Goroutine to handle the DoMsgHandler() method, messages with different MsgIDs will match different processing business flows.

Translated into English, using the original Markdown format.

6.3 Developing an Application using Zinx-V0.6

To test the development using Zinx-V0.6, we will create a server-side application. The code is as follows:

// 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()
}
Enter fullscreen mode Exit fullscreen mode

The server sets up two routers: one for messages with MsgID 0, which will execute the Handle() method overridden in PingRouter{}, and another for messages with MsgID 1, which will execute the Handle() method overridden in HelloZinxRouter{}.

Next, we'll create two clients that will send messages with MsgID 0 and MsgID 1 to test if Zinx can handle these different message types.

The first client will be implemented in Client0.go, with the following code:

//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)
    }
}
Enter fullscreen mode Exit fullscreen mode

The Client0 sends a message with MsgID 0 and the content "Zinx V0.6 Client0 Test Message".

The second client will be implemented in Client1.go, with the following code:

//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)
    }
}
Enter fullscreen mode Exit fullscreen mode

The Client1 sends a message with MsgID 1 and the content "Zinx V0.6 Client1 Test Message".

To run the server and the two clients, execute the following commands in three different terminals:

$ go run Server.go
$ go run Client0.go
$ go run Client1.go
Enter fullscreen mode Exit fullscreen mode

The server output will be as follows:

$ 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
// ...
Enter fullscreen mode Exit fullscreen mode

The Client0 output will be as follows:

$ 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
// ...
Enter fullscreen mode Exit fullscreen mode

The Client1 output will be as follows:

$ 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
// ...
Enter fullscreen mode Exit fullscreen mode

From the results, it can be observed that the server code is now able to handle different message types and perform different logic based on the message ID. Client0 receives only "ping...ping...ping" as a reply, while Client1 receives only "Hello Zinx Router V0.6" as a reply.

7.4 Summary

In conclusion, up to Zinx V0.6, it is possible to handle different business logic based on different message IDs. Zinx provides a basic framework for server-side network communication, allowing developers to define message types, register different handle functions based on the message types, and add them to the server service object using AddRouter(), thus enabling server-side business development capabilities. The next step would be to further upgrade the internal module structure handling in Zinx.


source code

https://github.com/aceld/zinx/blob/master/examples/zinx_release/zinx-v0.6.tar.gz


[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>
<8.Zinx Message Queue and Task Worker Pool Design and Implementation>
<9. Zinx Connection Management and Property Setting>

[Zinx Application - MMO Game Case Study]

<10. Application Case Study using the Zinx Framework>
<11. MMO Online Game AOI Algorithm>
<12.Data Transmission Protocol: Protocol Buffers>
<13. MMO Game Server Application Protocol>
<14. Building the Project and User Login>
<15. World Chat System Implementation>
<16. Online Location Information Synchronization>
<17. Moving position and non-crossing grid AOI broadcasting>
<18. Player Logout>
<19. Movement and AOI Broadcast Across Grids>


Author:
discord: https://discord.gg/xQ8Xxfyfcz
zinx: https://github.com/aceld/zinx
github: https://github.com/aceld
aceld's home: https://yuque.com/aceld

Top comments (0)