DEV Community

Cover image for 9. Zinx Connection Management and Property Setting
Aceld
Aceld

Posted on • Edited on

9. Zinx Connection Management and Property Setting

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


In this chapter, we will add connection limitations to the Zinx framework. If the number of client connections exceeds a certain threshold, Zinx will reject new connection requests in order to ensure timely response for existing connections. Additionally, we will introduce connection properties to Zinx, allowing developers to associate business-specific parameters with connections for easy access during business processing.

9.1 Connection Management

We need to create a connection management module for Zinx, which consists of an abstract layer and an implementation layer. We'll start by implementing the abstract layer in the iconnmanager.go file located in the zinx/ziface directory:

// zinx/ziface/iconnmanager.go

package ziface

/*
   Connection management abstract layer
*/
type IConnManager interface {
    Add(conn IConnection)                            // Add a connection
    Remove(conn IConnection)                         // Remove a connection
    Get(connID uint32) (IConnection, error)           // Get a connection using the connection ID
    Len() int                                        // Get the current number of connections
    ClearConn()                                      // Remove and stop all connections
}
Enter fullscreen mode Exit fullscreen mode

The IConnManager interface defines the following methods:

  • Add: Add a connection to the connection manager.
  • Remove: Remove a connection from the connection manager. This does not close the connection; it simply removes it from the management.
  • Get: Retrieve a connection object based on the connection ID.
  • Len: Get the total number of connections managed by the connection manager.
  • ClearConn: Remove all connections from the manager and close them.

Next, we'll create the implementation layer for IConnManager in the connmanager.go file in the zinx/znet directory:

// zinx/znet/connmanager.go

package znet

import (
    "errors"
    "fmt"
    "sync"
    "zinx/ziface"
)

/*
   Connection manager module
*/
type ConnManager struct {
    connections map[uint32]ziface.IConnection // Map to hold connection information
    connLock    sync.RWMutex                  // Read-write lock for concurrent access to the map
}
Enter fullscreen mode Exit fullscreen mode

The ConnManager struct contains a connections map that stores all the connection information. The key is the connection ID, and the value is the connection itself. The connLock is a read-write lock used to protect concurrent access to the map.

The constructor for ConnManager initializes the map using the make function:

// zinx/znet/connmanager.go

/*
   Create a connection manager
*/
func NewConnManager() *ConnManager {
    return &ConnManager{
        connections: make(map[uint32]ziface.IConnection),
    }
}
Enter fullscreen mode Exit fullscreen mode

The Add method, which adds a connection to the manager, is implemented as follows:

// zinx/znet/connmanager.go

// Add a connection
func (connMgr *ConnManager) Add(conn ziface.IConnection) {
    // Protect shared resource (map) with a write lock
    connMgr.connLock.Lock()
    defer connMgr.connLock.Unlock()

    // Add the connection to ConnManager
    connMgr.connections[conn.GetConnID()] = conn

    fmt.Println("Connection added to ConnManager successfully: conn num =", connMgr.Len())
}
Enter fullscreen mode Exit fullscreen mode

Since Go's standard library map is not thread-safe, we need to use a lock to protect concurrent write operations. Here, we use a write lock (connLock) to ensure mutual exclusion when modifying the map.

The Remove method, which removes a connection from the manager, is implemented as follows:

// zinx/znet/connmanager.go

// Remove a connection
func (connMgr *ConnManager) Remove(conn ziface.IConnection) {
    // Protect shared resource (map) with a write lock
    connMgr.connLock.Lock()
    defer connMgr.connLock.Unlock()

    // Remove the connection
    delete(connMgr.connections, conn.GetConnID())

    fmt.Println("Connection removed: ConnID =", conn.GetConnID(), "successfully: conn num =", connMgr.Len())
}
Enter fullscreen mode Exit fullscreen mode

The Remove method simply removes the connection from the map without stopping the connection's business processing.

The Get and Len methods are implemented as follows:

// zinx/znet/connmanager.go

// Get a connection using the connection ID
func (connMgr *ConnManager) Get(connID uint32) (ziface.IConnection, error) {
    // Protect shared resource (map) with a read lock
    connMgr.connLock.RLock()
    defer connMgr.connLock.RUnlock()

    if conn, ok := connMgr.connections[connID]; ok {
        return conn, nil
    } else {
        return nil, errors.New("connection not found")
    }
}

// Get the current number of connections
func (connMgr *ConnManager) Len() int {
    return len(connMgr.connections)
}
Enter fullscreen mode Exit fullscreen mode

The Get method uses a read lock (connLock.RLock()) to allow concurrent read access to the map, ensuring data consistency. If a connection with the given ID is found, it is returned; otherwise, an error is returned.

The ClearConn method is implemented as follows:

// zinx/znet/connmanager.go

