DEV Community

Cover image for 3.Design and Implementation of the Zinx Framework's Routing Module
Aceld
Aceld

Posted on • Updated on

3.Design and Implementation of the Zinx Framework's Routing Module

[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.3.tar.gz



In Zinx, it is necessary to provide users with a customized connection handling interface. Clearly, binding the business handling methods to the format type HandFunc func(*net.TCPConn, []byte, int) error is not sufficient. Here, we need to define some interface{} to allow users to provide connection handling methods in any format.

Using only the func type clearly cannot meet the development requirements. Now, we need to create several abstract interface classes.

3.1 IRequest - Abstract Class for Message Requests

In this section, we will combine the connection information and request data of a client's request into a request class called Request. The benefit of this approach is that we can obtain all the client's request information from Request, which also serves as a basis for future framework expansion. If the client has additional meaningful data, it can be stored in this Request class. It can be understood that every time the client sends a complete request, Zinx will put them together in a Request structure.

3.1.1 Creating the Abstract IRequest Layer

Create a new file irequest.go in the ziface directory. This file represents the IRequest interface and is located in the abstract layer directory of Zinx (ziface). The interface is defined as follows:

//zinx/ziface/irequest.go
package ziface

/*
    IRequest interface:
    This interface encapsulates the client's connection 
    information and request data into a Request.
*/

type IRequest interface {
    GetConnection() IConnection // Get the connection information of the request
    GetData() []byte // Get the data of the request message
}
Enter fullscreen mode Exit fullscreen mode

It is evident that the current abstraction layer only provides two getter methods, indicating that two members must be present. One is the client's connection, and the other is the data passed by the client. Of course, as the Zinx framework becomes more feature-rich, new members should be added to it.

Note the interface design of GetConnection() in IRequest. It returns IConnection instead of Connection. Although it has no impact on program implementation when returning the latter, considering design patterns and architectural scalability, it is recommended that the interface in the abstraction layer still depends on the abstraction layer. It is primarily about programming against the abstraction layer, so the returned concrete Connection object only needs to be a subclass of IConnection. This also demonstrates the application of the polymorphism feature of object-oriented programming. Therefore, the design of this interface should comply with the "Liskov Substitution Principle."

3.1.2 Implementation of the Request class

Create a new file named request.go in the znet directory, which will primarily implement the Request class of Zinx. The code is as follows:

// zinx/znet/request.go
package znet

import "zinx/ziface"

type Request struct {
    conn ziface.IConnection // The established connection with the client
    data []byte             // Data requested by the client
}

// GetConnection retrieves the connection information of the request
func (r *Request) GetConnection() ziface.IConnection {
    return r.conn
}

// GetData retrieves the data of the request message
func (r *Request) GetData() []byte {
    return r.data
}
Enter fullscreen mode Exit fullscreen mode

Now the Request class is created and it will be used later when configuring the router.

3.2 IRouter - Abstract Router Configuration

In this section, we will implement a very basic routing functionality in Zinx, aiming to quickly introduce routing into the framework.

3.2.1 Creating the Abstract IRouter Layer

Create a file named irouter.go in the ziface directory. This file will define the interface for the routing functionality. The code implementation is as follows:

// zinx/ziface/irouter.go
package ziface

/*
    Router interface.
    Routers are used by framework users to configure custom business methods for a connection.
    The IRequest in the router contains the connection information and the request data of that connection.
*/
type IRouter interface {
    PreHandle(request IRequest)  // Hook method executed before handling the conn business
    Handle(request IRequest)     // Method to handle the conn business
    PostHandle(request IRequest) // Hook method executed after handling the conn business
}
Enter fullscreen mode Exit fullscreen mode

The role of the Router is to allow the server application to configure the handling business method for a particular connection. In previous versions of Zinx (V0.2), the method for handling connection requests was fixed. Now, it can be customized, and there are three interfaces that can be overridden:

  • Handle: This is the main business function for handling the current connection.
  • PreHandle: If there is a need for pre-processing before the main business function, this method can be overridden.
  • PostHandle: If there is a need for post-processing after the main business function, this method can be overridden.

Each method has a unique parameter, IRequest object, which represents the connection and request data coming from the client, and serves as the input data for the business methods.

3.2.2 Implementing the Router Class

Create a file named router.go in the znet directory. This file contains the implementation of the Router class. The code is as follows:

// zinx/znet/router.go
package znet

import "zinx/ziface"

// When implementing a router, embed this base class and override its methods as needed
type BaseRouter struct{}

func (br *BaseRouter) PreHandle(req ziface.IRequest) {}
func (br *BaseRouter) Handle(req ziface.IRequest) {}
func (br *BaseRouter) PostHandle(req ziface.IRequest) {}
Enter fullscreen mode Exit fullscreen mode

The BaseRouter class serves as the parent class for all subclasses implementing the Router. It implements the three interfaces of IRouter, but the method implementations in BaseRouter are empty. This is because some implementation layer routers may not require PreHandle or PostHandle methods. By inheriting BaseRouter, it is not necessary to implement these methods in order to instantiate a router.

At this point, the directory structure of Zinx should look as follows:

.
├── README.md
├── ziface
│   ├── iconnection.go
│   ├── irequest.go
│   ├── irouter.go
│   └── iserver.go
└── znet
    ├── connection.go
    ├── request.go
    ├── router.go
    ├── server.go
    └── server_test.go
Enter fullscreen mode Exit fullscreen mode

3.3 Integrating Basic Routing Functionality into Zinx-V0.3

Now that IRequest and IRouter have been defined, the next step is to integrate them into the Zinx framework.

3.3.1 Adding Router Registration Functionality to IServer

The IServer interface needs to add an abstract method AddRouter(). The purpose is to allow users of the Zinx framework to customize a router for handling business methods. The code is as follows:

// zinx/ziface/iserver.go
package ziface

// Definition of the server interface
type IServer interface {
    // Start the server
    Start()
    // Stop the server
    Stop()
    // Serve the business services
    Serve()
    // Router function: Register a router business method for the current server to handle client connections
    AddRouter(router IRouter)
}
Enter fullscreen mode Exit fullscreen mode

The AddRouter() method takes an IRouter as its parameter, which is an abstract layer and not a concrete implementation.

3.3.2 Adding Router Member to Server Class

With the abstract method in place, the Server class needs to implement it and add a Router member. The modified Server data structure is as follows:

// zinx/znet/server.go

// Implementation of the iServer interface, defining a Server service class
type Server struct {
    // Server name
    Name string
    // IP version (tcp4 or other)
    IPVersion string
    // IP address that the service binds to
    IP string
    // Port that the service binds to
    Port int
    // Callback router bound by the user for the current Server, which is responsible for handling business for registered connections
    Router ziface.IRouter
}
Enter fullscreen mode Exit fullscreen mode

Correspondingly, the NewServer() method should include default initialization and assignment for the Router:

// zinx/znet/server.go

/*
  Create a server handle
 */
func NewServer(name string) ziface.IServer {
    s := &Server{
        Name:      name,
        IPVersion: "tcp4",
        IP:        "0.0.0.0",
        Port:      7777,
        Router:    nil, // Default not specified
    }

    return s
}
Enter fullscreen mode Exit fullscreen mode

The Server class needs to implement the AddRouter() method to add the router. Here, it simply needs to be registered in the s.Router member. The AddRouter() method is used by developers for business functionality. The code is as follows:

// zinx/znet/server.go

// Router function: Register a router business method for the current server to handle client connections
func (s *Server) AddRouter(router ziface.IRouter) {
    s.Router = router

    fmt.Println("Add Router success!")
}
Enter fullscreen mode Exit fullscreen mode

3.3.3 Binding a Router Member to the Connection Class

Since the Server has integrated the Router functionality, the Connection class also needs to be associated with the Router. Modify the data structure of the Connection implementation class and add the Router member. The code modifications are as follows:

// zinx/znet/connection.go

type Connection struct {
    // TCP socket for the current connection
    Conn *net.TCPConn
    // Connection ID, also known as SessionID, globally unique
    ConnID uint32
    // Connection close status
    isClosed bool

    // Router for handling the connection
    Router ziface.IRouter

    // Channel to notify that the connection has exited/stopped
    ExitBuffChan chan bool
}
Enter fullscreen mode Exit fullscreen mode

When creating a new connection in NewConnection(), the Router parameter needs to be passed. Modify the code as follows:

// zinx/znet/connection.go

// Method to create a connection
func NewConnection(conn *net.TCPConn, connID uint32, router ziface.IRouter) *Connection {
    c := &Connection{
        Conn:          conn,
        ConnID:        connID,
        isClosed:      false,
        Router:        router,
        ExitBuffChan:  make(chan bool, 1),
    }

    return c
}
Enter fullscreen mode Exit fullscreen mode

3.3.4 Calling the Registered Router to Handle Business in Connection

After integrating the Router member into the Connection class, the registered Router can be called in the logic of StartReader() that handles the data reading. The following code adds the invocation of the Router after reading the data:

// zinx/znet/connection.go

func (c *Connection) StartReader() {
    fmt.Println("Reader Goroutine is running")
    defer fmt.Println(c.RemoteAddr().String(), " conn reader exit!")
    defer c.Stop()

    for {
        // Read the maximum data into the buffer
        buf := make([]byte, 512)
        _, err := c.Conn.Read(buf)
        if err != nil {
            fmt.Println("recv buf err ", err)
            c.ExitBuffChan <- true
            continue
        }
        // Get the Request data of the current client request
        req := Request{
            conn: c,
            data: buf,
        }
        // Find the corresponding Handle registered with the bound Conn from the Routers
        go func(request ziface.IRequest) {
            // Execute the registered router methods
            c.Router.PreHandle(request)
            c.Router.Handle(request)
            c.Router.PostHandle(request)
        }(&req)
    }
}
Enter fullscreen mode Exit fullscreen mode

After reading the client data in the StartReader() method of the connection, the data and the connection are encapsulated in a Request as input data for the Router:

// Get the Request data of the current client request
req := Request{
    conn: c,
    data: buf,
}
Enter fullscreen mode Exit fullscreen mode

Then, a Goroutine is started to invoke the registered router business logic in the Zinx framework:

// Find the corresponding Handle registered with the bound Conn from the Routers
go func(request ziface.IRequest) {
    // Execute the registered router methods
    c.Router.PreHandle(request)
    c.Router.Handle(request)
    c.Router.PostHandle(request)
}(&req)
Enter fullscreen mode Exit fullscreen mode

If the Router has overridden PreHandle(), it will be called; otherwise, the empty method of BaseRouter() will be called. The same logic applies to Handle() and PostHandle().

3.4 Passing the Router Parameter to Connection in Server

After the Server successfully establishes a new connection, a new Connection needs to be created, and the current Router parameter should be passed to the connection. The main modification is in the NewConnection() method. The relevant key code changes are as follows:

// zinx/znet/server.go

package znet

import (
    "fmt"
    "net"
    "time"
    "zinx/ziface"
)

// Start the network service
func (s *Server) Start() {
    // ... (partial code omitted)

    // Start a goroutine to handle server listener business
    go func() {
        // 1. Get a TCP Addr
        // ... (partial code omitted)

        // 2. Listen to the server address
        // ... (partial code omitted)

        // 3. Start the server network connection business
        for {
            // 3.1 Block and wait for client connection requests
            conn, err := listener.AcceptTCP()
            if err != nil {
                fmt.Println("Accept err ", err)
                continue
            }

            // 3.2 TODO: Set the server's maximum connection control in Server.Start()
            // If the maximum connection is exceeded, close this new connection

            // 3.3 Handle the business method of the new connection request
            // The handler and conn should be bound at this point
            dealConn := NewConnection(conn, cid, s.Router)
            cid++

            // 3.4 Start handling the business of the current connection
            go dealConn.Start()
        }
    }()
}
Enter fullscreen mode Exit fullscreen mode

3.5 Completing the Application with Zinx-V0.3

Now, with Zinx, a simple routing functionality can be configured.

3.5.1 Testing the Server Application Built with Zinx

Now let's continue with the development of the Server.go application. Developers need to define a custom Router class and implement the PreHandle(), Handle(), and PostHandle() methods to handle specific business logic for data requests in Zinx. The code is as follows:

// Server.go
package main

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

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

// Test PreHandle
func (this *PingRouter) PreHandle(request ziface.IRequest) {
    fmt.Println("Call Router PreHandle")

    _, err := request.GetConnection().GetTCPConnection().Write([]byte("before ping ....\n"))
    if err != nil {
        fmt.Println("callback ping ping ping error")
    }
}

// Test Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
    fmt.Println("Call PingRouter Handle")

    _, err := request.GetConnection().GetTCPConnection().Write([]byte("ping...ping...ping\n"))
    if err != nil {
        fmt.Println("callback ping ping ping error")
    }
}

