[Zinx]
<1.Building Basic Services with Zinx Framework>
<2. Zinx-V0.2 Simple Connection Encapsulation and Binding with Business>
<3.Design and Implementation of the Zinx Framework's Routing Module>
<4.Zinx Global Configuration>
<5.Zinx Message Encapsulation Module Design and Implementation>
<6.Design and Implementation of Zinx Multi-Router Mode>
<7. Building Zinx's Read-Write Separation Model>
<8.Zinx Message Queue and Task Worker Pool Design and Implementation>
<9. Zinx Connection Management and Property Setting>
[Zinx Application - MMO Game Case Study]
<10. Application Case Study using the Zinx Framework>
<11. MMO Online Game AOI Algorithm>
<12.Data Transmission Protocol: Protocol Buffers>
<13. MMO Game Server Application Protocol>
<14. Building the Project and User Login>
<15. World Chat System Implementation>
<16. Online Location Information Synchronization>
<17. Moving position and non-crossing grid AOI broadcasting>
<18. Player Logout>
<19. Movement and AOI Broadcast Across Grids>
source code
https://github.com/aceld/zinx/blob/master/examples/zinx_release/zinx-v0.3.tar.gz
In Zinx, it is necessary to provide users with a customized connection handling interface. Clearly, binding the business handling methods to the format type HandFunc func(*net.TCPConn, []byte, int)
error is not sufficient. Here, we need to define some interface{}
to allow users to provide connection handling methods in any format.
Using only the func
type clearly cannot meet the development requirements. Now, we need to create several abstract interface classes.
3.1 IRequest - Abstract Class for Message Requests
In this section, we will combine the connection information and request data of a client's request into a request class called Request. The benefit of this approach is that we can obtain all the client's request information from Request, which also serves as a basis for future framework expansion. If the client has additional meaningful data, it can be stored in this Request class. It can be understood that every time the client sends a complete request, Zinx will put them together in a Request structure.
3.1.1 Creating the Abstract IRequest Layer
Create a new file irequest.go in the ziface directory. This file represents the IRequest interface and is located in the abstract layer directory of Zinx (ziface). The interface is defined as follows:
//zinx/ziface/irequest.go
package ziface
/*
IRequest interface:
This interface encapsulates the client's connection
information and request data into a Request.
*/
type IRequest interface {
GetConnection() IConnection // Get the connection information of the request
GetData() []byte // Get the data of the request message
}
It is evident that the current abstraction layer only provides two getter methods, indicating that two members must be present. One is the client's connection, and the other is the data passed by the client. Of course, as the Zinx framework becomes more feature-rich, new members should be added to it.
Note the interface design of GetConnection() in IRequest. It returns IConnection instead of Connection. Although it has no impact on program implementation when returning the latter, considering design patterns and architectural scalability, it is recommended that the interface in the abstraction layer still depends on the abstraction layer. It is primarily about programming against the abstraction layer, so the returned concrete Connection object only needs to be a subclass of IConnection. This also demonstrates the application of the polymorphism feature of object-oriented programming. Therefore, the design of this interface should comply with the "Liskov Substitution Principle."
3.1.2 Implementation of the Request class
Create a new file named request.go
in the znet directory, which will primarily implement the Request class of Zinx. The code is as follows:
// zinx/znet/request.go
package znet
import "zinx/ziface"
type Request struct {
conn ziface.IConnection // The established connection with the client
data []byte // Data requested by the client
}
// GetConnection retrieves the connection information of the request
func (r *Request) GetConnection() ziface.IConnection {
return r.conn
}
// GetData retrieves the data of the request message
func (r *Request) GetData() []byte {
return r.data
}
Now the Request class is created and it will be used later when configuring the router.
3.2 IRouter - Abstract Router Configuration
In this section, we will implement a very basic routing functionality in Zinx, aiming to quickly introduce routing into the framework.
3.2.1 Creating the Abstract IRouter Layer
Create a file named irouter.go
in the ziface
directory. This file will define the interface for the routing functionality. The code implementation is as follows:
// zinx/ziface/irouter.go
package ziface
/*
Router interface.
Routers are used by framework users to configure custom business methods for a connection.
The IRequest in the router contains the connection information and the request data of that connection.
*/
type IRouter interface {
PreHandle(request IRequest) // Hook method executed before handling the conn business
Handle(request IRequest) // Method to handle the conn business
PostHandle(request IRequest) // Hook method executed after handling the conn business
}
The role of the Router is to allow the server application to configure the handling business method for a particular connection. In previous versions of Zinx (V0.2), the method for handling connection requests was fixed. Now, it can be customized, and there are three interfaces that can be overridden:
-
Handle
: This is the main business function for handling the current connection. -
PreHandle
: If there is a need for pre-processing before the main business function, this method can be overridden. -
PostHandle
: If there is a need for post-processing after the main business function, this method can be overridden.
Each method has a unique parameter, IRequest
object, which represents the connection and request data coming from the client, and serves as the input data for the business methods.
3.2.2 Implementing the Router Class
Create a file named router.go
in the znet
directory. This file contains the implementation of the Router
class. The code is as follows:
// zinx/znet/router.go
package znet
import "zinx/ziface"
// When implementing a router, embed this base class and override its methods as needed
type BaseRouter struct{}
func (br *BaseRouter) PreHandle(req ziface.IRequest) {}
func (br *BaseRouter) Handle(req ziface.IRequest) {}
func (br *BaseRouter) PostHandle(req ziface.IRequest) {}
The BaseRouter
class serves as the parent class for all subclasses implementing the Router
. It implements the three interfaces of IRouter
, but the method implementations in BaseRouter
are empty. This is because some implementation layer routers may not require PreHandle
or PostHandle
methods. By inheriting BaseRouter
, it is not necessary to implement these methods in order to instantiate a router.
At this point, the directory structure of Zinx should look as follows:
.
├── README.md
├── ziface
│ ├── iconnection.go
│ ├── irequest.go
│ ├── irouter.go
│ └── iserver.go
└── znet
├── connection.go
├── request.go
├── router.go
├── server.go
└── server_test.go
3.3 Integrating Basic Routing Functionality into Zinx-V0.3
Now that IRequest
and IRouter
have been defined, the next step is to integrate them into the Zinx framework.
3.3.1 Adding Router Registration Functionality to IServer
The IServer
interface needs to add an abstract method AddRouter()
. The purpose is to allow users of the Zinx framework to customize a router for handling business methods. The code is as follows:
// zinx/ziface/iserver.go
package ziface
// Definition of the server interface
type IServer interface {
// Start the server
Start()
// Stop the server
Stop()
// Serve the business services
Serve()
// Router function: Register a router business method for the current server to handle client connections
AddRouter(router IRouter)
}
The AddRouter()
method takes an IRouter
as its parameter, which is an abstract layer and not a concrete implementation.
3.3.2 Adding Router Member to Server Class
With the abstract method in place, the Server
class needs to implement it and add a Router member. The modified Server
data structure is as follows:
// zinx/znet/server.go
// Implementation of the iServer interface, defining a Server service class
type Server struct {
// Server name
Name string
// IP version (tcp4 or other)
IPVersion string
// IP address that the service binds to
IP string
// Port that the service binds to
Port int
// Callback router bound by the user for the current Server, which is responsible for handling business for registered connections
Router ziface.IRouter
}
Correspondingly, the NewServer()
method should include default initialization and assignment for the Router
:
// zinx/znet/server.go
/*
Create a server handle
*/
func NewServer(name string) ziface.IServer {
s := &Server{
Name: name,
IPVersion: "tcp4",
IP: "0.0.0.0",
Port: 7777,
Router: nil, // Default not specified
}
return s
}
The Server
class needs to implement the AddRouter()
method to add the router. Here, it simply needs to be registered in the s.Router
member. The AddRouter()
method is used by developers for business functionality. The code is as follows:
// zinx/znet/server.go
// Router function: Register a router business method for the current server to handle client connections
func (s *Server) AddRouter(router ziface.IRouter) {
s.Router = router
fmt.Println("Add Router success!")
}
3.3.3 Binding a Router Member to the Connection Class
Since the Server
has integrated the Router functionality, the Connection
class also needs to be associated with the Router. Modify the data structure of the Connection implementation class and add the Router
member. The code modifications are as follows:
// zinx/znet/connection.go
type Connection struct {
// TCP socket for the current connection
Conn *net.TCPConn
// Connection ID, also known as SessionID, globally unique
ConnID uint32
// Connection close status
isClosed bool
// Router for handling the connection
Router ziface.IRouter
// Channel to notify that the connection has exited/stopped
ExitBuffChan chan bool
}
When creating a new connection in NewConnection()
, the Router
parameter needs to be passed. Modify the code as follows:
// zinx/znet/connection.go
// Method to create a connection
func NewConnection(conn *net.TCPConn, connID uint32, router ziface.IRouter) *Connection {
c := &Connection{
Conn: conn,
ConnID: connID,
isClosed: false,
Router: router,
ExitBuffChan: make(chan bool, 1),
}
return c
}
3.3.4 Calling the Registered Router to Handle Business in Connection
After integrating the Router member into the Connection
class, the registered Router can be called in the logic of StartReader()
that handles the data reading. The following code adds the invocation of the Router after reading the data:
// zinx/znet/connection.go
func (c *Connection) StartReader() {
fmt.Println("Reader Goroutine is running")
defer fmt.Println(c.RemoteAddr().String(), " conn reader exit!")
defer c.Stop()
for {
// Read the maximum data into the buffer
buf := make([]byte, 512)
_, err := c.Conn.Read(buf)
if err != nil {
fmt.Println("recv buf err ", err)
c.ExitBuffChan <- true
continue
}
// Get the Request data of the current client request
req := Request{
conn: c,
data: buf,
}
// Find the corresponding Handle registered with the bound Conn from the Routers
go func(request ziface.IRequest) {
// Execute the registered router methods
c.Router.PreHandle(request)
c.Router.Handle(request)
c.Router.PostHandle(request)
}(&req)
}
}
After reading the client data in the StartReader()
method of the connection, the data and the connection are encapsulated in a Request
as input data for the Router:
// Get the Request data of the current client request
req := Request{
conn: c,
data: buf,
}
Then, a Goroutine is started to invoke the registered router business logic in the Zinx framework:
// Find the corresponding Handle registered with the bound Conn from the Routers
go func(request ziface.IRequest) {
// Execute the registered router methods
c.Router.PreHandle(request)
c.Router.Handle(request)
c.Router.PostHandle(request)
}(&req)
If the Router has overridden PreHandle()
, it will be called; otherwise, the empty method of BaseRouter()
will be called. The same logic applies to Handle()
and PostHandle()
.
3.4 Passing the Router Parameter to Connection in Server
After the Server successfully establishes a new connection, a new Connection needs to be created, and the current Router parameter should be passed to the connection. The main modification is in the NewConnection()
method. The relevant key code changes are as follows:
// zinx/znet/server.go
package znet
import (
"fmt"
"net"
"time"
"zinx/ziface"
)
// Start the network service
func (s *Server) Start() {
// ... (partial code omitted)
// Start a goroutine to handle server listener business
go func() {
// 1. Get a TCP Addr
// ... (partial code omitted)
// 2. Listen to the server address
// ... (partial code omitted)
// 3. Start the server network connection business
for {
// 3.1 Block and wait for client connection requests
conn, err := listener.AcceptTCP()
if err != nil {
fmt.Println("Accept err ", err)
continue
}
// 3.2 TODO: Set the server's maximum connection control in Server.Start()
// If the maximum connection is exceeded, close this new connection
// 3.3 Handle the business method of the new connection request
// The handler and conn should be bound at this point
dealConn := NewConnection(conn, cid, s.Router)
cid++
// 3.4 Start handling the business of the current connection
go dealConn.Start()
}
}()
}
3.5 Completing the Application with Zinx-V0.3
Now, with Zinx, a simple routing functionality can be configured.
3.5.1 Testing the Server Application Built with Zinx
Now let's continue with the development of the Server.go application. Developers need to define a custom Router class and implement the PreHandle(), Handle(), and PostHandle() methods to handle specific business logic for data requests in Zinx. The code is as follows:
// Server.go
package main
import (
"fmt"
"zinx/ziface"
"zinx/znet"
)
// Ping test custom router
type PingRouter struct {
znet.BaseRouter // Must embed BaseRouter first
}
// Test PreHandle
func (this *PingRouter) PreHandle(request ziface.IRequest) {
fmt.Println("Call Router PreHandle")
_, err := request.GetConnection().GetTCPConnection().Write([]byte("before ping ....\n"))
if err != nil {
fmt.Println("callback ping ping ping error")
}
}
// Test Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
fmt.Println("Call PingRouter Handle")
_, err := request.GetConnection().GetTCPConnection().Write([]byte("ping...ping...ping\n"))
if err != nil {
fmt.Println("callback ping ping ping error")
}
}
// Test PostHandle
func (this *PingRouter) PostHandle(request ziface.IRequest) {
fmt.Println("Call Router PostHandle")
_, err := request.GetConnection().GetTCPConnection().Write([]byte("After ping .....\n"))
if err != nil {
fmt.Println("callback ping ping ping error")
}
}
func main() {
// Create a server instance
s := znet.NewServer("[zinx V0.3]")
s.AddRouter(&PingRouter{})
// Start the server
s.Serve()
}
The above code defines a custom router similar to the Ping operation. When the client sends data, the server's business logic is to return "ping...ping...ping" to the client. To test it, the current router also implements the PreHandle() and PostHandle() methods. In practice, Zinx will use the template method design pattern to call the PreHandle(), Handle(), and PostHandle() methods sequentially in the framework.
3.5.2 Starting the Server and Client
1 Start Server.go
Execute the following command to start the Server program with the registered Router:
go run Server.go
2 Start Client.go
The client code in Client.go remains unchanged from the previous version. Execute the following command to run the program:
go run Client.go
3 Server Output
$ go run Server.go
Add Router succ!
[START] Server listener at IP: 0.0.0.0, Port 7777, is starting
start Zinx server [zinx V0.3] succ, now listening...
Reader Goroutine is running
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
...
4 Client Output
$ go run Client.go
Client Test ... start
Server callback: before ping ...., cnt = 17
Server callback: ping...ping...ping
After ping ....., cnt = 36
Server callback: before ping ....
ping...ping...ping
After ping ....., cnt = 53
Server callback: before ping ....
ping...ping...ping
After ping ....., cnt = 53
Server callback: before ping ....
ping...ping...ping
After ping ....., cnt = 53
...
3.6 Conclusion
Now Zinx framework has routing functionality, although currently only one Router can be configured. However, Zinx now allows developers to freely configure business logic. In the next steps, Zinx will add the ability to configure multiple routers.
source code
https://github.com/aceld/zinx/blob/master/examples/zinx_release/zinx-v0.3.tar.gz
--
[Zinx]
<1.Building Basic Services with Zinx Framework>
<2. Zinx-V0.2 Simple Connection Encapsulation and Binding with Business>
<3.Design and Implementation of the Zinx Framework's Routing Module>
<4.Zinx Global Configuration>
<5.Zinx Message Encapsulation Module Design and Implementation>
<6.Design and Implementation of Zinx Multi-Router Mode>
<7. Building Zinx's Read-Write Separation Model>
<8.Zinx Message Queue and Task Worker Pool Design and Implementation>
<9. Zinx Connection Management and Property Setting>
[Zinx Application - MMO Game Case Study]
<10. Application Case Study using the Zinx Framework>
<11. MMO Online Game AOI Algorithm>
<12.Data Transmission Protocol: Protocol Buffers>
<13. MMO Game Server Application Protocol>
<14. Building the Project and User Login>
<15. World Chat System Implementation>
<16. Online Location Information Synchronization>
<17. Moving position and non-crossing grid AOI broadcasting>
<18. Player Logout>
<19. Movement and AOI Broadcast Across Grids>
Author:
discord: https://discord.gg/xQ8Xxfyfcz
zinx: https://github.com/aceld/zinx
github: https://github.com/aceld
aceld's home: https://yuque.com/aceld
Top comments (0)