// Remove and stop all connections
func (connMgr *ConnManager) ClearConn() {
    // Protect shared resource (map) with a write lock
    connMgr.connLock.Lock()
    defer connMgr.connLock.Unlock()

    // Stop and remove all connections
    for connID, conn := range connMgr.connections {
        // Stop the connection
        conn.Stop()
        // Remove the connection
        delete(connMgr.connections, connID)
    }

    fmt.Println("All connections cleared successfully: conn num =", connMgr.Len())
}
Enter fullscreen mode Exit fullscreen mode

The ClearConn method stops each connection's business processing by calling conn.Stop(), and then removes all connections from the map.

9.1.2 Integrating Connection Management Module into Zinx

1. Adding ConnManager to Server

We need to add the ConnManager to the Server struct and initialize it in the server's constructor. The Server struct in the zinx/znet/server.go file will have a new member property called ConnMgr, which is of type ziface.IConnManager:

// zinx/znet/server.go

// Server is a server service class implementing the IServer interface
type Server struct {
    // Server name
    Name string
    // IP version (e.g., tcp4 or other)
    IPVersion string
    // IP address to bind the server to
    IP string
    // Port to bind the server to
    Port int
    // Message handler for binding MsgID and corresponding processing methods
    MsgHandler ziface.IMsgHandle
    // Connection manager for the server
    ConnMgr ziface.IConnManager
}
Enter fullscreen mode Exit fullscreen mode

In the server's constructor NewServer(), we need to initialize the ConnMgr:

// zinx/znet/server.go

// NewServer creates a server instance
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(),
        ConnMgr:   NewConnManager(), // Create a ConnManager
    }
    return s
}
Enter fullscreen mode Exit fullscreen mode

The NewServer() function creates a new server instance and initializes the ConnMgr property.

To provide access to the ConnMgr from the server, we need to add a method GetConnMgr() to the IServerinterface in the zinx/ziface/iserver.go file:

// zinx/ziface/iserver.go

type IServer interface {
    // Start the server
    Start()
    // Stop the server
    Stop()
    // Serve the business services
    Serve()
    // Register a router business method for the current service, used by client connection processing
    AddRouter(msgID uint32, router IRouter)
    // Get the connection manager
    GetConnMgr() IConnManager
}
Enter fullscreen mode Exit fullscreen mode

The GetConnMgr() method should return the ConnMgr property of the server:

// zinx/znet/server.go

// GetConnMgr returns the connection manager
func (s *Server) GetConnMgr() ziface.IConnManager {
    return s.ConnMgr
}
Enter fullscreen mode Exit fullscreen mode

By implementing the GetConnMgr() method, we provide a way to access the connection manager from the server.

Because the connection (Connection) sometimes needs access to the connection manager (ConnMgr) in the server (Server), we need to establish a mutual reference relationship between the Server and Connection objects. In the Connection struct, we will add a member called TcpServer, which represents the server that the current connection belongs to. Add the TcpServer member to the Connection struct in the zinx/znet/connection.go file as follows:

// zinx/znet/connection.go

type Connection struct {
    // The server to which the current connection belongs
    TcpServer    ziface.IServer  // Add this line to indicate the server to which the connection belongs
    // The TCP socket of the current connection
    Conn         *net.TCPConn
    // The ID of the current connection (also known as SessionID, globally unique)
    ConnID       uint32
    // The closing state of the current connection
    isClosed     bool
    // The message handler that manages MsgID and corresponding processing methods
    MsgHandler   ziface.IMsgHandle
    // The channel that informs that the connection has exited/stopped
    ExitBuffChan chan bool
    // The unbuffered channel used for message communication between the reading and writing goroutines
    msgChan      chan []byte
    // The buffered channel used for message communication between the reading and writing goroutines
    msgBuffChan  chan []byte
}
Enter fullscreen mode Exit fullscreen mode

By adding the TcpServer member to the Connection struct, we establish a reference to the server that the connection belongs to. The TcpServer property is of type ziface.IServer.

2. Adding Connection to the Connection Manager

When initializing a connection, we need to add the connection to the server's connection manager. In the zinx/znet/connection.go file, modify the NewConnection() function to include the server object:

// zinx/znet/connection.go

// NewConnection creates a connection
func NewConnection(server ziface.IServer, conn *net.TCPConn, connID uint32, msgHandler ziface.IMsgHandle) *Connection {
    c := &Connection{
        TcpServer:    server,      // Set the server object
        Conn:         conn,
        ConnID:       connID,
        isClosed:     false,
        MsgHandler:   msgHandler,
        ExitBuffChan: make(chan bool, 1),
        msgChan:      make(chan []byte),
        msgBuffChan:  make(chan []byte, utils.GlobalObject.MaxMsgChanLen),
    }

    // Add the newly created connection to the connection manager
    c.TcpServer.GetConnMgr().Add(c)
    return c
}
Enter fullscreen mode Exit fullscreen mode

