SignalR for Go

repository·master·Indexed 41 days ago

https://github.com/philippseith/signalr

An open-source Go library implementing the SignalR protocol for real-time, bidirectional communication. It provides both a server and a client supporting WebSockets, Server-Sent Events (SSE), and raw TCP with JSON or MessagePack encoding. Features include hub implementation via signalr.HubInterface, automatic reconnection with exponential backoff, and support for server streaming via PullStream and client streaming via PushStreams.

Tokens
22.5K
Snippets
94
Records
111
Agent score
52%

What's inside SignalR

  1. Compare SignalR hub management options

    master

    SignalR provides three primary ways to manage hub instances, depending on whether you need shared state, statelessness, or per-connection state:

    OptionHub InstancesState PersistenceUse Case
    UseHub1 (shared)Global stateBroadcast to all clients
    HubFactoryNew per callNo stateStateless operations
    PerConnectionHubFactory1 per connectionPer-connection stateUser sessions, personal data
  2. Use the SignalR Go client

    master

    To use the SignalR client in a Go application, you need to create a receiver to handle server callbacks and then initialize a Client using an HTTPConnection.

    1. Define a Receiver: Create a struct that embeds signalr.Hub and implement methods that match the server's Send calls.
    2. Create Connection: Use signalr.NewHTTPConnection.
    3. Create Client: Use signalr.NewClient with the connection and the receiver.
    4. Start: Call client.Start() to begin the loop.
    type receiver struct {
        signalr.Hub
    }
    
    func (c *receiver) Receive(msg string) {
        fmt.Println(msg)
    }
    
    // Usage
    conn, _ := signalr.NewHTTPConnection(ctx, address)
    client, _ := signalr.NewClient(ctx, 
        signalr.WithConnection(conn), 
        signalr.WithReceiver(&receiver{}),
    )
    client.Start()
    
    // Invoke a server method
    resultCh := client.Invoke("update", data)
    data := <-resultCh
  3. Use HubConnection to connect to a SignalR server

    master

    To connect to a SignalR hub from a browser, use the signalR.HubConnectionBuilder.

    1. Build the connection: Use .withUrl('/path') to specify the hub endpoint.
    2. Bind events:
      • Use connection.on(target, callback) to handle messages sent from the server.
      • Use connection.invoke(target, payload) to call methods on the server hub.
    3. Start: Call connection.start() to initiate the connection.
    <script src="js/signalr.js"></script>
    <script>
    (async function () {
        const connection = new signalR.HubConnectionBuilder()
                .withUrl('/chat')
                .build();
    
        // Handle messages from server
        connection.on('chatMessageReceived', (payload) => {
            console.log("Received:", payload);
        });
    
        await connection.start();
    
        // Invoke server method
        async function sendMessage(msg) {
            await connection.invoke('sendChatMessage', msg);
        }
    })();
    </script>
  4. Install SignalR JavaScript/TypeScript client

    master

    The client-side libraries are published as @microsoft/signalr. You can install them using your preferred package manager:

    Package ManagerCommand
    npmnpm install @microsoft/signalr@latest
    yarnyarn add @microsoft/signalr@latest
    LibManlibman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.min.js --files dist/browser/signalr.map.js
  5. Implement the signalr.HubInterface on the server

    master

    To anchor a connection on the server, you must implement the signalr.HubInterface. The easiest way is to declare a custom type and embed signalr.Hub.

    • Custom Methods: Define public methods on your custom type to handle client-side invocations. These methods are invoked from the client using their lowercase names.
    • Lifecycle Events: You can override the default connection/disconnection behavior by implementing OnConnected(connectionID string) and OnDisconnected(connectionID string) on your custom type.
    package main
    
    import "github.com/philippseith/signalr"
    
    type AppHub struct {
        signalr.Hub
    }
    
    // This method is invoked from the client as 'sendChatMessage'
    func (h *AppHub) SendChatMessage(message string) {
        h.Clients().All().Send("chatMessageReceived", message)
    }
    
    func (h *AppHub) OnConnected(connectionID string) {
        fmt.Printf("%s connected\n", connectionID)
    }
    
    func (h *AppHub) OnDisconnected(connectionID string) {
       fmt.Printf("%s disconnected\n", connectionID)
    }
  6. Use PerConnectionHubFactory for per-connection state

    master

    Use the signalr.PerConnectionHubFactory option to create a 1:1 mapping between client connections and hub instances. This allows a hub instance to persist for the entire lifetime of a connection, enabling you to maintain connection-specific state (like user sessions, counters, or personal data) without recreating the hub for every method call.

    Lifecycle

    1. Connection Establishment: When a client connects, the factory function is called with the connectionID.
    2. Hub Creation: A new hub instance is created and stored in the server's connection map.
    3. Method Invocation: All subsequent method calls on that specific connection reuse the same hub instance.
    4. Connection Cleanup: When the connection closes, the hub instance is automatically removed from the map to prevent memory leaks.
    package main
    
    import (
        "context"
        "github.com/philippseith/signalr"
    )
    
    type ChatHub struct {
        signalr.Hub
        connectionID string
        username     string
        messageCount int
    }
    
    func (h *ChatHub) OnConnected(connectionID string) {
        h.connectionID = connectionID
        h.messageCount = 0
    }
    
    func (h *ChatHub) SendMessage(message string) {
        h.messageCount++
        h.Clients().All().Send("ReceiveMessage", h.username, message)
    }
    
    func main() {
        ctx := context.Background()
        
        server, err := signalr.NewServer(ctx,
            signalr.PerConnectionHubFactory(func(connectionID string) signalr.HubInterface {
                return &ChatHub{}
            }),
        )
        if err != nil {
            panic(err)
        }
        _ = server
    }
  7. Serve SignalR with http.ServeMux

    master

    To integrate the SignalR server into a Go web application, use signalr.NewServer to create a server instance and then use server.MapHTTP to bind the SignalR API routes to your existing http.ServeMux.

    import (
        "context"
        "fmt"
        "log"
        "net/http"
        "time"
        "github.com/philippseith/signalr"
    )
    
    func runHTTPServer() {
        address := "localhost:8080"
        hub := AppHub{}
        
        // Build the SignalR server
        server, _ := signalr.NewServer(context.TODO(),
            signalr.SimpleHubFactory(hub),
            signalr.KeepAliveInterval(2*time.Second),
        )
        
        router := http.NewServeMux()
        
        // Map SignalR routes to the router under a specific base URL
        server.MapHTTP(signalr.WithHTTPServeMux(router), "/chat")
    
        fmt.Printf("Listening for websocket connections on http://%s\n", address)
        if err := http.ListenAndServe(address, router); err != nil {
            log.Fatal("ListenAndServe:", err)
        }
    }
  8. What is a MappableRouter?

    master

    A MappableRouter is an abstraction that allows the SignalR server to work with different HTTP multiplexers (like http.ServeMux or third-party routers) without being tightly coupled to a specific implementation.

    To be compatible, a router must implement:

    • HandleFunc(string, func(w http.ResponseWriter, r *http.Request))
    • Handle(string, http.Handler)
  9. How RedisHubLifetimeManager handles multi-instance messaging

    master

    The redisHubLifetimeManager uses a Pub/Sub pattern to synchronize state and messages across multiple SignalR server instances:

    1. Message Dispatching: When an Invoke method is called, the manager publishes a redisInvocation JSON payload to a Redis channel (signalr:all or signalr:group:<name>).
    2. Loop Prevention: Each manager instance has a unique instanceID. When receiving a message via Redis, the manager checks the SourceInstance field. If it matches its own instanceID, it skips the message to prevent infinite loops.
    3. Local Delivery: Upon receiving a message from Redis, each instance checks if the target (a specific connectionID or a groupName) matches any of its locally connected clients. If a match is found, the message is delivered to the client via SendInvocation.
    4. Group Verification: For group messages, the manager verifies membership using Redis SIsMember to ensure that even if local state is slightly out of sync, messages are only sent to valid group members.
  10. Implement Server-Sent Events (SSE) transport in Go

    master

    The serverSSEConnection type provides a transport implementation for SignalR using Server-Sent Events. It manages a bidirectional communication flow where the server sends data to the client via SSE, and the client sends data back to the server via HTTP POST requests.

    Key behaviors:

    • Writing to Client: When calling Write(p []byte), the implementation wraps the payload in the SSE data: format and sends it through a job channel. It waits for a RWJobResult to confirm the operation.
    • Reading from Client: The Read(p []byte) method reads data that has been posted to the server. This is facilitated by an internal io.Pipe that connects the HTTP POST body to the connection's reader.
    • Concurrency: It uses a mutex to manage postWriting state to prevent concurrent POST requests from conflicting (returning 409 Conflict).
    // Note: serverSSEConnection is an internal implementation detail of the SignalR transport layer.
    // It is initialized via newServerSSEConnection(ctx, connectionID).
  11. Use the MessagePack Hub Protocol implementation

    master

    The messagePackHubProtocol provides a MessagePack-based implementation of the SignalR Hub Protocol. It handles the encoding and decoding of SignalR messages (such as invocations, stream items, and completions) using MessagePack framing.

    Key capabilities include:

    • Parsing Messages: Uses ParseMessages to read and decode multiple messages from an io.Reader.
    • Writing Messages: Uses WriteMessage to encode a SignalR message type into MessagePack format and write it to an io.Writer.
    • Argument Unmarshaling: Uses UnmarshalArgument to convert msgpack.RawMessage arguments into specific Go types.
    • Configuration: You can set a maximum receive message size via setMaxReceiveMessageSize to prevent large message attacks.
    // Note: messagePackHubProtocol is unexported. 
    // You typically interact with it through the public HubProtocol interface 
    // which provides methods like ParseMessages, WriteMessage, and UnmarshalArgument.