DEV Community

Cover image for 1.Building Basic Services with Zinx Framework
Aceld
Aceld

Posted on • Edited on

1.Building Basic Services with Zinx Framework

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


Currently, there are many application frameworks in Go for servers, but there are few lightweight enterprise frameworks that can be applied in the gaming or other long connection fields. The author designed Zinx to enable developers who are learning Go to understand the overall outline of writing a TCP server based on Go through the Zinx framework. This will allow more Go enthusiasts to learn and understand this field in a simple and easy-to-understand way.

The Zinx framework project is created synchronously with coding and learning tutorials, with all development progress and iterative thinking incorporated into the tutorials. Instead of throwing a very complete framework for everyone to learn, it is easy to confuse many people who do not know how to get started.

In this chapter, the content of the Zinx framework construction will be iterated version by version, and the addition of each version's function will be minimal. This way, even if the reader is a novice in the service framework, they will gradually understand the field of server frameworks through a curve-linear learning approach.

Tip: We hope that more people will join Zinx and provide valuable feedback. Thank you for your attention!

Zinx Source Code:
GitHub Address: https://github.com/aceld/zinx
Gitee (Open Source China) Address: https://gitee.com/Aceld/zinx


1.1 Exploring the Zinx Architecture

The architecture design of Zinx is very simple and consists of three major parts. The first part is the design of service communication, as shown in the figure above. Zinx uses a read-write separation approach to process messages and perform basic data reading and writing. The read data is handed over to the second part, the message work pool, for concurrent processing. For the upper layer business of data, the Worker work pool will register many different business callback actions, which are represented by APIs in the figure. These are the collections of different messages that developers have registered to handle different businesses in the upper layer business layer, which is the third part. The architecture diagram is the initial design idea, as shown in Figure 1.1.

Figure 1.1

Figure 1.1

The running effect of a complete Zinx server during startup is shown in Figure 1.2.

Figure 1.2

Figure 1.2

1.2 Zinx-V0.1 Basic Server

At the beginning of the framework, the file structure of the current project should be determined first. Zinx builds the two basic modules of ziface and znet.
(1) ziface mainly stores the abstract interface classes of all modules in the Zinx framework. The most basic service interface is the iserver, which is defined in the ziface module.
(2) The znet module is the implementation of network-related functions in the Zinx framework, and all network-related modules will be defined in the znet module.

1.2.1 Zinx-V0.1 Code Implementation

1. Create the Zinx framework

Create a zinx folder under $GOPATH/src, which serves as the project root directory for the Zinx project.

2. Create the ziface and znet modules

Create the ziface and znet folders under zinx/, so that the current file path looks like this:

└── zinx
  ├── ziface
  │ └──
  └── znet
    ├──
Enter fullscreen mode Exit fullscreen mode

3. Create the service module abstraction layer iserver.go in ziface

//zinx/ziface/iserver.go
package ziface

// Define the server interface
type IServer interface{
    // Start the server method
    Start()
    // Stop the server method
    Stop()
    // Start the business service method
    Serve()
}
Enter fullscreen mode Exit fullscreen mode

Here, the IServer interface is defined first, which provides three methods for the server to implement later. The Start() method is a method to start a communication service, Stop() is a method to stop the service, and Serve() is a method to start the overall business service.

4. Implement the service module server.go under znet.

After defining the Iserver interface, the implementation layer can implement the basic interface of this service. The code is as follows:

//zinx/znet/server.go
package znet

import (
    "fmt"
    "net"
    "time"
    "zinx/ziface"
)

// Implementation of the iServer interface, defining a Server service class.
type Server struct {
    // Name of the server.
    Name string
    // TCP4 or other.
    IPVersion string
    // IP address that the service is bound to.
    IP string
    // Port that the service is bound to.
    Port int
}
Enter fullscreen mode Exit fullscreen mode

The Server structure is defined, and its attributes include:

(1) Name: the name of the current service. Although Name has no actual effect, it helps developers use it for logging or comparing service versions in the future.

(2) IPVersion: the version number of the current IP protocol. Currently, Zinx uses IPv4, and this parameter is one of the parameters provided to the Go network programming standard library.

