How to Use Websockets in Golang wechat public account: Operation and maintenance development story, by Wanger

Sending messages and getting instant responses without refreshing the page is something we take for granted. But in the past, enabling real-time functionality was a real challenge for developers. The developer community has come a long way from HTTP long polling and AJAX to find a solution for building truly real-time applications. The solution comes in the form of WebSockets, which can open interactive sessions between the user’s browser and the server. WebSockets allow browsers to send messages to the server and receive event-driven responses without polling the server for a reply. WebSockets are currently the preferred solution for building real-time applications: online games, instant messaging, tracking applications, and more. This tutorial explains how WebSockets work and shows how we can build WebSocket applications using the Go programming language.

Network sockets and WebSockets

Network socket

Network sockets, or sockets for short, are used as internal endpoints for exchanging data between applications running on the same computer or different computers on the same network. Sockets are a key part of Unix – and Windows-based operating systems, and they make it easier for developers to create network-enabled software. Application developers can include sockets in their programs instead of building network connections from scratch. Because network sockets are used for multiple network protocols (HTTP, FTP, and so on), multiple sockets can be used at the same time. A socket is created and used by a set of function calls defined by the socket’s application programming interface (API). There are several types of network sockets: datagram sockets (SOCK_DGRAM), also known as connectionless sockets, use the User Datagram Protocol (UDP). Datagram sockets support two-way message flow and preserve record boundaries. Stream sockets (SOCK_STREAM), also known as connection-oriented sockets, use Transmission Control Protocol (TCP), Flow Control Transport Protocol (SCTP), or Datagram Congestion Control Protocol (DCCP). These sockets provide a two-way, reliable, ordered, and non-repetitive flow of data with no record boundaries. Raw sockets (or raw IP sockets) are typically available in routers and other network devices. These sockets are typically datagram oriented, although their exact nature depends on the interface provided by the protocol. Most applications do not use raw sockets. They are provided to support the development of new communication protocols and to provide access to more esoteric facilities of existing protocols.

Socket communication

Each network socket is identified by an address, which is a triplet of transport protocol, IP address, and port number. Hosts communicate with each other using two protocols: TCP and UDP.

  • Connect to a TCP socket

The Go client uses the DialTCP function in the NET package to establish a TCP connection. DialTCP returns a TCPConn object. Once the connection is established, the client and server begin exchanging data: the client sends a request to the server through a TCPConn object, the server parses the request and sends a response, and the TCPConn object receives the response from the server. ! [] (img – blog. Csdnimg. Cn/img_convert… Object]&originHeight= 895&OriginWidth =800&status=done&style=none&width=800) This connection remains valid until the client or server closes it. The function to create the connection is as follows: client:


// init
   tcpAddr, err := net.ResolveTCPAddr(resolver, serverAddr)
   iferr ! =nil {
        // handle error
   }
   conn, err := net.DialTCP(network, nil, tcpAddr)
   iferr ! =nil {
           // handle error
   }

   // send message
    _, err = conn.Write({message})
   iferr ! =nil {
        // handle error
   }

   // receive message
   var buf [{buffSize}]byte
   _, err := conn.Read(buf[0:)iferr ! =nil {
        // handle error
   }
Copy the code

Server side:


// init
   tcpAddr, err := net.ResolveTCPAddr(resolver, serverAddr)
       iferr ! =nil {
           // handle error
       }
   
       listener, err := net.ListenTCP("tcp", tcpAddr)
    iferr ! =nil {
        // handle error
    }
    
    // listen for an incoming connection
    conn, err := listener.Accept()
    iferr ! =nil {
        // handle error
    }
    
    // send message
    if_, err := conn.Write({message}); err ! =nil {
        // handle error
    }    
    // receive message
    buf := make([]byte.512)
    n, err := conn.Read(buf[0:)iferr ! =nil {
        // handle error
    }
Copy the code
  • Connect to a UDP socket

In contrast to TCP sockets, with UDP sockets, the client only sends datagrams to the server. There is no Accept function, because the server does not need to Accept the connection, just wait for the datagram to arrive. ! [] (img – blog. Csdnimg. Cn/img_convert… Object]&originHeight=800&originWidth=800&status=done&style=none&width=800) Other TCP functions correspond to UDP. Simply replace TCP with UDP in the above function. Client:


// init
    raddr, err := net.ResolveUDPAddr("udp", address)
    iferr ! =nil {
        // handle error
    }
       
    conn, err := net.DialUDP("udp".nil, raddr)
    iferr ! =nil {
        // handle error}...// send message
    buffer := make([]byte, maxBufferSize)
    n, addr, err := conn.ReadFrom(buffer)
    iferr ! =nil {
        // handle error}...// receive message
    buffer := make([]byte, maxBufferSize)
    n, err = conn.WriteTo(buffer[:n], addr)
    iferr ! =nil {
        // handle error
    }
Copy the code

Server side:

 // init
    udpAddr, err := net.ResolveUDPAddr(resolver, serverAddr)
    iferr ! =nil {
        // handle error
    }
    
    conn, err := net.ListenUDP("udp", udpAddr)
    iferr ! =nil {
        // handle error}...// send message
    buffer := make([]byte, maxBufferSize)
    n, addr, err := conn.ReadFromUDP(buffer)
    iferr ! =nil {
        // handle error}...// receive message
    buffer := make([]byte, maxBufferSize)
    n, err = conn.WriteToUDP(buffer[:n], addr)
    iferr ! =nil {
        // handle error
    }

Copy the code

What is the WebSocket

The WebSocket communication packet provides a full-duplex communication channel over a single TCP connection. This means that both the client and the server can send data at the same time without any request when needed. WebSockets are a great solution for services that require continuous data exchange — for example, instant messaging, online gaming, and real-time trading systems. Complete information about the WebSocket protocol can be found in the Internet Engineering Task Force (IETF) RFC 6455 specification.

WebSocket connections are requested by the browser and responded to by the server, which then establishes the connection. This process is often called a handshake. Special types of headers in WebSockets require only a handshake between the browser and the server to establish a connection that will remain active for its entire life. The WebSocket protocol uses port 80 for insecure connections and port 443 for secure connections. The WebSocket specification determines which uniform resource identifier schemes are required by the WS (WebSocket) and WSS (WebSocket Secure) protocols. WebSockets solve many of the headaches of developing real-time Web applications, and offer several benefits over traditional HTTP:

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

! [] (img – blog. Csdnimg. Cn/img_convert… Object]&originHeight=405 &OriginWidth =800&status= DONe&style =none&width=800) WebSocket protocol is relatively simple to implement. It uses the HTTP protocol for the initial handshake. After a successful handshake, the connection is established, and the WebSocket essentially uses raw TCP to read/write data. This is what the client request looks like:

 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
Copy the code

Here’s the server response:

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

How do I create a WebSocket application in Go

To write a simple WebSocket echo server based on the NET/HTTP library:

  1. A handshake
  2. Receives data frames from the client
  3. Sends data frames to the client
  4. Close to shake hands

First, create an HTTP handler with a WebSocket endpoint:


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

Then initialize the WebSocket structure. The initial handshake request always comes from the client. Once the server defines a WebSocket request, it needs to respond with a handshake response. You cannot write a response using http.responseWriter because it closes the underlying TCP connection once you start sending the response. HTTP hijacking can be used. HTTP hijacking takes 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}}Copy the code

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


// Handshake creates a handshake header
    func (ws *WS) Handshake(a) 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"))... }Copy the code

The sec-websocket-key is randomly generated and Base64 encoded. After the server accepts the request, it needs to append this key to the fixed string. Suppose you have x3jhMBDL1ezLkh9Gbhxw == key. You can use SHA-1 to compute binary values and encode them using Base64. You can get HSmrc0sMlYUkAGmm5OPpG2HaGWk =. Use this as the value of the sec-websocket-Accept response header.

Transmission data frame

After the handshake completes successfully, the application can read data from and write data to the client. The WebSocket specification defines a particular frame format for use between a client and a server. This is the bit mode of the frame:! [] (img – blog. Csdnimg. Cn/img_convert… Object]&originHeight=480&originWidth=800&status=done&style=none&width=800) decode the client load with the following code:


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

These lines, in turn, allow the 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}...Copy the code