// Test PostHandle
func (this *PingRouter) PostHandle(request ziface.IRequest) {
    fmt.Println("Call Router PostHandle")

    _, err := request.GetConnection().GetTCPConnection().Write([]byte("After ping .....\n"))
    if err != nil {
        fmt.Println("callback ping ping ping error")
    }
}

func main() {
    // Create a server instance
    s := znet.NewServer("[zinx V0.3]")

    s.AddRouter(&PingRouter{})

    // Start the server
    s.Serve()
}
Enter fullscreen mode Exit fullscreen mode

The above code defines a custom router similar to the Ping operation. When the client sends data, the server's business logic is to return "ping...ping...ping" to the client. To test it, the current router also implements the PreHandle() and PostHandle() methods. In practice, Zinx will use the template method design pattern to call the PreHandle(), Handle(), and PostHandle() methods sequentially in the framework.

3.5.2 Starting the Server and Client

1 Start Server.go

Execute the following command to start the Server program with the registered Router:

go run Server.go
Enter fullscreen mode Exit fullscreen mode

2 Start Client.go

The client code in Client.go remains unchanged from the previous version. Execute the following command to run the program:

go run Client.go
Enter fullscreen mode Exit fullscreen mode

3 Server Output

$ go run Server.go
Add Router succ!
[START] Server listener at IP: 0.0.0.0, Port 7777, is starting
start Zinx server [zinx V0.3] succ, now listening...
Reader Goroutine is running
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
...
Enter fullscreen mode Exit fullscreen mode

4 Client Output

$ go run Client.go
Client Test ... start
Server callback: before ping ...., cnt = 17
Server callback: ping...ping...ping
After ping ....., cnt = 36
Server callback: before ping ....
ping...ping...ping
After ping ....., cnt = 53
Server callback: before ping ....
ping...ping...ping
After ping ....., cnt = 53
Server callback: before ping ....
ping...ping...ping
After ping ....., cnt = 53
...
Enter fullscreen mode Exit fullscreen mode

3.6 Conclusion

Now Zinx framework has routing functionality, although currently only one Router can be configured. However, Zinx now allows developers to freely configure business logic. In the next steps, Zinx will add the ability to configure multiple routers.


source code

https://github.com/aceld/zinx/blob/master/examples/zinx_release/zinx-v0.3.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)