添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
俊逸的小马驹  ·  Capturing Url ...·  2 月前    · 
重情义的甘蔗  ·  Forums·  6 月前    · 
帅呆的海龟  ·  laravel ...·  1 年前    · 
豪情万千的刺猬  ·  [Resolved] ...·  1 年前    · 

Mastering Gorilla WebSockets

We're about to dive headfirst into the exciting world of Gorilla WebSockets.

·

9 min read

Mastering Gorilla WebSockets

If you've ever wondered how to supercharge your web applications with real-time interactivity, you're in the right place, but Before we do that let us Understand What WebSockets

The WebSocket Protocol :

The WebSocket Protocol enables two-way communication between a client running code and a remote host that has agreed to communicate through that code.

How does the connection Begin?

The protocol Starts with an opening handshake which is followed by basic message framing, layered over TCP.

Why Should I Use Websocket?

The goal of Websockets is to provide a mechanism for Web-based applications that need two-way communication with servers that do not rely on opening multiple HTTP connections.

well...If that doesn't sound convincing to you

Let's take a practical example

Historically, creating web applications that need bidirectional communication between a client and a server

Say for example: instant messaging, Online Games, stock tickers and multiuser applications with simultaneous editing have required an abuse of HTTP to request the server for updates while sending upstream notifications as separate HTTP calls