In the NewConnection() function, we pass the server object as a parameter and set it in the TcpServer property of the connection object. Then we add the connection to the connection manager using c.TcpServer.GetConnMgr().Add(c).

3. Checking Connection Count in Server

In the Start() method of the server, after a successful connection is established with a client, we can check the number of connections and terminate the connection creation if it exceeds the maximum connection count. Modify the Start() method in the zinx/znet/server.go file as follows:

// zinx/znet/server.go

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

    // Start a goroutine to handle the server listener
    go func() {
        // ... (omitted code)

        // Start the server network connection business
        for {
            // 1. Block and wait for client connection requests
            // ... (omitted code)

            // 2. Set the maximum connection limit for the server
            //    If the limit is exceeded, close the new connection
            if s.ConnMgr.Len() >= utils.GlobalObject.MaxConn {
                conn.Close()
                continue
            }

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

In the server's Start() method, we check the connection count using s.ConnMgr.Len() and compare it with the maximum connection limit. If the limit is reached, we close the new connection (conn.Close()) and continue to the next iteration.

Developers can define the maximum connection count in the configuration file zinx.json or in the GlobalObject global configuration using the MaxConn attribute.

4. Removing a Connection

When a connection is closed, it should be removed from the ConnManager. In the Stop() method of the Connection struct, we add the removal action from the ConnManager. Modify the Stop() method in the zinx/znet/connection.go file as follows:

// zinx/znet/connection.go

func (c *Connection) Stop() {
    fmt.Println("Conn Stop()... ConnID =", c.ConnID)

    if c.isClosed == true {
        return
    }
    c.isClosed = true

    c.Conn.Close()
    c.ExitBuffChan <- true

    // Remove the connection from the ConnManager
    c.TcpServer.GetConnMgr().Remove(c)

    close(c.ExitBuffChan)
    close(c.msgBuffChan)
}
Enter fullscreen mode Exit fullscreen mode

In the Stop() method, after closing the connection, we remove the connection from the ConnManager using c.TcpServer.GetConnMgr().Remove(c).

Additionally, when stopping the server in the Stop() method, we need to clear all connections as well:

// zinx/znet/server.go

func (s *Server) Stop() {
    fmt.Println("[STOP] Zinx server, name", s.Name)

    // Stop or clean up other necessary connection information or other information
    s.ConnMgr.ClearConn()
}
Enter fullscreen mode Exit fullscreen mode

In the Stop() method, we call s.ConnMgr.ClearConn() to stop and remove all connections from the ConnManager.

With the above code, we have successfully integrated the connection management into Zinx.

9.1.3 Buffered Message Sending Method for Connection

Previously, a method called SendMsg() was provided for the Connection struct, which sends data to an unbuffered channel called msgChan. However, if there are a large number of client connections and the recipient is unable to process the messages promptly, it may lead to temporary blocking. To provide a non-blocking sending experience, a buffered message sending method can be added.

IConnection Interface Definition (zinx/ziface/iconnection.go)

// Connection interface definition
type IConnection interface {
    // Start the connection, allowing the current connection to start working
    Start()
    // Stop the connection, ending the current connection state
    Stop()
    // Get the raw TCPConn of the current connection
    GetTCPConnection() *net.TCPConn
    // Get the current connection ID
    GetConnID() uint32
    // Get the remote client address information
    RemoteAddr() net.Addr
    // Send Message data directly to the remote TCP client (unbuffered)
    SendMsg(msgID uint32, data []byte) error
    // Send Message data directly to the remote TCP client (buffered)
    SendBuffMsg(msgID uint32, data []byte) error  // Add buffered message sending interface
}
Enter fullscreen mode Exit fullscreen mode

In addition to the SendMsg() method, we will provide a SendBuffMsg() method in the IConnectioninterface. The SendBuffMsg() method is similar to SendMsg() but uses a buffered channel for communication between two goroutines. The definition of the Connection struct in the zinx/znet/connection.go file will be modified as follows:

Connection Struct Definition (zinx/znet/connection.go)

type Connection struct {
    // The server to which the current connection belongs
    TcpServer    ziface.IServer
    // The TCP socket of the current connection
    Conn         *net.TCPConn
    // The ID of the current connection (also known as SessionID, globally unique)
    ConnID       uint32
    // The closing state of the current connection
    isClosed     bool
    // The message handler that manages MsgID and corresponding processing methods
    MsgHandler   ziface.IMsgHandle
    // The channel that informs that the connection has exited/stopped
    ExitBuffChan chan bool
    // The unbuffered channel used for message communication between the reading and writing goroutines
    msgChan      chan []byte
    // The buffered channel used for message communication between the reading and writing goroutines
    msgBuffChan  chan []byte
}
Enter fullscreen mode Exit fullscreen mode

To implement the buffered message sending functionality, a msgBuffChan of type chan []byte is added to the Connection struct. The msgBuffChan will be used for communication between the reading and writing goroutines. Make sure to initialize the msgBuffChan member in the connection's constructor method:

NewConnection Constructor (zinx/znet/connection.go)

func NewConnection(server ziface.IServer, conn *net.TCPConn, connID uint32, msgHandler ziface.IMsgHandle) *Connection {
    // Initialize Conn properties
    c := &Connection{
        TcpServer:    server,
        Conn:         conn,
        ConnID:       connID,
        isClosed:     false,
        MsgHandler:   msgHandler,
        ExitBuffChan: make(chan bool, 1),
        msgChan:      make(chan []byte),
        msgBuffChan:  make(chan []byte, utils.GlobalObject.MaxMsgChanLen), // Don't forget to initialize
    }

    // Add the newly created Conn to the connection manager
    c.TcpServer.GetConnMgr().Add(c)
    return c
}
Enter fullscreen mode Exit fullscreen mode

The SendBuffMsg() method implementation is similar to SendMsg(). It packs and sends the data to the client through the msgBuffChan. Here's the implementation:

SendBuffMsg() Method Implementation (zinx/znet/connection.go)

func (c *Connection) SendBuffMsg(msgID uint32, data []byte) error {
    if c.isClosed {
        return errors.New("Connection closed when sending buffered message")
    }
    // Pack the data and send it
    dp := NewDataPack()
    msg, err := dp.Pack(NewMsgPackage(msgID, data))
    if err != nil {
        fmt.Println("Pack error msg ID =", msgID)
        return errors.New("Pack error message")
    }

    // Write to the client
    c.msgBuffChan <- msg

    return nil
}
Enter fullscreen mode Exit fullscreen mode

The StartWriter() method in the Connection struct needs to handle the msgBuffChan for data transmission. Here's the implementation:

StartWriter() Method Implementation (zinx/znet/connection.go)

func (c *Connection) StartWriter() {
    fmt.Println("[Writer Goroutine is running]")
    defer fmt.Println(c.RemoteAddr().String(), "[conn Writer exit!]")

    for {
        select {
        case data := <-c.msgChan:
            // Data to be written to the client
            if _, err := c.Conn.Write(data); err != nil {
                fmt.Println("Send Data error:", err, "Conn Writer exit")
                return
            }

        case data, ok := <-c.msgBuffChan:
            // Handling data for buffered channel
            if ok {
                // Data to be written to the client
                if _, err := c.Conn.Write(data); err != nil {
                    fmt.Println("Send Buffered Data error:", err, "Conn Writer exit")
                    return
                }
            } else {
                fmt.Println("msgBuffChan is Closed")
                break
            }

        case <-c.ExitBuffChan:
            return
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The StartWriter() method listens to both the msgChan and msgBuffChan channels. If there's data in the msgChan, it writes it directly to the client. If there's data in the msgBuffChan, it writes it to the client after processing it accordingly.

9.1.5 Registering Connection Start/Stop Custom Hook Methods for Link Initialization/Shutdown

During the lifecycle of a connection, there are two moments when developers need to register callback functions to execute custom business logic. These moments occur after the connection is created and before it is disconnected. To meet this requirement, Zinx needs to add callback functions, also known as hook functions, that are triggered after the connection is created and before it is disconnected.

The IServer interface in the zinx/ziface/iserver.go file provides methods for registering connection hooks that can be used by developers. The interface definition is as follows:

type IServer interface {
    // Start the server
    Start()
    // Stop the server
    Stop()
    // Start the business service
    Serve()
    // Register a routing business method for the current server to be used for client connection processing
    AddRouter(msgID uint32, router IRouter)
    // Get the connection manager
    GetConnMgr() IConnManager
    // Set the hook function to be called when a connection is created for this server
    SetOnConnStart(func(IConnection))
    // Set the hook function to be called when a connection is about to be disconnected for this server
    SetOnConnStop(func(IConnection))
    // Invoke the OnConnStart hook function for the connection
    CallOnConnStart(conn IConnection)
    // Invoke the OnConnStop hook function for the connection
    CallOnConnStop(conn IConnection)
}
Enter fullscreen mode Exit fullscreen mode

Four new hook methods are added:

1.SetOnConnStart: Set the hook function to be called when a connection is created for the current server.
2.SetOnConnStop: Set the hook function to be called when a connection is about to be disconnected for the current server.
3.CallOnConnStart: Invoke the hook function after a connection is created.
4.CallOnConnStop: Invoke the hook function before a connection is about to be disconnected.

The Server struct in the zinx/znet/server.go file is updated to include two new fields for the hook functions:

type Server struct {
    // Server name
    Name string
    // TCP version (e.g., tcp4 or other)
    IPVersion string
    // IP address to which the server is bound
    IP string
    // Port to which the server is bound
    Port int
    // Message handler for the server, used to bind MsgID with corresponding handling methods
    MsgHandler ziface.IMsgHandle
    // Connection manager for the server
    ConnMgr ziface.IConnManager

    // New hook function prototypes //

    // Hook function to be called when a connection is created for this server
    OnConnStart func(conn ziface.IConnection)
    // Hook function to be called when a connection is about to be disconnected for this server
    OnConnStop func(conn ziface.IConnection)
}
Enter fullscreen mode Exit fullscreen mode

The Server struct now includes OnConnStart and OnConnStop fields to hold the addresses of the hook functions passed by developers.

The implementation of the four new hook methods is as follows:

// Set the hook function to be called when a connection is created for the server
func (s *Server) SetOnConnStart(hookFunc func(ziface.IConnection)) {
    s.OnConnStart = hookFunc
}

// Set the hook function to be called when a connection is about to be disconnected for the server
func (s *Server) SetOnConnStop(hookFunc func(ziface.IConnection)) {
    s.OnConnStop = hookFunc
}

// Invoke the OnConnStart hook function for the connection
func (s *Server) CallOnConnStart(conn ziface.IConnection) {
    if s.OnConnStart != nil {
        fmt.Println("---> CallOnConnStart....")
        s.OnConnStart(conn)
    }
}

// Invoke the OnConnStop hook function for the connection
func (s *Server) CallOnConnStop(conn ziface.IConnection) {
    if s.OnConnStop != nil {
        fmt.Println("---> CallOnConnStop....")
        s.OnConnStop(conn)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's determine the positions where these two hook methods should be called. The first position is after the connection is created, which is the last step in the Start() method of the Connection struct:

// Start the connection, allowing it to begin working
func (c *Connection) Start() {
    // 1. Start the Goroutine for reading data from the client
    go c.StartReader()
    // 2. Start the Goroutine for writing data back to the client
    go c.StartWriter()

    // Call the registered hook method for connection creation according to the user's requirements
    c.TcpServer.CallOnConnStart(c)
}
Enter fullscreen mode Exit fullscreen mode

The second position is just before the connection is stopped, which is when the Stop() method of the Connection struct is called. It should be called before the Close() action of the socket because once the socket is closed, the communication with the remote end is terminated. If the hook method involves writing data back to the client, it will not be able to communicate properly. Therefore, the hook method should be called before the Close() method. Here's the code:

// Stop the connection, ending the current connection state
func (c *Connection) Stop() {
    fmt.Println("Conn Stop()...ConnID = ", c.ConnID)
    // If the current connection is already closed
    if c.isClosed == true {
        return
    }
    c.isClosed = true

    // ==================
    // If the user registered a callback function for this connection's closure, it should be called explicitly at this moment
    c.TcpServer.CallOnConnStop(c)
    // ==================

    // Close the socket connection
    c.Conn.Close()
    // Close the writer
    c.ExitBuffChan <- true

    // Remove the connection from the connection manager
    c.TcpServer.GetConnMgr().Remove(c)

    // Close all channels of this connection
    close(c.ExitBuffChan)
    close(c.msgBuffChan)
}
Enter fullscreen mode Exit fullscreen mode

9.1.5 Using Zinx-V0.9 to Complete the Application

By now, all the connection management functionality has been integrated into Zinx. The next step is to test whether the connection management module is functional. Let's test a server that demonstrates the ability to handle connection management hook function callbacks. 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 the data from the client first, then write back ping...ping...ping
    fmt.Println("recv from client: msgId=", request.GetMsgID(), ", data=", string(request.GetData()))

    err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
    if err != nil {
        fmt.Println(err)
    }
}

type HelloZinxRouter struct {
    znet.BaseRouter
}

// HelloZinxRouter Handle
func (this *HelloZinxRouter) Handle(request ziface.IRequest) {
    fmt.Println("Call HelloZinxRouter Handle")
    // Read the data from the client first, then write back Hello Zinx Router V0.8
    fmt.Println("recv from client: msgId=", request.GetMsgID(), ", data=", string(request.GetData()))

    err := request.GetConnection().SendBuffMsg(1, []byte("Hello Zinx Router V0.8"))
    if err != nil {
        fmt.Println(err)
    }
}

// Executed when a connection is created
func DoConnectionBegin(conn ziface.IConnection) {
    fmt.Println("DoConnectionBegin is Called...")
    err := conn.SendMsg(2, []byte("DoConnection BEGIN..."))
    if err != nil {
        fmt.Println(err)
    }
}

// Executed when a connection is lost
func DoConnectionLost(conn ziface.IConnection) {
    fmt.Println("DoConnectionLost is Called...")
}

func main() {
    // Create a server handler
    s := znet.NewServer()

    // Register connection hook callback functions
    s.SetOnConnStart(DoConnectionBegin)
    s.SetOnConnStop(DoConnectionLost)

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

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

The server-side business code registers two hook functions: DoConnectionBegin() to be executed after the connection is created and DoConnectionLost() to be executed before the connection is lost.

  • DoConnectionBegin(): After the connection is created, it sends a message with ID 2 to the client and prints a debug message on the server side saying "DoConnectionBegin is Called...".

  • DoConnectionLost(): Before the connection is lost, it prints a debug message on the server side saying "DoConnectionLost is Called...".

The code for the client Client.go remains unchanged. To test the server and client, open different terminals and start the server and client using the following commands:

1.Start the server:

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

2.Start the Client:

$ go run Client.go
Enter fullscreen mode Exit fullscreen mode

The server-side output will be as follows:

$ go run Server.go
Add api msgId = 0
Add api msgId =  1
[START] Server name: zinx v-0.8 demoApp, listener at IP: 127.0.0.1, Port 7777 is starting
[Zinx] Version: V0.4, MaxConn: 3, MaxPacketSize: 4096
start Zinx server zinx v-0.8 demoApp succ, now listening...
Worker ID =  9  is started.
Worker ID =  5  is started.
Worker ID =  6  is started.
Worker ID =  7  is started.
Worker ID =  8  is started.
Worker ID =  1  is started.
Worker ID =  0  is started.
Worker ID =  2  is started.
Worker ID =  3  is started.
Worker ID =  4  is started.
connection add to ConnManager successfully: conn num =  1
---> CallOnConnStart....
DoConnectionBegin is Called...
[Writer Goroutine is running]
[Reader Goroutine is running]
Add ConnID= 0  request msgID= 0 to workerID= 0
Call PingRouter Handle
recv from client: msgId= 0 , data= Zinx V0.8 Client0 Test Message
Add ConnID= 0  request msgID= 0 to workerID= 0
Call PingRouter Handle
recv from client: msgId= 0 , data= Zinx V0.8 Client0 Test Message
Add ConnID= 0  request msgID= 0 to workerID= 0
Call PingRouter Handle
recv from client: msgId= 0 , data= Zinx V0.8 Client0 Test Message
Add ConnID= 0  request msgID= 0 to workerID= 0
Call PingRouter Handle
recv from client: msgId= 0 , data= Zinx V0.8 Client0 Test Message
Add ConnID= 0  request msgID= 0 to workerID= 0
Call PingRouter Handle
recv from client: msgId= 0 , data= Zinx V0.8 Client0 Test Message
read msg head error read tcp4 127.0.0.1:7777->127.0.0.1:49510: read: connection reset by peer
Conn Stop()...ConnID =  0
---> CallOnConnStop....
DoConnectionLost is Called...
connection Remove ConnID= 0  successfully: conn num =  0
127.0.0.1:49510 [conn Reader exit!]
127.0.0.1:49510 [conn Writer exit!]
Enter fullscreen mode Exit fullscreen mode

The client-side output will be as follows:

$ go run Client0.go
Client Test... start
==> Recv Msg: ID= 2 , len= 21 , data= DoConnection BEGIN...
==> 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
==> Recv Msg: ID= 0 , len= 18 , data= ping...ping...ping
^Csignal: interrupt
Enter fullscreen mode Exit fullscreen mode

From the above results, we can see that the client is successfully created, and the callback hooks have been executed. The connection has been added to the ConnManager in the server, and the current connection count conn num is 1. When we manually press "CTRL+C" to close the client, the ConnManager on the server side successfully removes the connection, and the connection count conn num becomes 0. Additionally, the server-side debug information for connection stop callback is printed.

9.2 Setting Connection Attributes in Zinx

When dealing with connections, developers often want to bind certain user data or parameters to a connection. This allows them to retrieve the passed parameters from the connection during handle processing and carry out business logic accordingly. In order to provide this capability, Zinx needs to establish interfaces or methods for setting attributes on a current connection. This section will implement the functionality to set attributes for connections.

9.2.1 Adding Connection Configuration Interface

To begin with, in the IConnection abstract layer, three related interfaces for configuring connection attributes are added. The code is as follows:

//zinx/ziface/iconnection.go

// Definition of the connection interface
type IConnection interface {
    // Start the connection and initiate its operations
    Start()
    // Stop the connection and terminate its current state
    Stop()

    // Get the underlying TCPConn from the current connection
    GetTCPConnection() *net.TCPConn
    // Get the connection's unique ID
    GetConnID() uint32
    // Get the remote client's address information
    RemoteAddr() net.Addr

    // Send Message data directly to the remote TCP client (unbuffered)
    SendMsg(msgId uint32, data []byte) error
    // Send Message data directly to the remote TCP client (buffered)
    SendBuffMsg(msgId uint32, data []byte) error

    // Set connection attributes
    SetProperty(key string, value interface{})
    // Get connection attributes
    GetProperty(key string) (interface{}, error)
    // Remove connection attributes
    RemoveProperty(key string)
}

Enter fullscreen mode Exit fullscreen mode

In the provided code snippet, IConnection has three added methods: SetProperty(), GetProperty(), and RemoveProperty(). The key parameter in each method is of type string, and the value parameter is of the versatile interface{} type. The subsequent step involves defining the specific types of properties within the Connection.

9.2.2 Implementation of Connection Property Methods

In the implementation layer, the Connection structure is augmented with a member attribute named property. This attribute will hold all user-provided parameters passed through the connection, and it is defined as a map[string]interface{} type. The definition is as follows:

//zinx/znet/connction.go

type Connection struct {
    // Current Conn belongs to which Server
    TcpServer ziface.IServer
    // The TCP socket of the current connection
    Conn *net.TCPConn
    // ID of the current connection, also known as SessionID; globally unique
    ConnID uint32
    // Current connection's closure state
    isClosed bool
    // Message manager module for managing MsgId and corresponding message handling methods
    MsgHandler ziface.IMsgHandle
    // Channel to signal that the connection has exited/stopped
    ExitBuffChan chan bool
    // Unbuffered channel for message communication between read and write goroutines
    msgChan chan []byte
    // Buffered channel for message communication between read and write goroutines
    msgBuffChan chan []byte
    // Connection properties
    property     map[string]interface{}
    // Lock for protecting concurrent property modifications
    propertyLock sync.RWMutex
}
Enter fullscreen mode Exit fullscreen mode

The property map is not concurrency-safe, and read and write operations on the map need to be protected with the propertyLock. Additionally, the constructor of Connection must initialize both property and propertyLock. The code snippet for this is provided below:

//zinx/znet/connction.go

// Method to create a connection
func NewConntion(server ziface.IServer, conn *net.TCPConn, connID uint32, msgHandler ziface.IMsgHandle) *Connection {
    // Initialize Conn properties
    c := &Connection{
        TcpServer:    server,
        Conn:         conn,
        ConnID:       connID,
        isClosed:     false,
        MsgHandler:   msgHandler,
        ExitBuffChan: make(chan bool, 1),
        msgChan:      make(chan []byte),
        msgBuffChan:  make(chan []byte, utils.GlobalObject.MaxMsgChanLen),
        property:     make(map[string]interface{}), // Initialize connection property map
    }

    // Add the newly created Conn to the connection manager
    c.TcpServer.GetConnMgr().Add(c)
    return c
}
Enter fullscreen mode Exit fullscreen mode

The implementation of the three methods for handling connection properties is straightforward. They respectively involve adding, reading, and removing entries from the map. The implementations of these methods are as follows:

//zinx/znet/connction.go

// Set connection property
func (c *Connection) SetProperty(key string, value interface{}) {
    c.propertyLock.Lock()
    defer c.propertyLock.Unlock()

    c.property[key] = value
}

// Get connection property
func (c *Connection) GetProperty(key string) (interface{}, error) {
    c.propertyLock.RLock()
    defer c.propertyLock.RUnlock()

    if value, ok := c.property[key]; ok {
        return value, nil
    } else {
        return nil, errors.New("no property found")
    }
}

// Remove connection property
func (c *Connection) RemoveProperty(key string) {
    c.propertyLock.Lock()
    defer c.propertyLock.Unlock()

    delete(c.property, key)
}
Enter fullscreen mode Exit fullscreen mode

These methods manage the interaction with the property map, allowing for adding, retrieving, and removing properties associated with a connection.

9.1.3 Connection Property Testing in Zinx-V0.10

After encapsulating the functionality for connection properties, we can use the relevant interfaces on the server side to set some properties and test whether the setting and extraction of properties are functional. Below is the server-side code:

// 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 reply with ping...ping...ping
    fmt.Println("recv from client: msgId=", request.GetMsgID(), ", data=", string(request.GetData()))

    err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
    if err != nil {
        fmt.Println(err)
    }
}

type HelloZinxRouter struct {
    znet.BaseRouter
}

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

    err := request.GetConnection().SendBuffMsg(1, []byte("Hello Zinx Router V0.10"))
    if err != nil {
        fmt.Println(err)
    }
}

// Executed when a connection is created
func DoConnectionBegin(conn ziface.IConnection) {
    fmt.Println("DoConnectionBegin is Called ... ")

    // Set two connection properties after the connection is created
    fmt.Println("Set conn Name, Home done!")
    conn.SetProperty("Name", "Aceld")
    conn.SetProperty("Home", "https://github.com/aceld/zinx")

    err := conn.SendMsg(2, []byte("DoConnection BEGIN..."))
    if err != nil {
        fmt.Println(err)
    }
}

// Executed when a connection is lost
func DoConnectionLost(conn ziface.IConnection) {
    // Before the connection is destroyed, query the "Name" and "Home" properties of the conn
    if name, err := conn.GetProperty("Name"); err == nil {
        fmt.Println("Conn Property Name =", name)
    }

    if home, err := conn.GetProperty("Home"); err == nil {
        fmt.Println("Conn Property Home =", home)
    }

    fmt.Println("DoConnectionLost is Called ... ")
}

func main() {
    // Create a server handle
    s := znet.NewServer()

    // Register connection hook callback functions
    s.SetOnConnStart(DoConnectionBegin)
    s.SetOnConnStop(DoConnectionLost)

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

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

The key focus is on the implementations of the DoConnectionBegin() and DoConnectionLost() functions. Within these functions, connection properties are set and extracted using the hook callbacks. Once a connection is established, the properties "Name" and "Home" are assigned to the connection using the conn.SetProperty() method. Later, these properties can be retrieved using the conn.GetProperty() method.

Open a terminal to launch the server program, and observe the following results:

$ go run Server.go 
Add api msgId =  0
Add api msgId =  1
[START] Server name: zinx v-0.10 demoApp,listener at IP: 127.0.0.1, Port 7777 is starting
[Zinx] Version: V0.4, MaxConn: 3, MaxPacketSize: 4096
start Zinx server   zinx v-0.10 demoApp  succ, now listening...
Worker ID =  9  is started.
Worker ID =  5  is started.
Worker ID =  6  is started.
Worker ID =  7  is started.
Worker ID =  8  is started.
Worker ID =  1  is started.
Worker ID =  0  is started.
Worker ID =  2  is started.
Worker ID =  3  is started.
Worker ID =  4  is started.
connection add to ConnManager successfully: conn num =  1
---> CallOnConnStart....
DoConnecionBegin is Called ... 
Set conn Name, Home done!
[Writer Goroutine is running]
[Reader Goroutine is running]
Add ConnID= 0  request msgID= 0 to workerID= 0
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.8 Client0 Test Message
Add ConnID= 0  request msgID= 0 to workerID= 0
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.8 Client0 Test Message
Add ConnID= 0  request msgID= 0 to workerID= 0
Call PingRouter Handle
recv from client : msgId= 0 , data= Zinx V0.8 Client0 Test Message
read msg head error  read tcp4 127.0.0.1:7777->127.0.0.1:55208: read: connection reset by peer
Conn Stop()...ConnID =  0
---> CallOnConnStop....
Conn Property Name =  Aceld
Conn Property Home =  https://github.com/aceld/zinx
DoConneciotnLost is Called ... 
connection Remove ConnID= 0  successfully: conn num =  0
127.0.0.1:55208 [conn Reader exit!]
127.0.0.1:55208 [conn Writer exit!]
Enter fullscreen mode Exit fullscreen mode

In a new terminal, start the client program, and observe the following results:

$ go run Client0.go 
Client Test ... start
==> Recv Msg: ID= 2 , len= 21 , data= DoConnection BEGIN...
==> Recv Msg: ID= 0 , len= 18 , data= ping...ping...ping
==> Recv Msg: ID= 0 , len= 18 , data= ping...ping...ping
^Csignal: interrupt
Enter fullscreen mode Exit fullscreen mode

The server output is particularly noteworthy:

---> CallOnConnStop....
Conn Property Name =  Aceld
Conn Property Home =  https://github.com/aceld/zinx
DoConneciotnLost is Called ... 

Enter fullscreen mode Exit fullscreen mode

9.3 Summary

In this chapter, two functionalities were introduced to enhance the capabilities of connections in Zinx. The first one is the connection management module, which gathers all the connections within the Zinx server, providing an aggregated view and counting of the total number of connections. Currently, Zinx simply employs a basic incremental calculation for connection IDs. Readers are encouraged to optimize this aspect by replacing ConnID with a more common distributed ID, ensuring uniqueness across IDs.

The connection management module aids in controlling the concurrent load on the server by limiting the number of connections. The second addition, connection properties, enriches the convenience of handling business logic within Zinx. Developers can utilize the SetProperty() and GetProperty() methods to associate different attributes with different connections. This enables the linking of various markers or attributes to different connections, facilitating flexible business logic handling.


source code

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