(3) IP: the string type IP address, using dotted decimal notation (such as 192.168.1.101, etc.).

(4) Port: the integer port number.

Next, we will implement the three abstract interfaces Start(), Stop(), and Serve() in the IServer interface. The implementation of the Start() method is as follows:

//zinx/znet/server.go

// Start the network service.
func (s *Server) Start() {
    fmt.Printf("[START] Server listener at IP: %s, Port %d, is starting\n", s.IP, s.Port)

    // Start a goroutine to do the server Linster business.
    go func() {
        // 1. Get a TCP address.
        addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
        if err != nil {
            fmt.Println("resolve tcp addr err: ", err)
            return

        }

        // 2. Listen to the server address.
        listener, err := net.ListenTCP(s.IPVersion, addr)
        if err != nil {
            fmt.Println("listen", s.IPVersion, "err", err)
            return

        }

        // The server has successfully started listening.
        fmt.Println("start Zinx server  ", s.Name, " succ, now listening...")

        // 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 Server.Start() sets the server's maximum connection control. If the maximum connection is exceeded, then close this new connection.

            // 3.3. TODO Server.Start() handles the business method of the new connection request. At this time, handler and conn should be bound.

            // We will temporarily create an echo service with a maximum of 512 bytes.
            go func() {
                // Continuously loop to get data from the client.
                for {
                    buf := make([]byte, 512)
                    cnt, err := conn.Read(buf)
                    if err != nil {
                        fmt.Println("recv buf err ", err)
                        continue

                    }
                    // Echo back the received data.
                    if _, err := conn.Write(buf[:cnt]); err != nil {
                        fmt.Println("write back buf err ", err)
                        continue

                    }

                }

            }()

        }

    }()
}
Enter fullscreen mode Exit fullscreen mode

The Start() method actually creates a Goroutine to handle the listening ability of the Socket. This new Goroutine will loop forever, and if there is a new connection coming in, the Accept() block will return, the server and client connection will be established, and a new connection "conn" will be obtained. Then another Goroutine will be created to handle the data reading and writing of this established connection "conn". It implements a very simple application-layer business, echoing back the data input by the client by writing it back to the client through the current connection "conn". When all the input data from the client is read, conn.Read() will read EOF, and "err" will not be nil, and the current loop will exit, and the business Goroutine will exit. These two Goroutines are asynchronous, so while handling the client connection data, it will not block the server from continuing to listen for the creation of other new connections.

This is due to the inherent nature of Goroutines and concurrency in Golang. Developers do not have to worry about the number of business Goroutines because the Goroutine scheduler in Golang has already optimized the switching cost of the underlying physical threads, which is the advantage of writing server programs in Golang.

Next, let's take a look at the Stop() method, which is implemented as follows:

//zinx/znet/server.go

func (s *Server) Stop() {
    fmt.Println("[STOP] Zinx server , name " , s.Name)
    //TODO  Server.Stop() needs to stop or clean up other connection information or other information that needs to be cleared.
}
Enter fullscreen mode Exit fullscreen mode

Currently, Stop() only prints a log message, and the cleaning function will be added as a TODO in the future.

Now let's take a look at the last interface of IServer, the implementation of the Serve() method is as follows:

//zinx/znet/server.go

func (s *Server) Serve() {
    s.Start()

    select{}
}
Enter fullscreen mode Exit fullscreen mode

Serve() is actually a wrapper for Start(), and its purpose is to separate the overall startup of the server from the individual startup of the service listening function. This also facilitates developers to consider whether to add additional logic when starting the service in the future by adding it to Serve().

At the end of the Server(), there is a code block that permanently blocks using select{}, which prevents the current Go routine from exiting and causing the Go routine of Start() to exit.

Next, let's provide the constructor of the Server class, which is the public interface for creating a Server object. Generally, it will start with the method name New. The specific implementation is as follows:


//zinx/znet/server.go


func NewServer (name string) ziface.IServer {
    s:= &Server {
        Name :name,
        IPVersion:"tcp4",
        IP:"0.0.0.0",
        Port:7777,
    }

    return s
}


Enter fullscreen mode Exit fullscreen mode