This results in a variety of problems:

  • The server is forced to use several different underlying TCP connections for each client: one for sending information to the client and a new one for each incoming message.

  • The wire protocol has a high overhead, with each client-to-server message having an HTTP header.

  • The client-side script is forced to maintain a mapping from the outgoing connections to the incoming connection to track replies.

    A simpler solution would be to use a single TCP connection for traffic in both directions. This is what the WebSocket Protocol provides. Combined with the WebSocket API, it provides an alternative to HTTP polling for two-way communication from a web page to a remote server.

    Gorilla WebSocket

    There are 2 ways in which a Websocket connection can be established

    1) Upgrade

    Upgrade upgrades the HTTP server connection to the WebSocket protocol.

    func WsHandler(w http.ResponseWriter, r *http.Request) {
        WsConnection, err := upgrader.Upgrade(w, r, nil)
    //for sake of simplicity i'll refer to WsConnection as conn
        if err != nil {
            log.Println(err) //standard error handler
            return
    //From here we can use the WsConnection to read or write
    

    ReadBufferSize and WriteBufferSize specify [Input/Output] buffer sizes in bytes. If a buffer size is zero, then buffers allocated by the HTTP server are used. An interesting thing to note is that the I/O buffer sizes do not limit the size of the messages that can be sent or received.The buffer sizes just control How much data is buffered before a read/write system call and How much data is buffered before being flushed to the network.

    var upgrader = websocket.Upgrader{
        ReadBufferSize:  2048,
        WriteBufferSize: 2048,
    

    How does the Upgrade take place?

    For the WebSocket connection to be firmly established, there are a number of internal function calls that happen. If I filter out Some pretty important error handlers and come back to the main exoskeleton, these functions are the backbone of upgrade

    func (u *Upgrader) Upgrade(w http.ResponseWriter,
     r *http.Request, responseHeader http.Header)
     (*Conn, error) {
    h, ok := w.(http.Hijacker) //First step
    var brw *bufio.ReadWriter
        netConn, brw, err := h.Hijack() //Second step
    

    Hijack lets the caller take over the connection(This is the crucial part). After a call to Hijack the HTTP server library will not do anything else with the connection.

    It becomes the caller's responsibility to manage and close the connection. The returned net.Conn may have read or write deadlines already set, depending on the configuration of the Server. It is the caller's responsibility to set or clear those deadlines as needed.

    var br *bufio.Reader
        if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
            br = brw.Reader
        buf := bufioWriterBuffer(netConn, brw.Writer)
    

    var br *bufio.Reader : Declares a Reader variable.

    if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {...xyz} : If no read buffer size is configured and the hijacked Reader is large enough, it reuses that Reader instead of allocating a new one.

    buf := bufioWriterBuffer(netConn, brw.Writer) : Gets the buffer from the hijacked Writer.

    var writeBuf []byte
    if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
    writeBuf = buf
    

    var writeBuf []byte: Declares a byte slice for the write buffer.

    if u.WriteBufferPool == nil ... { ... }: If no write buffer pool is configured and the hijacked write buffer is large enough, it reuses that buffer instead of allocating a new one.

    c := newConn(netConn, //Final step
     true, u.ReadBufferSize,
     u.WriteBufferSize,
     u.WriteBufferPool,
     br, writeBuf)
    
    func newConn{//not including the args cuz they're too long
    mu := make(chan struct{}, 1)
        mu <- struct{}{}
        c := &Conn{
            isServer:               isServer,
            br:                     br,
            conn:                   conn,
            mu:                     mu,
            readFinal:              true,
            writeBuf:               writeBuf,
            writePool:              writeBufferPool,
            writeBufSize:           writeBufferSize,
            enableWriteCompression: true,
            compressionLevel:       defaultCompressionLevel,
    return c
    

    to Visualize the Upgrade Method:

    2) Dialer:

    Here is how the Dialer works :

    The Dialer is used to establish a WebSocket connection to a server. It is defined in the gorilla/websocket package as:

      type Dialer struct {
         NetDial     func(net, addr string) (net.Conn, error)  
         Proxy       func(req *http.Request) (*url.URL, error)
         HandshakeTimeout time.Duration
         TLSClientConfig   *tls.Config   
         Subprotocols     []string  
         ReadBufferSize   int
         WriteBufferSize  int  
         WriteTimeout     time.Duration  
         HandleHTTP       bool
    

    The NetDial field specifies the function used to connect to the WebSocket server. By default it uses net.Dial.

    The Proxy field specifies an HTTP proxy function to use when connecting.

    The HandshakeTimeout field specifies the duration to wait for a WebSocket handshake to complete.

    The TLSClientConfig field specifies the TLS configuration to use for secure connections.

    The Subprotocols field specifies list of supported subprotocols.

    The BufferSize fields specify the read and write buffer sizes.

    The WriteTimeout specifies the duration to wait for a write operation to the server to complete.

    The HandleHTTP field specifies if the Dialer should handle regular HTTP connections, false is the default value.

    To connect to a WebSocket server, you call the Dial() method on the Dialer, passing the URL of the WebSocket server:

      dialer := websocket.Dialer{}
      conn, _, err := dialer.Dial("ws://localhost:8080", nil)
      if err != nil {
         // handle error
      defer conn.Close()
    

    Dial() uses the Dialer configuration

  • It creates an HTTP request for the WebSocket URL

  • The request may be rewritten if a Proxy is used

  • It then makes a TCP connection to the server

  • Then attempts to upgrade this to a WebSocket connection

  • If the handshake fails, an error is returned

  • If it succeeds, a WebSocket connection is returned

  • Read and write listeners are started on the connection

  • Client can now send/receive messages over the socket

    So Dial() handles the connection setup, while the returned WebSocketConnection enables communication.

    Visualize it this way

    Ping & Pong (not the game:p)

    The Ping and Pong messages are used to keep the WebSocket connection alive.

    The client ( or the browser) will send a Ping message to the server periodically. The server will then respond with a Pong message. This has two purposes:

  • It checks that the connection is still open and active. If the server does not receive any Ping messages for some time, it understands that the connection has been closed or lost.

  • It keeps the connection alive. TCP connections have an idle timeout, and if no data is sent for a while the connection will be closed. Sending Ping/Pong messages regularly prevents the connection from timing out.

    The implementation for ping|pong is as follows:

    var upgrader = websocket.Upgrader{
        ReadBufferSize:  1024,
        WriteBufferSize: 1024,
    func pingHandler(w http.ResponseWriter, r *http.Request) {
        conn, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
            log.Print("upgrade:", err)
            return
        defer conn.Close()
        // Set ping handler
        upgrader.SetPingHandler(func(appData string) error {
            log.Printf("Received ping from client!")
            return nil
        // Set pong handler
        upgrader.SetPongHandler(func(appData string) error {
            log.Printf("Received pong from client!")
            return nil
        // Send ping every 10 seconds
        go func() {
            for {
                conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second*10))
    

    Difference between Upgrade and Dial

  • Use the dialer when you want to establish a completely new WebSocket connection to a URL.

  • Use the upgrader when you have an already existing HTTP connection, for example from an HTTP request to your server, and you want to upgrade that connection to WebSocket.

    Reading & Writing Methods for Websockets

    Reading:

    conn.ReadMessage()

  • Used to read a complete message from the connection.

  • It handles details like fragmentation and masking transparently.

  • Returns the message payload as bytes and the message type (text or binary).

  • Blocks until the full message is received or error happens.

    conn.NextReader()

  • Returns an io.Reader to read the next message payload incrementally.

  • The reader handles fragmentation and masking.

  • Allows reading the message in chunks/streams instead of all at once.

  • Returns io.EOF error when message payload is fully read.

    Writing:

    conn.WriteMessage()

  • Writes a message to the connection.

  • Handles fragmentation if the message exceeds max frame size.

  • Accepts the message payload as bytes and the message type.

  • Blocks until the message is fully written.

    conn.NextWriter()

  • Returns an io.Writer to write the next message incrementally.

  • The writer handles fragmentation automatically.

  • Allows writing the message payload in chunks/streams.

  • Returns nil error after message is fully written.

    conn.WriteControl()

  • Used to write control frames like ping, pong, close.

  • Accepts the control frame type and payload.

  • Write is completed immediately.

    Additionally, there are some convenience methods like ReadJSON() and WriteJSON() for structured data.

    refer: https://pkg.go.dev/github.com/gorilla/websocket#Conn.ReadJSON

    and : https://pkg.go.dev/github.com/gorilla/websocket#Conn.WriteJSON

    Closing a WebSocket connection

    You might be wondering "why should I bother closing a WebSocket connection ?"

    Well let me give you 3 points on why it is necessary to close a WebSocket connection:

  • Releases resources - WebSocket connections hold onto resources like TCP connections and memory. By closing the connection, these resources are released and made available again for other uses.

  • Following standard protocol - The WebSocket protocol specifies that connections should be closed gracefully using proper close frames. Closing the connection ensures you follow the protocol.

  • Allows for clean reconnect - If your application needs to reconnect to the WebSocket, a graceful close allows it to smoothly reconnect without any issues. However, an abrupt disconnection can cause problems reconnecting.

    Methods for closing a ws Connection:

    close() && closeHandler

    These are two functions used for closing a WebSocket connection:

    close():

  • This is the method to gracefully close a WebSocket connection. It sends a close frame to the server, indicating a successful close-down of the connection. it's syntax is:

      conn.Close()
    

    closeHandler:

  • This is a handler function that is called when the server closes the WebSocket connection. We can register a close handler using:

      conn.SetCloseHandler(func(code int, text string) error {
         // Handle close and perform logging and cleanup
    

    The close handler receives the close code and text sent by the server, and this is the place where we can perform any cleanup or logging.

    An example of close handler:

      conn.SetCloseHandler(func(code int, text string) error {
         log.Println("Connection closed with code:", code, "and reason:", text)
         return nil
    

    So in summary:

  • close() - Gracefully closes the connection by sending a close frame

  • closeHandler - Handles the closure of the connection initiated by the server

  •