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
}

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
}

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),
}
}

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())
}

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())
}

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)
}

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())
}

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
}

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
}

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
}

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
}

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
}

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
}

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)
}
}()
}

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)
}

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()
}

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
}

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
}

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
}

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
}

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
}
}
}

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)
}

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)
}

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)
}
}

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)
}

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)
}

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()
}

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

2.Start the Client:

$ go run Client.go

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!]

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

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)
}

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
}

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
}

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)
}

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()
}

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!]

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

The server output is particularly noteworthy:

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

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.

--

--