SoFunction
Updated on 2025-03-05

Golang: Solution to write Tcp servers

Golang develops Tcp servers and solutions for unpacking and sticking, elegantly shutting down

As a programming language widely used in the server and cloud computing fields, Golang is a crucial feature. You can see the complete code of the TCP server described in this article and its applications in /hdt3213/godis/tcp.

Early Tomcat/Apache servers used a blocking IO model. It uses a thread to process a connection, listening to the thread blocking when no new data is received until the thread is awakened after the data is ready. Because blocking the IO model requires a large number of threads to be turned on and context switches frequently, the efficiency is very poor.

In order to solve the above problems, IO multiplexing technology adopts a thread monitoring multi-connection solution. A thread holds multiple connections and blocks waiting, and the thread is waked up for processing when one of the connections is read and writeable. Because multiple connections multiplex one thread, IO multiplexing requires much fewer threads.

Mainstream operating systems all provide implementations of IO multiplexing technology, such as epoll on Linux, kqueue on freeBSD, and iocp on Windows platform. There are gains and losses, because the interfaces provided by technologies such as epoll are aimed at IO events rather than connections, so complex asynchronous code is required, which is very difficult to develop.

Golang'snetpollerBased on IO multiplexing and goroutine scheduler, a simple and high-performance network model was built, and provided developers withgoroutine-per-connectionA minimalist interface in style.

More AboutnetpollerThe analysis can be used as referenceGolang implements four load balancing algorithms (random, polling, etc.)Next we trynetpollerWrite our server.

Echo Server

As a start, we will implement a simple Echo server. It accepts the client connection and passes the content sent by the client back to the client as it is.

package main

import (
    "fmt"
    "net"
    "io"
    "log"
    "bufio"
)

func ListenAndServe(address string) {
    // Bind the listening address    listener, err := ("tcp", address)
    if err != nil {
        (("listen err: %v", err))
    }
    defer ()
    (("bind: %s, start listening...", address))

    for {
        // Accept will block until a new connection is established or listen interrupted will not return.        conn, err := ()
        if err != nil {
            // Usually an error caused by the listener being closed and unable to continue listening            (("accept err: %v", err))
        }
        // Open a new goroutine to handle the connection        go Handle(conn)
    }
}

func Handle(conn ) {
    // Use the buffer function provided by the bufio standard library    reader := (conn)
    for {
        // ReadString will block until the delimiter '\n' is encountered        // After encountering a delimiter, it will return all data received after the last time the delimiter or connection is established, including the delimiter itself        // If an exception is encountered before the delimiter is encountered, ReadString will return the received data and error messages        msg, err := ('\n')
        if err != nil {
            // The error usually encountered is that the connection is interrupted or closed, which means            if err ==  {
                ("connection close")
            } else {
                (err)
            }
            return
        }
        b := []byte(msg)
        // Send the received information to the client        (b)
    }
}

func main() {
    ListenAndServe(":8000")
}

Use the telnet tool to test the Echo server we wrote:

$ telnet 127.0.0.1 8000
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
> a
a
> b
b
Connection closed by foreign host.

Unpacking and sticking issues

Some friends may be extremely shocked after seeing "unpacking and sticking" and repeatedly emphasized: TCP is a byte streaming protocol and does not have sticking problems.

The TCP server we often call is not a "server that implements the TCP protocol" but an "application-layer server based on the TCP protocol". TCP is a protocol that is oriented to byte streams, while application layer protocols are mostly message-oriented, such as requests/responses of the HTTP protocol, and instructions/replies of the Redis protocol are communicated in units of messages.

As an application layer server, we have the responsibility to correctly parse application layer messages from the byte stream provided by TCP. In this step, we will encounter the "unpacking/sticking" problem.

The socket allows us to read a newly received piece of data through the read function (of course, this piece of data does not correspond to a TCP packet). In the above Echo server example we use\nIndicates that the message ends, the data read from the read function may have the following situations:

  • Two pieces of data received: "abc", "def\n" They belong to a message "abcdef\n" This is the case of unpacking
  • Received a piece of data: "abc\ndef\n" They belong to two messages "abc\n", "def\n" This is the case of sticking packets