Alright, we have completed the basic framework of Zinx-V0.1, although it only involves basic functionality of echoing back client data (we will customize client business methods later). Now, it's time to test if Zinx-V0.1 is functional.

1.2.2 Zinx Framework Unit Test Example

Now we can import the Zinx framework and write a server program and a client program for testing. Here we use the Go unit test function to perform unit tests. First, create a file named "server_test.go" under the zinx/znet/ folder, and the code is as follows:

//zinx/znet/server_test.go
package znet

import (
    "fmt"
    "net"
    "testing"
    "time"
)

/*
    Simulate the client
*/
func ClientTest() {

    fmt.Println("Client Test ... start")

    // Wait for 3 seconds to give the server a chance to start its service
    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 {
        _, err := conn.Write([]byte("hello ZINX"))
        if err != nil {
            fmt.Println("write error err ", err)
            return
        }

        buf := make([]byte, 512)
        cnt, err := conn.Read(buf)
        if err != nil {
            fmt.Println("read buf error ")
            return
        }

        fmt.Printf(" server call back : %s, cnt = %d\n", buf, cnt)

        time.Sleep(1 * time.Second)
    }
}

// Test function for the Server module
func TestServer(t *testing.T) {

    /*
        Server test
    */
    // 1. Create a server handle s
    s := NewServer("[zinx V0.1]")

    /*
        Client test
    */
    go ClientTest()

    // 2. Start the service
    s.Serve()
}

Enter fullscreen mode Exit fullscreen mode

The part of simulating the client is in the ClientTest() function, which is simple logic: first, connect to the IP address and port number that the server is listening to, and get a new connection conn. Then, in the loop, every one second, send the string "hello ZINX" to the server, and then block and wait for the response data from the server, print the log, and continue the loop.

TestServer() is a unit test method. First, it starts a new server with the name "[zinx V0.1]". Then it launches a Goroutine to start a simulated client, and finally the server starts its service.

To perform unit testing, execute the following code in the zinx/znet directory:
$ go test
The execution result is as follows:

[START] Server listenner at IP: 0.0.0.0, Port 7777, is starting
Client Test ... start
listen tcp4 err listen tcp4 0.0.0.0:7777: bind: address already in use
server call back : hello ZINX, cnt = 6
server call back : hello ZINX, cnt = 6
server call back : hello ZINX, cnt = 6
server call back : hello ZINX, cnt = 6
Enter fullscreen mode Exit fullscreen mode

The simplest Zinx framework can now be used.

1.2.3 Building an Application with Zinx-V0.1

If you find running the "go test" command for unit testing cumbersome, you can also build two applications, Server.go and Client.go, entirely based on Zinx, as shown below:

//Server.go
package main

import (
    "zinx/znet"
)

//Server module's test function
func main() {

    //1 create a server handle s
    s := znet.NewServer("[zinx V0.1]")

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

Server.go is the backend program developed using the Zinx framework, with a main() function and an import of the zinx/znet package. Building a server using Zinx framework currently only requires two steps: creating a Server object and starting the server.

To start Server.go, use the following command:

go run Server.go
Enter fullscreen mode Exit fullscreen mode

Next, let's implement the code for Client.go, as follows:

//Client.go
package main

import (
    "fmt"
    "net"
    "time"
)

func main() {

    fmt.Println("Client Test ... start")
    //After 3 seconds, initiate a test request to give the server a chance to start the service
    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 {
    _, err := conn.Write([]byte("hahaha"))
    if err !=nil {
        fmt.Println("write error err ", err)
        return
    }

    buf :=make([]byte, 512)
    cnt, err := conn.Read(buf)
    if err != nil {
        fmt.Println("read buf error ")
        return
    }

    fmt.Printf(" server call back : %s, cnt = %d\n", buf,  cnt)

    time.Sleep(1*time.Second)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, the code for the client connection process is implemented in the main() function, which is almost the same as the simulated client code in the unit test. Start Client.go for testing with the following command:

go run Client.go
Enter fullscreen mode Exit fullscreen mode

This way, even without unit testing, you can test the use of the Zinx framework, which produces the same results as the unit test results described earlier.


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