gobwas/ws

repository·master·Indexed 27 days ago

https://github.com/gobwas/ws

A high-performance, low-allocation WebSocket client and server implementation in Go. It provides tools for efficient connection upgrading via ws.Upgrader, buffer management through the wsutil package, and low-level protocol control for zero-allocation needs. Features include support for Permessage-Deflate compression via ws/wsflate, RFC6455 compliance validation, and a customizable Dialer for fine-grained handshake control.

Tokens
7.6K
Snippets
7
Records
45
Agent score
92%

What's inside gobwas/ws

  1. Implement a WebSocket echo server using wsutil

    master

    For a high-level implementation, use the wsutil package to handle message reading and writing. This approach abstracts the protocol internals while providing a simple interface for common tasks like reading client data and writing server messages.

    Use ws.UpgradeHTTP to upgrade an incoming http.Request and wsutil.ReadClientData / wsutil.WriteServerMessage for the I/O loop.

    package main
    
    import (
    	"net/http"
    
    	"github.com/gobwas/ws"
    	"github.com/gobwas/ws/wsutil"
    )
    
    func main() {
    	http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		conn, _, _, err := ws.UpgradeHTTP(r, w)
    		if err != nil {
    			// handle error
    		}
    		go func() {
    			defer conn.Close()
    
    			for {
    				msg, op, err := wsutil.ReadClientData(conn)
    				if err != nil {
    					// handle error
    				}
    				err = wsutil.WriteServerMessage(conn, op, msg)
    				if err != nil {
    					// handle error
    				}
    			}
    		}()
    	}))
    })
  2. Use Permessage-Deflate compression with wsutil

    master

    The ws/wsflate package provides support for the Permessage-Deflate Compression Extension. It is compatible with wsutil's reader and writer via the wsflate.MessageState type, which implements wsutil.SendExtension and wsutil.RecvExtension.

    To use compression with wsutil:

    1. Use ws.Upgrader.Negotiate with wsflate.Extension.Negotiate.
    2. Initialize wsflate.Reader and wsflate.Writer with appropriate flate implementations.
    3. Use a wsflate.MessageState variable to track compression state.
    4. When configuring wsutil.Reader, include ws.StateExtended in the state and add the MessageState to the Extensions slice.
    5. When configuring wsutil.Writer, include ws.StateExtended and use wr.SetExtensions(&msg).
    // ... (Upgrade and negotiate compression) ...
    
    // Initialize flate reader/writer
    fr := wsflate.NewReader(nil, func(r io.Reader) wsflate.Decompressor {
    	return flate.NewReader(r)
    })
    fw := wsflate.NewWriter(nil, func(w io.Writer) wsflate.Compressor {
    	f, _ := flate.NewWriter(w, 9)
    	return f
    })
    
    var msg wsflate.MessageState
    
    rd := &wsutil.Reader{
    	Source:     conn,
    	State:      ws.StateServerSide | ws.StateExtended,
    	Extensions: []wsutil.RecvExtension{
    		&msg, 
    	},
    }
    
    wr := wsutil.NewWriter(conn, ws.StateServerSide|ws.StateExtended, 0)
    wr.SetExtensions(&msg)
    
    for {
    	h, err := rd.NextFrame()
    	// ... handle error ...
    
    	wr.Reset(h.OpCode)
    
    	fr.Reset(rd)
    	fw.Reset(wr)
    
    	if _, err := io.Copy(fw, fr); err != nil {
    		// handle error
    	}
    	if err := fw.Close(); err != nil {
    		// handle error
    	}
    	if err := wr.Flush(); err != nil {
    		// handle error
    	}
    }
  3. Implement a WebSocket echo server using low-level wsutil buffers

    master

    For better performance and buffer reuse, use wsutil.NewReader and wsutil.NewWriter. This allows you to use io.Copy to stream data between the reader and writer efficiently. You must call writer.Reset for each new frame to ensure the correct OpCode is used.

    import (
    	"net/http"
    	"io"
    
    	"github.com/gobwas/ws"
    	"github.com/gobwas/ws/wsutil"
    )
    
    func main() {
    	http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		conn, _, _, err := ws.UpgradeHTTP(r, w)
    		if err != nil {
    			// handle error
    		}
    		go func() {
    			defer conn.Close()
    
    			var (
    				state  = ws.StateServerSide
    				reader = wsutil.NewReader(conn, state)
    				writer = wsutil.NewWriter(conn, state, ws.OpText)
    			)
    			for {
    				header, err := reader.NextFrame()
    				if err != nil {
    					// handle error
    				}
    
    				// Reset writer to write frame with right operation code.
    				writer.Reset(conn, state, header.OpCode)
    
    				if _, err = io.Copy(writer, reader); err != nil {
    					// handle error
    				}
    				if err = writer.Flush(); err != nil {
    					// handle error
    				}
    			}
    		}()
    	}))
    }
  4. Implement a WebSocket echo server using low-level ws (no wsutil)

    master

    For maximum control and zero intermediate allocations, use the ws package directly. You are responsible for reading the header with ws.ReadHeader, reading the payload, handling masking with ws.Cipher (if header.Masked is true), and writing the header back with ws.WriteHeader.

    Note: Per RFC6455, server frames must not be masked. If you read a masked frame from a client, you must unmask it before processing or re-sending.

    package main
    
    import (
    	"net"
    	"io"
    
    	"github.com/gobwas/ws"
    )
    
    func main() {
    	ln, err := net.Listen("tcp", "localhost:8080")
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	for {
    		conn, err := ln.Accept()
    		if err != nil {
    			// handle error
    		}
    		_, err = ws.Upgrade(conn)
    		if err != nil {
    			// handle error
    		}
    
    		go func() {
    			defer conn.Close()
    
    			for {
    				header, err := ws.ReadHeader(conn)
    				if err != nil {
    					// handle error
    				}
    
    				payload := make([]byte, header.Length)
    				_, err = io.ReadFull(conn, payload)
    				if err != nil {
    					// handle error
    				}
    				if header.Masked {
    					ws.Cipher(payload, header.Mask, 0)
    				}
    
    				// Reset the Masked flag, server frames must not be masked as
    				// RFC6455 says.
    				header.Masked = false
    
    				if err := ws.WriteHeader(conn, header); err != nil {
    					// handle error
    				}
    				if _, err := conn.Write(payload); err != nil {
    					// handle error
    				}
    
    				if header.OpCode == ws.OpClose {
    					return
    				}
    			}
    		}()
    	}
    }
  5. Perform a zero-copy WebSocket upgrade

    master

    The ws.Upgrader allows for high-performance, zero-copy HTTP upgrades by processing non-websocket headers in-place via callbacks. This is ideal for high-load services that need to control connection resources (like buffers) at the TCP level.

    Key fields in ws.Upgrader:

    • OnHost: Callback to validate the Host header. Returning ws.RejectConnectionError allows you to reject connections early.
    • OnHeader: Callback to process non-websocket headers in-place. Arguments are only valid until the callback returns.
    • OnBeforeUpgrade: Callback to provide the handshake headers to be sent back to the client.
    • Negotiate: Callback used to negotiate extensions (like compression).
    package main
    
    import (
    	"log"
    	"net"
    
    	"github.com/gobwas/ws"
    )
    
    func main() {
    	ln, err := net.Listen("tcp", "localhost:8080")
    	if err != nil {
    		log.Fatal(err)
    	}
    	u := ws.Upgrader{
    		OnHeader: func(key, value []byte) (err error) {
    			log.Printf("non-websocket header: %q=%q", key, value)
    			return
    		},
    	}
    	for {
    		conn, err := ln.Accept()
    		if err != nil {
    			// handle error
    		}
    
    		_, err = u.Upgrade(conn)
    		if err != nil {
    			// handle error
    		}
    	}
    }
  6. Establish a WebSocket connection with Dial

    master
    Use the Dial function for a quick way to establish a WebSocket connection using the DefaultDialer. It returns the network connection, a buffered reader for any data sent immediately after the handshake, and the handshake results.
  7. Configure rejection options with RejectOption

    master

    The RejectOption type is a functional option used to configure a ConnectionRejectedError.

    Functions that return a RejectOption:

    • RejectionReason(reason string)
    • RejectionStatus(code int)
    • RejectionHeader(h HandshakeHeader)
  8. Manage WebSocket RSV Bits

    master

    The Header struct contains an Rsv byte representing the three reserved bits (RSV1, RSV2, RSV3). Use these helpers to manipulate or read them:

    • Rsv(r1, r2, r3 bool) byte: Creates an Rsv byte from three boolean values.
    • RsvBits(rsv byte) (r1, r2, r3 bool): Extracts the three boolean values from an Rsv byte.
    • Header.Rsv1() bool: Returns true if the first RSV bit is set.
    • Header.Rsv2() bool: Returns true if the second RSV bit is set.
    • Header.Rsv3() bool: Returns true if the third RSV bit is set.
  9. Write a WebSocket frame or panic with MustWriteFrame

    master
    Use MustWriteFrame(w io.Writer, f Frame) when you want to write a frame but prefer the application to panic if the write fails, rather than handling the error explicitly.
  10. Identify WebSocket OpCode types

    master

    The OpCode type represents the operation code of a WebSocket frame. You can use helper methods to determine the nature of the frame:

    • IsControl(): Returns true if the opcode is a control frame (e.g., Ping, Pong, Close).
    • IsData(): Returns true if the opcode is a data frame (e.g., Text, Binary).
    • IsReserved(): Returns true if the opcode is reserved for future use.

    Available OpCode constants:

    • OpContinuation (0x0)
    • OpText (0x1)
    • OpBinary (0x2)
    • OpClose (0x8)
    • OpPing (0x9)
    • OpPong (0xa)
  11. Write a WebSocket header with WriteHeader

    master
    Use WriteHeader(w io.Writer, h Header) to write the binary representation of a WebSocket header directly to an io.Writer. This function internally allocates a buffer of MaxHeaderSize (14 bytes) to perform the write.
  12. Validate WebSocket headers with CheckHeader

    master

    Use CheckHeader to verify that a Header object contains valid data according to the current State of the connection. This function checks for reserved opcodes, control frame payload limits, fragmentation consistency, and masking requirements (ensuring clients mask frames and servers do not).

    If validation fails, it returns a ProtocolError.

    Note: A zero state (0) represents a clean state with no specific properties enabled.