End to shake hands

The handshake is closed when one party sends a closed frame with a closed state as a payload. The party sending the closure frame can send the closure reason in the payload. If the shutdown is initiated by the client, the server should send a corresponding shutdown frame in response.

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

WebSocket library list

There are several third-party libraries that can simplify the developer’s life and greatly facilitate the use of WebSockets.

STDLIB ( x/net/websocket )

This WebSocket library is part of the standard Go library. It implements the client and server for the WebSocket protocol, as described in the RFC 6455 specification. It requires no installation and is well documented. On the other hand, it still lacks some functionality found in other WebSocket libraries. The Golang Websocket implementation in the /x/net/ webSocket package does not allow users to reuse I/O buffers between connections in an explicit way. First, to install and use the library, add this line of code:

import "golang.org/x/net/websocket"
Copy the code

Client:


 // create connection
    // schema can be ws:// or wss://
    // host, port -- WebSocket server
    conn, err := websocket.Dial("{schema}://{host}:{port}"."", op.Origin)
    iferr ! =nil {
        // handle error
    } 
    defer conn.Close()
             .......
      // send message
        iferr = websocket.JSON.Send(conn, {message}); err ! =nil {
         // handle error}...// receive message
    // messageType initializes some type of message
    message := messageType{}
    iferr := websocket.JSON.Receive(conn, &message); err ! =nil {
          // handle error}...Copy the code

Server side:


   // Initialize WebSocket handler + server
    mux := http.NewServeMux()
        mux.Handle("/", websocket.Handler(func(conn *websocket.Conn) {
            func(a) {
                for {
                
                    // do something, receive, send, etc.}}...// receive message
    // messageType initializes some type of message
    message := messageType{}
    iferr := websocket.JSON.Receive(conn, &message); err ! =nil {
        // handle error}...// send message
    iferr := websocket.JSON.Send(conn, message); err ! =nil {
        // handle error}...Copy the code

Gorilla

The WebSocket package in Gorilla Web toolkit has a complete and tested implementation of the WebSocket protocol and a stable package API. The WebSocket package is well-documented and easy to use. You can view the documentation on Gorilla’s official website. Installation:


go get github.com/gorilla/websocket
Examples of code
Client side:
 // init
    // schema -- can be ws:// or WSS ://
    // host, port -- WebSocket server
    u := url.URL{
        Scheme: {schema},
        Host:   {host}:{port},
        Path:   "/",
    }
    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    iferr ! =nil {
        // handle error}...// send message
    err := c.WriteMessage(websocket.TextMessage, {message})
    iferr ! =nil {
        // handle error}...// receive message
    _, message, err := c.ReadMessage()
    iferr ! =nil {
        // handle error}...Copy the code

Server side:


  // init
    u := websocket.Upgrader{}
    c, err := u.Upgrade(w, r, nil)
    iferr ! =nil {
        // handle error}...// receive message
    messageType, message, err := c.ReadMessage()
    iferr ! =nil {
        // handle error}...// send message
    err = c.WriteMessage(messageType, {message})
    iferr ! =nil {
        // handle error}...Copy the code

Gobwas

This tiny WebSocket package has a powerful list of features, such as zero-copy upgrades and low-level apis that allow you to build custom packet handling logic. Gobwas does not require intermediate allocation during I/O. It also has advanced wrappers and helpers for the APIS in the Wsutil package, allowing developers to get started quickly without delving into the insides of the protocol. Check out the GoDoc website for documentation. Install Gobwas by including the following lines of code:

go get github.com/gobwas/ws
Copy the code

Client:


 // init
    // schema -- can be WS or WSS
    // host, port -- ws server
    conn, _, _, err := ws.DefaultDialer.Dial(ctx, {schema}://{host}:{port})
    iferr ! =nil {
        // handle error}...// send message
    err = wsutil.WriteClientMessage(conn, ws.OpText, {message})
    iferr ! =nil {
        // handle error}...// receive message    
    msg, _, err := wsutil.ReadServerData(conn)
    iferr ! =nil {
        // handle error}...Copy the code

Server side:


 // init
    listener, err := net.Listen("tcp", op.Port)
    iferr ! =nil {
        // handle error
    }
    conn, err := listener.Accept()
    iferr ! =nil {
        // handle error
    }
    upgrader := ws.Upgrader{}
    if_, err = upgrader.Upgrade(conn); err ! =nil {
        // handle error}...// receive message
    for { 
         reader := wsutil.NewReader(conn, ws.StateServerSide)
         _, err := reader.NextFrame()
         iferr ! =nil {
             // handle error
         }
         data, err := ioutil.ReadAll(reader)
         iferr ! =nil {
             // handle error}... }...// send message
    msg := "new server message"
    iferr := wsutil.WriteServerText(conn, {message}); err ! =nil {
        // handle error}...Copy the code

GOWebsockets

The tool provides a wide range of easy-to-use features. It allows concurrency control, data compression, and setting request headers. GOWebsockets support proxies and subprotocols for sending and receiving text and binary data. Developers can also enable or disable SSL authentication. Documentation and examples of how to use GOWebsockets can be found on the GoDoc website and on the project’s GitHub page. Install the package by adding the following lines:

go get github.com/sacOO7/gowebsocket
Copy the code

Client:


  // init
    // schema -- can be WS or WSS
    // host, port -- ws server
    socket := gowebsocket.New({schema}://{host}:{port})
    socket.Connect()
        .......  
    // send message
    socket.SendText({message})
    or
    socket.SendBinary({message})
        .......
    // receive message
    socket.OnTextMessage = func(message string, socket gowebsocket.Socket) {
        // hande received message
    };
    or
    socket.OnBinaryMessage = func(data [] byte, socket gowebsocket.Socket) {
        // hande received message}; .Copy the code

Server side:


    // init
    // schema -- can be WS or WSS
    // host, port -- ws server
    conn, _, _, err := ws.DefaultDialer.Dial(ctx, {schema}://{host}:{port})
    iferr ! =nil {
        // handle error}...// send message
    err = wsutil.WriteClientMessage(conn, ws.OpText, {message})
    iferr ! =nil {
        // handle error}...// receive message    
    msg, _, err := wsutil.ReadServerData(conn)
    iferr ! =nil {
        // handle error
    }
Copy the code

nhooyr.io/websocket

Another popular websocket library is nhooyr. IO /websocket. There is a lot about this library in fish Fry’s book (A Journey into Go programming), including a comprehensive comparison with other libraries. Here you can see a books golang2.eddycjy.com/posts/ch4/0 electronic address…

Compare existing solutions

We have described the four most widely used WebSocket libraries for Golang. The table below contains a detailed comparison of these tools. ! [] (img – blog. Csdnimg. Cn/img_convert… Object]&originHeight=600&originWidth=800& Status =done&style=none&width=800) Several benchmarks were performed to better analyze their performance. ** Results are as follows:! [] (img – blog. Csdnimg. Cn/img_convert… Object]&originHeight=624&originWidth=800&status=done&style=none&width=800)

  • Gobwas has significant advantages over other libraries. It allocates less per operation, using less memory and time per allocation. In addition, its I/O allocation is zero. In addition, Gobwas has all the methods needed to create WebSocket client-server interactions and receive message fragments. You can also use it to easily process TCP sockets.
  • If Gobwas feels inappropriate, you can use Gorilla. It’s very simple and has almost all the same functionality. STDLIB can also be used, but it is not as good in a production environment because it lacks many of the necessary features and, as seen in benchmark testing, provides weak performance. GOWebsocket is roughly the same as STDLIB. But if you need to build a prototype or MVP quickly, it can be a reasonable choice.