Creating WebSocket service with Go language

Posted by CAPTAINCAPSLOCK on Sun, 05 Apr 2020 09:03:45 +0200

Thank you for your reference- http://bjbsair.com/2020-04-01/tech-info/18504.html

Today, I introduce how to create WebSocket service with Go language. The first two parts of this article briefly introduce the WebSocket protocol and how to create WebSocket service with Go standard library. In the third part, we use the gorilla/websocket library to help us quickly build WebSocket services. It helps us encapsulate the basic logic related to the implementation of WebSocket services using the Go standard library, so that we can get rid of the tedious underlying code and quickly build WebSocket services according to business needs.

WebSocket introduction

WebSocket communication protocol provides full duplex communication channel through a single TCP connection. Compared with HTTP, WebSocket doesn't require you to send a request to get a response. It allows two-way data flow, so you just need to wait for messages from the server. When WebSocket is available, it will send you a message. WebSocket is a good solution for services requiring continuous data exchange (such as instant messaging programs, online games and real-time trading systems). The WebSocket connection is requested by the browser, responded by the server, and then established. This process is usually called handshake. The special header in WebSocket only needs a handshake between browser and server to establish a connection, which will remain active throughout its life cycle. WebSocket solves many real-time Web development problems and has many advantages over traditional http

  • Lightweight headers reduce data transmission overhead.
  • A single Web client requires only one TCP connection.
  • WebSocket server can push data to Web client.

WebSocket protocol is relatively simple to implement. It uses the HTTP protocol for the initial handshake. After the handshake is successful, the connection is established. WebSocket essentially uses the original TCP to read / write data.

The client requests are as follows:

GET /chat HTTP/1.1  
    Host: server.example.com  
    Upgrade: websocket  
    Connection: Upgrade  
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==  
    Sec-WebSocket-Protocol: chat, superchat  
    Sec-WebSocket-Version: 13  
    Origin: http://example.com

This is the server response:

HTTP/1.1 101 Switching Protocols  
    Upgrade: websocket  
    Connection: Upgrade  
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=  
    Sec-WebSocket-Protocol: chat

How to create WebSocket application in Go

To write WebSocket server based on the built-in net/http Library of Go language, you need to:

  • Initiate a handshake
  • Receive data frame from client
  • Send data frame to client
  • Close handshake

Initiate a handshake

First, let's create an HTTP handler with a WebSocket endpoint:

// HTTP server with WebSocket endpoint  
func Server() {  
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {  
            ws, err := NewHandler(w, r)  
            if err != nil {  
                 // handle error  
            }  
            if err = ws.Handshake(); err != nil {  
                // handle error  
            }  
        ...

Then initialize the WebSocket structure.

The initial handshake request always comes from the client. After the server determines the WebSocket request, it needs to use the handshake response to reply.

Remember, you can't write a response using http.ResponseWriter because once you start sending a response, it will close its underlying TCP connection (this is determined by the HTTP protocol's operating mechanism, which closes the connection after sending the response).

Therefore, you need to use HTTP hijack. By hijacking, you can take over the underlying TCP connection handler and bufio.Writer. This allows data to be read and written without closing the TCP connection.

// NewHandler initializes a new handler  
func NewHandler(w http.ResponseWriter, req *http.Request) (*WS, error) {  
        hj, ok := w.(http.Hijacker)  
        if !ok {  
            // handle error  
        }                  .....  
}

To complete the handshake, the server must respond with the appropriate headers.

// Handshake creates a handshake header  
    func (ws *WS) Handshake() error {  

        hash := func(key string) string {  
            h := sha1.New()  
            h.Write([]byte(key))  
            h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))  

        return base64.StdEncoding.EncodeToString(h.Sum(nil))  
        }(ws.header.Get("Sec-WebSocket-Key"))  
      .....  
}

The SEC WebSocket key used by the client to initiate a WebSocket connection request is randomly generated and Base64 encoded. After accepting the request, the server needs to attach the key to a fixed string. Suppose the secret key is x3JJHMbDL1EzLkh9GBhXDw = =. In this example, you can use SHA-1 to calculate binary values and encode them using Base64. Get HSmrc0sMlYUkAGmm5OPpG2HaGWk =. It is then used as the value of the SEC WebSocket accept response header.

Transmit data frame

After the handshake completes successfully, your application can read data from the client or write data to the client. The WebSocket specification defines a specific frame format to be used between a client and a server. This is the bit pattern of the framework:

Figure: bit mode of data frame transmission

Use the following code to decode the client payload:

// Recv receives data and returns a Frame  
    func (ws *WS) Recv() (frame Frame, _ error) {  
        frame = Frame{}  
        head, err := ws.read(2)  
        if err != nil {  
            // handle error  
        }

In turn, these lines of code allow data to be encoded:

// Send sends a Frame  
    func (ws *WS) Send(fr Frame) error {  
        // make a slice of bytes of length 2  
        data := make([]byte, 2)  

        // Save fragmentation & opcode information in the first byte  
        data[0] = 0x80 | fr.Opcode  
        if fr.IsFragment {  
            data[0] &= 0x7F  
        }  
        .....

Close handshake

When one of the parties sends a closed frame with the status of closed as the payload, the handshake is closed. Optionally, the party sending the shutdown frame can send the shutdown reason in the payload. If the shutdown is initiated by the client, the server shall send the corresponding shutdown frame in response.

// Close sends a close frame and closes the TCP connection  
func (ws *Ws) Close() error {  
    f := Frame{}  
    f.Opcode = 8  
    f.Length = 2  
    f.Payload = make([]byte, 2)  
    binary.BigEndian.PutUint16(f.Payload, ws.status)  
    if err := ws.Send(f); err != nil {  
        return err  
    }  
    return ws.conn.Close()  
}

Using third-party library to quickly build WebSocket service

From the above chapters, we can see that it is too complex to implement WebSocket service with the net/http Library of Go. Fortunately, there are many third-party libraries that support WebSocket well, which can reduce a lot of our underlying coding work. Here we use gorilla/websocket, another library of gorilla web toolkit family, to realize our WebSocket service and build a simple echo service (echo means echo, which means what the client sends, and the server sends the message back to the client).

We create a new ws subdirectory under the handler directory of the HTTP demo project to store the request handlers corresponding to the routes related to the WebSocket service.

Add two routes:

  • /Routing of WebSocket service of ws/echo echo application.
  • /The route of the client page of the WS / echo echo display echo application.

Create WebSocket server

// handler/ws/echo.go  
package ws  

import (  
    "fmt"  
    "github.com/gorilla/websocket"  
    "net/http"  
)  

var upgrader = websocket.Upgrader{  
    ReadBufferSize:  1024,  
    WriteBufferSize: 1024,  
}  

func EchoMessage(w http.ResponseWriter, r *http.Request) {  
    conn, _ := upgrader.Upgrade(w, r, nil) // Remember to do error handling in practical application  

    for {  
        // Read messages from clients  
        msgType, msg, err := conn.ReadMessage()  
        if err != nil {  
            return  
        }  

        // Print messages to standard output  
        fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg))  

        // Write the message back to the client and complete the echo  
        if err = conn.WriteMessage(msgType, msg); err != nil {  
            return  
        }  
    }  
}
  • The type of Conn variable is websocket.Conn, which is used to represent WebSocket connections. The server application calls the upgrade.upgrade method from the HTTP request handler to get websocket.Conn
  • Call the connected WriteMessage and ReadMessage methods to send and receive messages. The above msg is received and sent back to the client below. msg is of type [] byte.

Create WebSocket client

The request handler corresponding to the front-end page route is as follows, which directly returns views/websockets.html to the browser rendering page.

// handler/ws/echo_display.go  
package ws  

import "net/http"  

func DisplayEcho(w http.ResponseWriter, r *http.Request) {  
    http.ServeFile(w, r, "views/websockets.html")  
}
<form>  
    <input id="input" type="text" />  
    <button onclick="send()">Send</button>  
    <pre id="output"></pre>  
</form>  
...  
<script>    var input = document.getElementById("input");  
    var output = document.getElementById("output");  
    var socket = new WebSocket("ws://localhost:8000/ws/echo");  

    socket.onopen = function () {  
        output.innerHTML += "Status: Connected\n";  
    };  

    socket.onmessage = function (e) {  
        output.innerHTML += "Server: " + e.data + "\n";  
    };  

    function send() {  
        socket.send(input.value);  
        input.value = "";  
    }</script>  
...

Registration routing

After the server and client programs are ready, we register the route and the corresponding request handler for them according to the previously agreed path:

// router/router.go  
func RegisterRoutes(r *mux.Router) {  
    ...  
    wsRouter := r.PathPrefix("/ws").Subrouter()  
    wsRouter.HandleFunc("/echo", ws.EchoMessage)  
    wsRouter.HandleFunc("/echo_display", ws.DisplayEcho)  
}

Test verification

Access after service restart http://localhost:8000/ws/echo_display , any message entered in the input box can be echoed back to the browser.

picture

The server prints the received message to the terminal and then calls writeMessage to send the message back to the client. The record can be viewed in the terminal.

summary

WebSocket is widely used in the frequently updated applications, and programming WebSocket is also a necessary skill we need to master. The practice of the article is a little simpler, and there is no error and security check. Mainly to clarify the general process. For more details about gorilla/websocket, you need to check the official documents when using it.

Topics: Java socket Go Web Development SHA1