-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
07 Zinx RouterSlice
Case Source Code : https://github.com/aceld/zinx-usage/tree/main/zinx_router
In Zinx, the original routing abstraction has been converted into a function type:
// A routing function that includes the request body Requset
type RouterHandler func(request IRequest)
This function is identical to the routing function that needed to be implemented in the original code, Handle()
, except that it does not require the construction of an empty structure. Currently, to use the new version of the routing, the RouterSlicesMode
configuration must be set to True
.
The RouterSlices mode of Zinx introduces significant differences from the previous version, as it supports adding one or more functions to a route at a time, and it simplifies route management while providing greater flexibility for business operations.The Zinx RouterSlices mode has two parts in its route interface definition, the route itself and the route group manager.
Route manager itself: First and foremost, it should be noted that the methods of IRouterSlices are not directly exposed, but are wrapped into methods of the Server and can be obtained through the router itself. There are three primary methods of the route itself, as follows:
type IRouterSlices interface {
// Add global components
Use(Handlers ...RouterHandler)
// Add a route
AddHandler(msgId uint32, handlers ...RouterHandler)
// Router group management, and return a group router manager
Group(start, end uint32, Handlers ...RouterHandler) IGroupRouterSlices
// Get the method set collection for processing
GetHandlers(MsgId uint32) ([]RouterHandler, bool)
}
The essence of the Use() method is to add one or more specified functions to all current routes. Use() can be used multiple times for more flexible middleware configuration. For example, if we need to perform authentication on all routes, we can use Use() to add this middleware to all routes. In the following example, route 1 will first execute Auth1 and then execute TestFunc:
func Auth1(request ziface.IRequest) {
// Verify business, default to pass.
fmt.Println("I am the Auth1, I will always pass.")
}
// I am a business function.
func TestFunc(request ziface.IRequest) {
fmt.Println("I am a business function.")
}
func main() {
server.Use(Auth1)
server.AddRouterSlices(1, TestFunc)
server.Serve()
}
There may be some special cases where some routes do not need or do not want to check permissions. In this case, we need to adjust the location of Use(). In the following code, use is in the middle of routes 1 and 2, so only the routes below 2 will be affected. That is to say, Use() will only affect all routes after it is executed. Route 1 before Use() will not be affected and only TestFunc will be executed. Similarly, put all routes that do not need authentication at the top:
func Auth1(request ziface.IRequest) {
// Verify business, default to pass.
fmt.Println("I am the Auth1, I will always pass.")
}
// I am a business function.
func TestFunc(request ziface.IRequest) {
fmt.Println("I am a business function.")
}
func main() {
// Not affected by Use()
server.AddRouterSlices(1, TestFunc)
server.Use(Auth1)
// Affected by Use()
server.AddRouterSlices(2, TestFunc)
//... Other routes
server.Serve()
}
If you use Protobuf3 as the format protocol for front-end and back-end communication, you may have a more classic requirement, that is, to deserialize protobuf3 messages. If each route needs to be parsed, it is better to write it as a public component as follows:
func Auth1(request ziface.IRequest) {
// Verify business, default to pass.
fmt.Println("I am the Auth1, I will always pass.")
}
func ProtoUnmarshal(request ziface.IRequest) {
// Decode your data
data:=decode(request.Data)
// Protobuf deserialization
pb := &SocketGameProtocol.MainPack{}
proto.Unmarshal(data, pb)
// Bind to Response for subsequent operations
request.SetResponse(data)
}
// I am a business function.
func TestFunc(request ziface.IRequest) {
fmt.Println("I am a business function.")
}
func main() {
server.Use(Auth1,ProtoUnmarshal)
server.AddRouterSlices(1, TestFunc)
server.Serve()
}
As mentioned earlier, Use can add multiple components. Route 1 will still execute Auth1 first, then ProtoUnmarshal, and finally TestFunc.
The AddHandler method is used to add functions and is not directly exposed to the outside. It is wrapped in the AddRouterSlices method of the Server. Other than the different parameters, it is the same as the old version.
AddRouterSlices(msgID uint32, router ...RouterHandler) IRouterSlices
From the return value, we can see that it returns an IRouterSlices, which can be used to manually call the AddHandler method. However, this is not recommended.
AddRouterSlices supports adding one or more functions, which allows adding middleware to a specific route. This is very useful when you only need to validate or perform pre-processing for certain operations. For example, route 1 executes the business function, while route 2 executes the Auth1 function before the route function. However, it is important to note the order when adding them.
func Auth1(request ziface.IRequest) {
// Verify business, default to pass.
fmt.Println("I am the Auth1, I will always pass.")
}
// I am a business function.
func TestFunc(request ziface.IRequest) {
fmt.Println("I am a business function.")
}
func main() {
server.AddRouterSlices(1, TestFunc)
server.AddRouterSlices(2, Auth1,TestFunc)
server.Serve()
}
Adding multiple functions is useful when you have a small number of authentication requirements. If multiple routes require authentication or other similar operations, it is recommended to use the Use method to add them all at once.
There is another scenario where some route business requires function A, while others require function B. In this case, it is not feasible to simply use Use. You can manually add them one by one as mentioned above, or you can use the Group
method mentioned below.
Similarly wrapped in the Server, it returns a router group manager and allows registering middleware with one or more functions.
Group(start, end uint32, Handlers ...RouterHandler) IGroupRouterSlices
The main purpose of router grouping is twofold. First, it improves the readability of the routes. Second, it addresses the aforementioned problem. If you need to implement two different functionalities, such as picking up items and purchasing items, and both require validation, they will definitely be different. Suppose the item-related operations require checking the backpack, while the movement-related operations require checking the character's status or the map.
In this case, writing two components and using Use is not sufficient. Using AddRouterSlices to add the components to each corresponding route can work, but there is a better approach: using Group to group the operations.
In the following code, assuming that all operations require authentication, we first use Auth1 with Use. Then, we group the different route operations and register the necessary components for each group. This way, the components will only affect the routes within that group, and other routes will not be affected.
func Auth1(request ziface.IRequest) {
// Verify business, default to pass.
fmt.Println("I am the Auth1, I will always pass.")
}
func CheckBackpack(request ziface.IRequest) {
//.....your backpack checking operation
}
func CheckMap(request ziface.IRequest) {
//.....your map validation operation for movement
}
// Business logic functions
func BuyitemsFunc(request ziface.IRequest) {
//your purchase items business operation
}
func GetitemsFunc(request ziface.IRequest) {
//your pick up items business operation
}
func MoveFunc(request ziface.IRequest) {
//your character movement business operation
}
func TPFunc(request ziface.IRequest) {
//your character teleportation business operation
}
func main() {
server := znet.NewUserConfServer(&zconf.Config{RouterSlicesMode: true, TCPPort: 8999, Host: "127.0.0.1"})
server.Use(Auth1)
// Items operation group
itemsGroup := server.Group(1, 2, CheckBackpack)
{
// Buy items
itemsGroup.AddHandler(1, BuyitemsFunc)
// Get items
itemsGroup.AddHandler(2, GetitemsFunc)
}
// Movement operation group
moveGroup := server.Group(3, 4, CheckMap)
{
// Move
itemsGroup.AddHandler(3, MoveFunc)
// Teleport
itemsGroup.AddHandler(4, TPFunc)
}
server.Serve()
}
With the above combined operations, routes 1 and 2 will have Auth1, CheckBackpack, and the corresponding business functions, while routes 3 and 4 will have Auth1, CheckMap, and the corresponding business functions.
It is worth mentioning that the grouped routes will also be affected by Use, and the components in Use will always be executed at the beginning. Therefore, if you have a group that should not be affected by Use, you should adjust the position of Use and the group accordingly.
type IGroupRouterSlices interface {
// Add global components
Use(Handlers ...RouterHandler)
// Add group routing components
AddHandler(MsgId uint32, Handlers ...RouterHandler)
}
It provides two methods, Use and AddHandler, which are both exposed methods because the group manager itself is obtained through Server.Group.
Use method It is consistent with the Use method of RouterSlices, but it only affects the routes within the group. In the following example, group1 and group2 are equivalent. Both will add the Auth1 component to routes 1 and 2. However, they are not equivalent to group3, where route 1 does not have the Auth1 component, while route 2 does, due to the position of Use.
group1 := server.Group(1, 2, Auth1)
{
group1.AddHandler(1, TestFunc)
group1.AddHandler(2, TestFunc)
}
group2 := server.Group(1, 2)
{
group2.Use(Auth1)
group2.AddHandler(1, TestFunc)
group2.AddHandler(2, TestFunc)
}
group3 := server.Group(1, 2)
{
group3.AddHandler(1, TestFunc)
group3.Use(Auth1)
group3.AddHandler(2, TestFunc)
}
AddHandler method
It is consistent with the AddHandler method of RouterSlices, but it only affects the routes within the group. If you have grouped the routes, you should only use this method to add functions to the routes within the group.
The purpose of router grouping in RouterSlices is to improve the readability of complex routing scenarios and separate components. It is not a strict requirement, and grouping or not grouping does not affect the original business operations. The range of msgId in the group only serves as a constraint, and it does not have any other impact on the routes within that range. Personally, I believe that grouping with appropriate comments can make the routing functionality clear and provide more granular control over components. It is worth reiterating that grouping is not a strict requirement, and you can achieve the same functionality by using the default method of adding routes.
RouterSlices itself only has these methods, but it still provides route control operations, which will be described in the following sections.
RouterSlices' route control methods are integrated into the request and mainly consist of the following two:
// Execute the next function
RouterSlicesNext()
// Terminate the execution of the processing function, but the function that calls this method will be executed until completion
Abort()
When this method is called, the next route function is executed and returns to the current method after all functions have been executed.
Let's look at two functions as examples. The first function, RouterRecovery, is responsible for catching any panic errors generated by subsequent route functions and recording the context. The second function, RouterTime, simply measures the time required for all route functions to execute.
The RouterRecovery function does nothing else except defer a function to catch panics and then calls RouterSlicesNext() to enter the next route execution. The implementation principle is that by calling RouterSlicesNext(), the subsequent route becomes the current function, which means that although routes are being executed continuously, this function has not yet ended. This is necessary to use defer and receive the error information during a panic.
// RouterRecovery If you use the server obtained from initializing with the NewDefaultRouterSlicesServer method,
// this function will be included. It is used to catch any panics that occur during request handling
// and attempt to record the context information.
func RouterRecovery(request ziface.IRequest) {
defer func() {
if err := recover(); err != nil {
panicInfo := getInfo(StackBegin)
// Record the error
zlog.Ins().ErrorF("MsgId:%d Handler panic: info:%s err:%v", request.GetMsgID(), panicInfo, err)
}
}()
request.RouterSlicesNext()
}
RouterTime is simpler. It records the current time, calls RouterSlicesNext() to proceed with subsequent route operations, and then utilizes the feature of returning to the current method after all functions have been executed. By recording the time again and calculating the difference, you can obtain the total processing time for all routes.
// RouterTime Simply accumulates the time taken by all the routing groups, but not enabled
func RouterTime(request ziface.IRequest) {
now := time.Now()
request.RouterSlicesNext()
duration := time.Since(now)
fmt.Println(duration.String())
}
Abort terminates the route invocation. It only terminates the subsequent functions of the route function that calls this method, while the calling route function will continue to execute until completion.
Let's say we need to time the execution, perform authentication, and then execute the business operation. If there is an authentication error and the user does not match, we can use Abort() to terminate the operation. In the following example, when the user calls the route operation with ID 1, we simulate an authentication failure (Auth) and, as a result, TestFunc will not be executed. Instead, the result of RouterTime() for timing will be returned.
func RouterTime(request ziface.IRequest) {
now := time.Now()
request.RouterSlicesNext()
duration := time.Since(now)
fmt.Println(duration.String())
}
func Auth(request ziface.IRequest) {
// I am the validation handler 2, and by default, I do not allow the request to pass.
// Terminate execution function, no more handlers will be executed after this one.
request.Abort()
fmt.Println("I am Auth, and I will definitely not pass.")
fmt.Println("The business terminates here, and the subsequent handlers will not be executed.")
}
func TestFunc(request ziface.IRequest) {
fmt.Println("I am a function.")
}
func main() {
server := znet.NewUserConfServer(&zconf.Config{RouterSlicesMode: true, TCPPort: 8999, Host: "127.0.0.1"})
server.Use(RouterTime, Auth)
server.AddRouterSlices(1, TestFunc)
server.Serve()
}