Application layer protocols usually use one of the following ideas to define messages to ensure complete reading:

  • Default length news
  • Add special delimiters at the end of the message, such as the Echo protocol and the FTP control protocol in the example. The bufio standard library caches received data until it encounters a delimiter and it helps us to properly split the byte stream.
  • Divide messages into header and body, and provide the total body length in the header. This subcontracting method is called the LTV(length, type, value) package. This is the most widely used strategy, such as the HTTP protocol. When the body length is obtained from the header, the function reads the byte stream of specified length, thereby parsing the application layer message.

Without a specific application layer protocol, it is difficult for us to discuss the unpacking and sticking issues in detail. In the second article in this series:Implementing the Redis protocol resolverIn this article, we can see the combination of Redis Serialization Protocol (RESP) for delimiters and LTV packages, as well as the specific parsing code of the two subcontracting methods.

Elegantly close

In a production environment, it is necessary to ensure that the necessary cleaning work is completed before the TCP server is shut down, including completing the ongoing data transmission, closing the TCP connection, etc. This shutdown mode is called elegant shutdown, which can avoid resource leakage and failures caused by client failure to receive complete data.

The elegant shutdown mode of the TCP server is usually: first close the listener to prevent new connections from entering, and then iterate through all connections to close one by one. First, modify the TCP server:

// handler is an abstraction of the application layer servertype Handler interface {
    Handle(ctx , conn )
    Close()error
}

// Listen and provide services, and close after receiving a closing notification from closeChanfunc ListenAndServe(listener , handler , closeChan <-chan struct{}) {
    // Listen to the shutdown notification    go func() {
        <-closeChan
        ("shutting down...")
        // Stop listening, () will return immediately        _ = ()
        // Close the application layer server        _ = ()
    }()

    // Release the resource after abnormal exit    defer func() {
        // close during unexpected error
        _ = ()
        _ = ()
    }()
    ctx := ()
    var waitDone 
    for {
        // Listen to the port, blocking until a new connection is received or an error occurs        conn, err := ()
        if err != nil {
            break
        }
        // Turn on goroutine to handle new connections        ("accept link")
        (1)
        go func() {
            defer func() {
                ()
            }()
            (ctx, conn)
        }()
    }
    ()
}

// ListenAndServeWithSignal listens for interrupt signals and notifies the server to shut down through closeChanfunc ListenAndServeWithSignal(cfg *Config, handler ) error {
    closeChan := make(chan struct{})
    sigCh := make(chan )
    (sigCh, , , , )
    go func() {
        sig := <-sigCh
        switch sig {
        case , , , :
            closeChan <- struct{}{}
        }
    }()
    listener, err := ("tcp", )
    if err != nil {
        return err
    }
    (("bind: %s, start listening...", ))
    ListenAndServe(listener, handler, closeChan)
    return nil
}

Next, modify the application layer server:

// Abstraction of client connectiontype Client struct {
    // tcp connection    Conn 
    // When the server starts sending data, enter waiting to prevent other goroutines from closing the connection    // is a package written by the author with maximum waiting time:    // /HDT3213/godis/blob/master/src/lib/sync/wait/
    Waiting 
}

type EchoHandler struct {
    
    // Save a collection of all working state clients (use map as set)    // Concurrently secure containers are required    activeConn  

    // Close the status flag bit    closing 
}

func MakeEchoHandler()(*EchoHandler) {
    return &EchoHandler{}
}

func (h *EchoHandler)Handle(ctx , conn ) {
    // Closed handler will not handle new connections    if () {
        ()
        return 
    }

    client := &Client {
        Conn: conn,
    }
    (client, struct{}{}) // Remember the connection that still survives
    reader := (conn)
    for {
        msg, err := ('\n')
        if err != nil {
            if err ==  {
                ("connection close")
                (client)
            } else {
                (err)
            }
            return
        }
        // Set it to waiting before sending data to prevent the connection from being closed        (1)

        // Simulate the situation where the sending is not completed during the closing        //("sleeping")
        //(10 * )

        b := []byte(msg)
        (b)
        // Send it, end waiting        ()
    }
}

// Close client connectionfunc (c *Client)Close()error {
    // Wait for data transmission to complete or timeout    (10 * )
    ()
    return nil
}

// Close the serverfunc (h *EchoHandler)Close()error {
    ("handler shutting down...")
    (true)
    // Close connections one by one    (func(key interface{}, val interface{})bool {
        client := key.(*Client)
        ()
        return true
    })
    return nil
}

This is the end of this article about Golang’s solution to write Tcp servers. For more related go tcp server content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!