[Zinx]
<1.Building Basic Services with Zinx Framework>
<2. Zinx-V0.2 Simple Connection Encapsulation and Binding with Business>
<3.Design and Implementation of the Zinx Framework's Routing Module>
<4.Zinx Global Configuration>
<5.Zinx Message Encapsulation Module Design and Implementation>
<6.Design and Implementation of Zinx Multi-Router Mode>
<7. Building Zinx's Read-Write Separation Model>
<8.Zinx Message Queue and Task Worker Pool Design and Implementation>
<9. Zinx Connection Management and Property Setting>
[Zinx Application - MMO Game Case Study]
<10. Application Case Study using the Zinx Framework>
<11. MMO Online Game AOI Algorithm>
<12.Data Transmission Protocol: Protocol Buffers>
<13. MMO Game Server Application Protocol>
<14. Building the Project and User Login>
<15. World Chat System Implementation>
<16. Online Location Information Synchronization>
<17. Moving position and non-crossing grid AOI broadcasting>
<18. Player Logout>
<19. Movement and AOI Broadcast Across Grids>
source code
https://github.com/aceld/zinx/blob/master/examples/zinx_release/zinx-v0.6.tar.gz
In Zinx v0.5, the routing mode functionality has been configured, but it can only bind a single routing handler method. Obviously, this cannot meet the basic requirements of a server. Now, we need to add a multi-router mode to Zinx based on the previous implementation.
What is the multi-router mode? It means that we need to associate MsgIDs with their corresponding processing logic. This association needs to be stored in a map data structure, defined as follows:
Apis map[uint32] ziface.IRouter
The key of the map is of type uint32 and stores the IDs of each type of message. The value is an abstraction layer of the IRouter routing business, which should be the Handle method overridden by the user. It is important to note that it is still not recommended to store specific implementation layer Router types here. The reason is that the design module is still based on abstract layer design. The name of this map is Apis.
6.1 Creating the Message Management Module
In this section, we will define a message management module to maintain the binding relationship in the Apis map.
6.1.1 Creating the Abstract Class for the Message Management Module
Create a file named imsghandler.go
in the zinx/ziface
directory. This file defines the abstract layer interface for message management. The interface for message management is named IMsgHandle
and is defined as follows:
//zinx/ziface/imsghandler.go
package ziface
/*
Abstract layer for message management
*/
type IMsgHandle interface {
DoMsgHandler(request IRequest) // Process messages immediately in a non-blocking manner
AddRouter(msgId uint32, router IRouter) // Add specific handling logic for a message
}
There are two methods inside this interface. AddRouter()
is used to add a MsgID and a routing relationship to the Apis map. DoMsgHandler()
is an interface that calls the specific Handle() method in the Router. The parameter is of type IRequest
because Zinx has already put all client message requests into an IRequest
with all the relevant message properties.
Translated into English, using the original Markdown format.
6.1.2 Implementation of the Message Management Module
Create a file named msghandler.go in the zinx/znet directory. This file contains the implementation code for the IMsgHandle interface. The specific implementation is as follows:
//zinx/znet/msghandler.go
package znet
import (
"fmt"
"strconv"
"zinx/ziface"
)
type MsgHandle struct {
Apis map[uint32]ziface.IRouter // Map to store the handler methods for each MsgID
}
The MsgHandle
struct has an Apis
attribute, which is a map that binds MsgIDs to Routers. Next, we provide the constructor method for MsgHandle
. The implementation code is as follows:
//zinx/znet/msghandler.go
func NewMsgHandle() *MsgHandle {
return &MsgHandle{
Apis: make(map[uint32]ziface.IRouter),
}
}
In Golang, initializing a map requires using the make
keyword to allocate space. Please note this. The MsgHandle
struct needs to implement the two interface methods of IMsgHandle: DoMsgHandler()
and AddRouter()
. The specific implementation is as follows:
//zinx/znet/msghandler.go
// Process messages in a non-blocking manner
func (mh *MsgHandle) DoMsgHandler(request ziface.IRequest) {
handler, ok := mh.Apis[request.GetMsgID()]
if !ok {
fmt.Println("api msgId =", request.GetMsgID(), "is not FOUND!")
return
}
// Execute the corresponding handler methods
handler.PreHandle(request)
handler.Handle(request)
handler.PostHandle(request)
}
// Add specific handling logic for a message
func (mh *MsgHandle) AddRouter(msgId uint32, router ziface.IRouter) {
// 1. Check if the current msg's API handler method already exists
if _, ok := mh.Apis[msgId]; ok {
panic("repeated api, msgId = " + strconv.Itoa(int(msgId)))
}
// 2. Add the binding relationship between msg and api
mh.Apis[msgId] = router
fmt.Println("Add api msgId =", msgId)
}
The DoMsgHandler()
method consists of two steps. First, it retrieves the MsgID from the input parameter request
and uses the Apis map to get the corresponding Router. If it cannot find a match, it indicates an unrecognized message, and the developer needs to register the callback business Router for that type of message in advance. Second, after obtaining the Router, it sequentially executes the registered PreHandle()
, Handle()
, and PostHandle()
methods in the template order. Once these three methods are executed, the message processing is completed.
The message management module has now been designed. The next step is to integrate this module into the Zinx framework and upgrade it to Zinx v0.6.
Translated into English, using the original Markdown format.
6.2 Implementation of Zinx-V0.6 Code
First, the AddRouter()
interface in the IServer
abstract layer needs to be modified to include the MsgID
parameter, as now we have added MsgID differentiation. The modification is as follows:
//zinx/ziface/iserver.go
package ziface
// Server interface definition
type IServer interface {
// Start the server
Start()
// Stop the server
Stop()
// Start the business service
Serve()
// Route function: Register a route business method for the current server to handle client connections
AddRouter(msgId uint32, router IRouter)
}
Second, in the Server
struct of the Server
class, the previous Router member, representing the unique message handling business method, should be replaced with the MsgHandler
member. After the modification, it should look like this:
//zinx/znet/server.go
type Server struct {
// Server name
Name string
// IP version, e.g., "tcp4" or "tcp6"
IPVersion string
// IP address the server is bound to
IP string
// Port the server is bound to
Port int
// Message handler module of the current server, used to bind MsgIDs with corresponding handling methods
msgHandler ziface.IMsgHandle
}
The constructor function for initializing the Server
also needs to be modified to include the initialization of the msgHandler
object:
//zinx/znet/server.go
/*
Create a server handler
*/
func NewServer () ziface.IServer {
utils.GlobalObject.Reload()
s := &Server {
Name: utils.GlobalObject.Name,
IPVersion: "tcp4",
IP: utils.GlobalObject.Host,
Port: utils.GlobalObject.TcpPort,
msgHandler: NewMsgHandle(), // Initialize msgHandler
}
return s
}
When the Server
is handling connection requests, the creation of a connection also needs to pass the msgHandler
as a parameter to the Connection
object. The relevant code modification is as follows:
//zinx/znet/server.go
// ... (omitted code)
dealConn := NewConntion(conn, cid, s.msgHandler)
// ... (omitted code)
Next, let's move on to the Connection
object. The Connection
object should have a MsgHandler
member to look up the callback route method for a message. The modified code is as follows:
//zinx/znet/connection.go
type Connection struct {
// Current socket TCP connection
Conn *net.TCPConn
// Current connection ID, also known as SessionID, which is globally unique
ConnID uint32
// Current connection closed state
isClosed bool
// Message handler, which manages MsgIDs and corresponding handling methods
MsgHandler ziface.IMsgHandle
// Channel to notify that the connection has exited/stopped
ExitBuffChan chan bool
}
The constructor method for creating a connection (NewConntion()
) also needs to pass the MsgHandler
as a parameter to assign it to the member:
//zinx/znet/connection.go
func NewConntion(conn *net.TCPConn, connID uint32, msgHandler ziface.IMsgHandle) *Connection {
c := &Connection{
Conn: conn,
ConnID: connID,
isClosed: false,
MsgHandler: msgHandler,
ExitBuffChan: make(chan bool, 1),
}
return c
}
Finally, after reading the complete Message data from the conn
, when encapsulating it in a Request
, and when it's necessary to invoke the routing business, we only need to call the DoMsgHandler()
method of MsgHandler
from the conn
. The relevant code modification is as follows:
//zinx/znet/connection.go
func (c *Connection) StartReader() {
// ... (omitted code)
for {
// ... (omitted code)
// Get the Request data for the current client request
req := Request{
conn: c,
msg: msg,
}
// Execute the corresponding Handle method from the bound message and its handling method
go c.MsgHandler.DoMsgHandler(&req)
}
}
By starting a new Goroutine to handle the DoMsgHandler() method, messages with different MsgIDs will match different processing business flows.
Translated into English, using the original Markdown format.
6.3 Developing an Application using Zinx-V0.6
To test the development using Zinx-V0.6, we will create a server-side application. The code is as follows:
// Server.go
package main
import (
"fmt"
"zinx/ziface"
"zinx/znet"
)
// Ping test custom router
type PingRouter struct {
znet.BaseRouter
}
// Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
fmt.Println("Call PingRouter Handle")
// Read client data first, then write back ping...ping...ping
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
err := request.GetConnection().SendMsg(0, []byte("ping...ping...ping"))
if err != nil {
fmt.Println(err)
}
}
// HelloZinxRouter Handle
type HelloZinxRouter struct {
znet.BaseRouter
}
func (this *HelloZinxRouter) Handle(request ziface.IRequest) {
fmt.Println("Call HelloZinxRouter Handle")
// Read client data first, then write back ping...ping...ping
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
err := request.GetConnection().SendMsg(1, []byte("Hello Zinx Router V0.6"))
if err != nil {
fmt.Println(err)
}
}
func main() {
// Create a server handle
s := znet.NewServer()
// Configure routers
s.AddRouter(0, &PingRouter{})
s.AddRouter(1, &HelloZinxRouter{})
// Start the server
s.Serve()
}
The server sets up two routers: one for messages with MsgID 0, which will execute the Handle() method overridden in PingRouter{}
, and another for messages with MsgID 1, which will execute the Handle() method overridden in HelloZinxRouter{}
.
Next, we'll create two clients that will send messages with MsgID 0 and MsgID 1 to test if Zinx can handle these different message types.
The first client will be implemented in Client0.go
, with the following code:
//Client0.go
package main
import (
"fmt"
"io"
"net"
"time"
"zinx/znet"
)
/*
Simulate client
*/
func main() {
fmt.Println("Client Test ... start")
// Wait for 3 seconds before sending the test request to give the server a chance to start
time.Sleep(3 * time.Second)
conn, err := net.Dial("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("client start err, exit!")
return
}
for {
// Pack the message
dp := znet.NewDataPack()
msg, _ := dp.Pack(znet.NewMsgPackage(0, []byte("Zinx V0.6 Client0 Test Message")))
_, err := conn.Write(msg)
if err != nil {
fmt.Println("write error err ", err)
return
}
// Read the head part from the stream
headData := make([]byte, dp.GetHeadLen())
_, err = io.ReadFull(conn, headData) // ReadFull fills the buffer until it's full
if err != nil {
fmt.Println("read head error")
break
}
// Unpack the headData into a message
msgHead, err := dp.Unpack(headData)
if err != nil {
fmt.Println("server unpack err:", err)
return
}
if msgHead.GetDataLen() > 0 {
// The message has data, so we need to read the data part
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())
// Read the data bytes from the stream based on the dataLen
_, err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println("server unpack data err:", err)
return
}
fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
}
time.Sleep(1 * time.Second)
}
}
The Client0 sends a message with MsgID 0 and the content "Zinx V0.6 Client0 Test Message".
The second client will be implemented in Client1.go
, with the following code:
//Client1.go
package main
import (
"fmt"
"io"
"net"
"time"
"zinx/znet"
)
/*
Simulate client
*/
func main() {
fmt.Println("Client Test ... start")
// Wait for 3 seconds before sending the test request to give the server a chance to start
time.Sleep(3 * time.Second)
conn, err := net.Dial("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("client start err, exit!")
return
}
for {
// Pack the message
dp := znet.NewDataPack()
msg, _ := dp.Pack(znet.NewMsgPackage(1, []byte("Zinx V0.6 Client1 Test Message")))
_, err := conn.Write(msg)
if err != nil {
fmt.Println("write error err ", err)
return
}
// Read the head part from the stream
headData := make([]byte, dp.GetHeadLen())
_, err = io.ReadFull(conn, headData) // ReadFull fills the buffer until it's full
if err != nil {
fmt.Println("read head error")
break
}
// Unpack the headData into a message
msgHead, err := dp.Unpack(headData)
if err != nil {
fmt.Println("server unpack err:", err)
return
}
if msgHead.GetDataLen() > 0 {
// The message has data, so we need to read the data part
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())
// Read the data bytes from the stream based on the dataLen
_, err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println("server unpack data err:", err)
return
}
fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
}
time.Sleep(1 * time.Second)
}
}
The Client1
sends a message with MsgID 1 and the content "Zinx V0.6 Client1 Test Message".
To run the server and the two clients, execute the following commands in three different terminals:
$ go run Server.go
$ go run Client0.go
$ go run Client1.go
The server output will be as follows:
$ go run Server.go
Add api msgId = 0
Add api msgId = 1
[START] Server name: zinx v-0.6 demoApp, listenner at IP: 127.0.0.1, Port 7777 is starting
[Zinx] Version: V0.4, MaxConn: 3, MaxPacketSize: 4096
start Zinx server zinx v-0.6 demoApp succ, now listenning...
Reader Goroutine is running
Call PingRouter Handle
recv from client: msgId= 0, data= Zinx V0.6 Client0 Test Message
Reader Goroutine is running
Call HelloZinxRouter Handle
recv from client: msgId= 1, data= Zinx V0.6 Client1 Test Message
Call PingRouter Handle
recv from client: msgId= 0, data= Zinx V0.6 Client0 Test Message
Call HelloZinxRouter Handle
recv from client: msgId= 1, data= Zinx V0.6 Client1 Test Message
Call PingRouter Handle
recv from client: msgId= 0, data= Zinx V0.6 Client0 Test Message
Call HelloZinxRouter Handle
recv from client: msgId= 1, data= Zinx V0.6 Client1 Test Message
// ...
The Client0 output will be as follows:
$ go run Client0.go
Client Test ... start
==> Recv Msg: ID= 0, len= 18, data= ping...ping...ping
==> Recv Msg: ID= 0, len= 18, data= ping...ping...ping
==> Recv Msg: ID= 0, len= 18, data= ping...ping...ping
// ...
The Client1 output will be as follows:
$ go run Client1.go
Client Test ... start
==> Recv Msg: ID= 1, len= 22, data= Hello Zinx Router V0.6
==> Recv Msg: ID= 1, len= 22, data= Hello Zinx Router V0.6
==> Recv Msg: ID= 1, len= 22, data= Hello Zinx Router V0.6
// ...
From the results, it can be observed that the server code is now able to handle different message types and perform different logic based on the message ID. Client0 receives only "ping...ping...ping" as a reply, while Client1 receives only "Hello Zinx Router V0.6" as a reply.
7.4 Summary
In conclusion, up to Zinx V0.6, it is possible to handle different business logic based on different message IDs. Zinx provides a basic framework for server-side network communication, allowing developers to define message types, register different handle functions based on the message types, and add them to the server service object using AddRouter(), thus enabling server-side business development capabilities. The next step would be to further upgrade the internal module structure handling in Zinx.
source code
https://github.com/aceld/zinx/blob/master/examples/zinx_release/zinx-v0.6.tar.gz
[Zinx]
<1.Building Basic Services with Zinx Framework>
<2. Zinx-V0.2 Simple Connection Encapsulation and Binding with Business>
<3.Design and Implementation of the Zinx Framework's Routing Module>
<4.Zinx Global Configuration>
<5.Zinx Message Encapsulation Module Design and Implementation>
<6.Design and Implementation of Zinx Multi-Router Mode>
<7. Building Zinx's Read-Write Separation Model>
<8.Zinx Message Queue and Task Worker Pool Design and Implementation>
<9. Zinx Connection Management and Property Setting>
[Zinx Application - MMO Game Case Study]
<10. Application Case Study using the Zinx Framework>
<11. MMO Online Game AOI Algorithm>
<12.Data Transmission Protocol: Protocol Buffers>
<13. MMO Game Server Application Protocol>
<14. Building the Project and User Login>
<15. World Chat System Implementation>
<16. Online Location Information Synchronization>
<17. Moving position and non-crossing grid AOI broadcasting>
<18. Player Logout>
<19. Movement and AOI Broadcast Across Grids>
Author:
discord: https://discord.gg/xQ8Xxfyfcz
zinx: https://github.com/aceld/zinx
github: https://github.com/aceld
aceld's home: https://yuque.com/aceld
Top comments (0)