Coder Social home page Coder Social logo

philippseith / signalr Goto Github PK

View Code? Open in Web Editor NEW
124.0 5.0 34.0 1.36 MB

SignalR server and client in go

License: MIT License

Makefile 0.68% Go 95.71% HTML 1.85% TypeScript 1.76%
golang realtime-web signalr signalr-server signalr-client server-sent-events websocket messagepack

signalr's Introduction

SignalR

Actions Status codecov PkgGoDev

SignalR is an open-source library that simplifies adding real-time web functionality to apps. Real-time web functionality enables server-side code to push content to clients instantly.

Historically it was tied to ASP.NET Core but the protocol is open and implementable in any language.

This repository contains an implementation of a SignalR server and a SignalR client in go. The implementation is based on the work of David Fowler at https://github.com/davidfowl/signalr-ports. Client and server support transport over WebSockets, Server Sent Events and raw TCP. Protocol encoding in JSON and MessagePack is fully supported.

Install

With a correctly configured Go toolchain:

go get -u github.com/philippseith/signalr

Getting Started

SignalR uses a signalr.HubInterface instance to anchor the connection on the server and a javascript HubConnection object to anchor the connection on the client.

Server side

Implement the HubInterface

The easiest way to implement the signalr.HubInterface in your project is to declare your own type and embed signalr.Hub which implements that interface and will take care of all the signalr plumbing. You can call your custom type anything you want so long as it implements the signalr.HubInterface interface.

package main

import "github.com/philippseith/signalr"

type AppHub struct {
    signalr.Hub
}

Add functions with your custom hub type as a receiver.

func (h *AppHub) SendChatMessage(message string) {
    h.Clients().All().Send("chatMessageReceived", message)
}

These functions must be public so that they can be seen by the signalr server package but can be invoked client-side as lowercase message names. We'll explain setting up the client side in a moment, but as a preview, here's an example of calling our AppHub.SendChatMessage(...) method from the client:

    // javascript snippet invoking that AppHub.Send method from the client
    connection.invoke('sendChatMessage', val);

The signalr.HubInterface contains a pair of methods you can implement to handle connection and disconnection events. signalr.Hub contains empty implementations of them to satisfy the interface, but you can "override" those defaults by implementing your own functions with your custom hub type as a receiver:

func (c *chat) OnConnected(connectionID string) {
    fmt.Printf("%s connected\n", connectionID)
}

func (c *chat) OnDisconnected(connectionID string) {
   fmt.Printf("%s disconnected\n", connectionID)
}

Serve with http.ServeMux

import (
    "net/http"
	
    "github.com/philippseith/signalr"
)

func runHTTPServer() {
    address := 'localhost:8080'
    
    // create an instance of your hub
    hub := AppHub{}
	
    // build a signalr.Server using your hub
    // and any server options you may need
    server, _ := signalr.NewServer(context.TODO(),
        signalr.SimpleHubFactory(hub)
        signalr.KeepAliveInterval(2*time.Second),
        signalr.Logger(kitlog.NewLogfmtLogger(os.Stderr), true))
    )
    
    // create a new http.ServerMux to handle your app's http requests
    router := http.NewServeMux()
    
    // ask the signalr server to map it's server
    // api routes to your custom baseurl
    server.MapHTTP(signalr.WithHTTPServeMux(router), "/chat")

    // in addition to mapping the signalr routes
    // your mux will need to serve the static files
    // which make up your client-side app, including
    // the signalr javascript files. here is an example
    // of doing that using a local `public` package
    // which was created with the go:embed directive
    // 
    // fmt.Printf("Serving static content from the embedded filesystem\n")
    // router.Handle("/", http.FileServer(http.FS(public.FS)))
    
    // bind your mux to a given address and start handling requests
    fmt.Printf("Listening for websocket connections on http://%s\n", address)
    if err := http.ListenAndServe(address, router); err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

Client side: JavaScript/TypeScript

Grab copies of the signalr scripts

Microsoft has published the client-side libraries as a node package with embedded typescript annotations: @microsoft/signalr.

You can install @microsoft/signalr through any node package manager:

package manager command
npm npm install @microsoft/signalr@latest
yarn yarn add @microsoft/signalr@latest
LibMan libman 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
none you can download the version we are using in our chatsample from here (the minified version is here)

Use a HubConnection to connect to your server Hub

How you format your client UI is going to depend on your application use case but here is a simple example. It illustrates the basic steps of connecting to your server hub:

  1. import the signalr.js library (or signalr.min.js);

  2. create a connection object using the HubConnectionBuilder;

  3. bind events

    • UI event handlers can use connection.invoke(targetMethod, payload) to send invoke functions on the server hub;
    • connection event handlers can react to the messages sent from the server hub;
  4. start your connection

<html>
<body>
    <!-- you may want the content you send to be dynamic -->
    <input type="text" id="message" />
    
    <!-- you may need a trigger to initiate the send -->
    <input type="button" value="Send" id="send" />
    
    <!-- you may want some container to display received messages -->
    <ul id="messages">
    </ul>

    <!-- 1. you need to import the signalr script which provides
            the HubConnectionBuilder and handles the connection
            plumbing.
    -->
    <script src="js/signalr.js"></script>
    <script>
    (async function () {
        // 2. use the signalr.HubConnectionBuilder to build a hub connection
        //    and point it at the baseurl which you configured in your mux
        const connection = new signalR.HubConnectionBuilder()
                .withUrl('/chat')
                .build();

        // 3. bind events:
        //    - UI events can invoke (i.e. dispatch to) functions on the server hub
        document.getElementById('send').addEventListener('click', sendClicked);
        //    - connection events can handle messages received from the server hub
        connection.on('chatMessageReceived', onChatMessageReceived);

        // 4. call start to initiate the connection and start streaming events
        //    between your server hub and your client connection
        connection.start();
        
        // that's it! your server and client should be able to communicate
        // through the signalr.Hub <--> connection pipeline managed by the
        // signalr package and client-side library.
        
        // --------------------------------------------------------------------
       
        // example UI event handler
        function sendClicked() {
            // prepare your target payload
            const msg = document.getElementById('message').value;
            if (msg) {
                // call invoke on your connection object to dispatch
                // messages to the server hub with two arguments:
                // -  target: name of the hub func to invoke
                // - payload: the message body
                // 
                const target = 'sendChatMessage';
                connection.invoke(target, msg);
            }
        }

        // example server event handler
        function onChatMessageReceived(payload) {
            // the payload is whatever was passed to the inner
            // clients' `Send(...)` method in your server-side
            // hub function.
           
            const li = document.createElement('li');
            li.innerText = payload;
            document.getElementById('messages').appendChild(li);
        }
    })();
    </script>
</body>
</html>

Client side: go

To handle callbacks from the server, create a receiver class which gets the server callbacks mapped to its methods:

type receiver struct {
	signalr.Hub
}

func (c *receiver) Receive(msg string) {
	fmt.Println(msg)
}

Receive gets called when the server does something like this:

hub.Clients().Caller().Send("receive", message)

The client itself might be used like that:

// Create a Connection (with timeout for the negotiation process)
creationCtx, _ := context.WithTimeout(ctx, 2 * time.Second)
conn, err := signalr.NewHTTPConnection(creationCtx, address)
if err != nil {
    return err
}
// Create the client and set a receiver for callbacks from the server
client, err := signalr.NewClient(ctx,
	signalr.WithConnection(conn),
	signalr.WithReceiver(receiver))
if err != nil {
    return err
}
// Start the client loop
c.Start()
// Do some client work
ch := <-c.Invoke("update", data)
// ch gets the result of the update operation

Debugging

Server, Client and the protocol implementations are able to log most of their operations. The logging option is disabled by default in all tests. To configure logging, edit the testLogConf.json file:

{
  "Enabled": false,
  "Debug": false
}
  • If Enabled is set to true, the logging will be enabled. The tests will log to os.Stderr.
  • If Debug ist set to true, the logging will be more detailed.

signalr's People

Contributors

andig avatar benlocal avatar davidalpert avatar davidfowl avatar dependabot[bot] avatar dillondrobena avatar dreamsxin avatar eerosal avatar jagopg avatar philippseith avatar roks0n avatar snyk-bot avatar sruehl avatar vanackere avatar way-dirk avatar x0y0z0tn avatar yaron2 avatar zwbdzb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

signalr's Issues

Client without Receiver, Server without Hub: Set specific handler funcs instead

Callback handlers in the client should not only be possible with public methods in a class given by the Receiver option, but also by passing single handler functions

action := func(paraminterface{}) { ... }

NewClient(..., ReceiveFunc(„ProductUpdate“, action), ...)

Originally posted by @philippseith in #65 (comment)

The server interface could be extended using the same pattern:

method := func(param ...interface{}) []interface{} { ... }

NewServer(..., Method("AddProduct", method) ...)

ReceiveFunc can be used also with the server.

Add client reconnection

I'd like using signalr in a long-running server application (evcc-io/evcc#1586). That requires reconnecting if necessary. At this time I couldn't find out how to enable this as part of this module or detect connection drop from consumer side?

Implement AllowReconnect

Currently, Server.Run ends after a closeMessage is send. When using MapHub, the client can reestablish a connection, but not on SignalR protocol layer, only by initiating a new websocket connection.

  1. It should be possible to start a new handshake and a HubConnection without closing the underlying transport connection
  2. There should be an option to enable/disable reconnecting

binding to mux other than http.ServeMux is difficult

while trying to use this package with gorilla/mux I couldn't figure out how to bind the server to my router.

I came up with two ideas on how to use this package with gorilla/mux (and potentially other community mux implementations) more easily:

  1. #28 expose HttpMux as an http.Handler implementation
  2. #29 introduce a new signalr.MappableRouter interface and bind server.MapHTTP through this interface

Sending binary data without transforming to base64?

Hi, thanks a lot for this awesome library!

I'm trying to send binary data (from an mp3) over SignalR, and finally realized after some troubleshooting that SignalR is transforming it to base64 behind the scenes. This is fine, but I'm wondering if there would be a performance advantage in sending the binary data as-is directly to the clients.

Is there an option I can pass to tell SignalR I want to send binary data and not convert it to a base64?

Thanks!

Separate projects for client and server.

Nice work, really appreciate it.
But would it be better to split this project into two? As no client code would need a server and the client in the same code base.

Reconnect does not restore client subscriptions

I'm creating the client like this:

client, err := signalr.NewClient(context.Background(),
	signalr.WithAutoReconnect(c.connect(ts)),
	signalr.WithReceiver(c),
	signalr.Logger(easee.SignalrLogger(c.log.TRACE), true),
)

When I connect to the remote server and subscribe for updates I'm greeted with a bunch of messages:

[easee ] TRACE 2021/11/30 21:41:48 level=debug caller=client.go:259 state=1
[easee ] TRACE 2021/11/30 21:41:48 POST https://api.easee.cloud/hubs/chargers/negotiate
[easee ] TRACE 2021/11/30 21:41:48 connection=-gVGevD2xUgR5IBoUYV0jw level=debug caller=client.go:523 event=handshake sent msg={"protocol":"json","version":1}
[easee ] TRACE 2021/11/30 21:41:48 connection=-gVGevD2xUgR5IBoUYV0jw level=debug caller=client.go:553 event=handshake received msg=signalr.handshakeResponse{Error:""}
[easee ] TRACE 2021/11/30 21:41:48 level=debug caller=client.go:259 state=2
[easee ] TRACE 2021/11/30 21:41:48 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":1,"target":"SubscribeWithCurrentState","invocationId":"1","arguments":["EH635870",true]}
[easee ] TRACE 2021/11/30 21:41:48 level=debug caller=jsonhubprotocol.go:76 event=read message={"type":1,"target":"ProductUpdate","arguments":[{"mid":"EH635870","dataType":2,"id":15,"timestamp":"2021-11-30T12:10:01Z","value":"1"}]}

However, after reconnect (forced by disabling Wifi), data is no longer received:

[easee ] TRACE 2021/11/30 21:44:47 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:44:52 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:44:57 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:02 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:07 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:12 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:13 level=debug caller=jsonhubprotocol.go:76 event=read message={"type":6}
[easee ] TRACE 2021/11/30 21:45:13 level=debug caller=jsonhubprotocol.go:76 event=read message={"type":6}
[easee ] TRACE 2021/11/30 21:45:13 connection=at72LoSuEwXQd7qK42Yoog level=debug caller=loop.go:285 event=message received message=signalr.hubMessage{Type:6}
[easee ] TRACE 2021/11/30 21:45:13 connection=at72LoSuEwXQd7qK42Yoog level=debug caller=loop.go:285 event=message received message=signalr.hubMessage{Type:6}
[easee ] TRACE 2021/11/30 21:45:13 level=debug caller=jsonhubprotocol.go:76 event=read message={"type":7,"allowReconnect":true}
[easee ] TRACE 2021/11/30 21:45:13 connection=at72LoSuEwXQd7qK42Yoog level=debug caller=loop.go:91 {{7  true} <nil>}=message received message=signalr.closeMessage{Type:7, Error:"", AllowReconnect:true}
[easee ] TRACE 2021/11/30 21:45:13 connection=at72LoSuEwXQd7qK42Yoog level=debug caller=loop.go:130 event=message loop ended
[easee ] TRACE 2021/11/30 21:45:13 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":7,"error":"","allowReconnect":false}
[easee ] TRACE 2021/11/30 21:45:13 level=info connect=*signalr.webSocketConnection: failed to write msg: WebSocket closed: received close frame: status = StatusNormalClosure and reason = ""
[easee ] TRACE 2021/11/30 21:45:14 level=debug caller=client.go:259 state=1
[easee ] TRACE 2021/11/30 21:45:14 POST https://api.easee.cloud/hubs/chargers/negotiate
[easee ] TRACE 2021/11/30 21:45:16 connection=-dB9hjF7o1oTAxjYC5pKuw level=debug caller=client.go:523 event=handshake sent msg={"protocol":"json","version":1}
[easee ] TRACE 2021/11/30 21:45:16 connection=-dB9hjF7o1oTAxjYC5pKuw level=debug caller=client.go:553 event=handshake received msg=signalr.handshakeResponse{Error:""}
[easee ] TRACE 2021/11/30 21:45:16 level=debug caller=client.go:259 state=2
[easee ] TRACE 2021/11/30 21:45:21 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:26 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:31 level=debug caller=jsonhubprotocol.go:76 event=read message={"type":6}
[easee ] TRACE 2021/11/30 21:45:31 connection=-dB9hjF7o1oTAxjYC5pKuw level=debug caller=loop.go:285 event=message received message=signalr.hubMessage{Type:6}
[easee ] TRACE 2021/11/30 21:45:36 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:41 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}

Is it possible that the subscriptions are not restored?

Improve debug logging

When using debug logging, the log will contains something like this:

Arguments:[]interface {}{json.RawMessage{0x7b, 0x22, 0x6d, 0x69, 0x64, 0x22, 0x3a, 0x22, 0x45, 0x48, 0x36, 0x33, 0x35, 0x38, 0x37, 0x30, 0x22, 0x2c, 0x22, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x33, 0x2c, 0x22, 0x69, 0x64, 0x22, 0x3a, 0x31, 0x39, 0x39, 0x2c, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x3a, 0x22, 0x32, 0x30, 0x32, 0x31, 0x2d, 0x30, 0x31, 0x2d, 0x31, 0x38, 0x54, 0x31, 0x33, 0x3a, 0x32, 0x39, 0x3a, 0x35, 0x36, 0x5a, 0x22, 0x2c, 0x22, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3a, 0x22, 0x33, 0x38, 0x38, 0x2e, 0x31, 0x34, 0x30, 0x30, 0x31, 0x34, 0x36, 0x34, 0x38, 0x34, 0x33, 0x38, 0x22, 0x7d}}, StreamIds:[]string(nil)}

It would be nice if the json could be displayed as string instead of bytes for readability and amount of log space. Duck-typing at the implementing logger is not possible as the embedding signalr.invocationMessage is private.

add cors support

e, When I use your signalr pgk, the request url is not match the origin header,

The server return 403 response status code. So the client can not connect with the server.

I have check the github source :

https://github.com/philippseith/signalr/blob/master/httpmux.go#L119

func (h *httpMux) handleWebsocket(writer http.ResponseWriter, request *http.Request) {
	websocketConn, err := websocket.Accept(writer, request,
		// Reuse compression sliding window. Should cause better compression with repetitive signalr messages
		&websocket.AcceptOptions{CompressionMode: websocket.CompressionContextTakeover},
	)
......

websocket.AcceptOptions do not pass the InsecureSkipVerify: true,
It mean that you have ingore the cors setting when handler the websockets request.

btw, I have intergate the cors pgk , so you can ingergate the pgk to get the user settings for the cors.

AutoReconnect currently broken in main branch code

I hope i don't disturb the dev experience too much, but AutoReconnect does not work properly (48ff89d)

When connected to any SignalR server everything is ok.
As soon as the opposing server "dies", the client will spam tons of error messages regarding reading from a closed websocket

example error log

ts=2021-11-02T20:36:43.6823154Z class=Client connection=4lO-LmP8pWerjdOek4MeJg hub=main.testHubHandler level=info event="handshake sent" msg="{\"protocol\":\"messagepack\",\"version\":1}\u001e" error="failed to write msg: WebSocket closed: failed to read frame header: read tcp 127.0.0.1:61265->127.0.0.1:5000: wsarecv: An existing connection was forcibly closed by the remote host."
ts=2021-11-02T20:36:43.6824605Z class=Client connection=4lO-LmP8pWerjdOek4MeJg hub=main.testHubHandler level=info event="handshake sent" msg="{\"protocol\":\"messagepack\",\"version\":1}\u001e" error="failed to write msg: WebSocket closed: failed to read frame header: read tcp 127.0.0.1:61265->127.0.0.1:5000: wsarecv: An existing connection was forcibly closed by the remote host."
ts=2021-11-02T20:36:43.6829672Z class=Client connection=4lO-LmP8pWerjdOek4MeJg hub=main.testHubHandler level=info event="handshake sent" msg="{\"protocol\":\"messagepack\",\"version\":1}\u001e" error="failed to write msg: WebSocket closed: failed to read frame header: read tcp 127.0.0.1:61265->127.0.0.1:5000: wsarecv: An existing connection was forcibly closed by the remote host."
ts=2021-11-02T20:36:43.6834886Z class=Client connection=4lO-LmP8pWerjdOek4MeJg hub=main.testHubHandler level=info event="handshake sent" msg="{\"protocol\":\"messagepack\",\"version\":1}\u001e" error="failed to write msg: WebSocket closed: failed to read frame header: read tcp 127.0.0.1:61265->127.0.0.1:5000: wsarecv: An existing connection was forcibly closed by the remote host."
ts=2021-11-02T20:36:43.6834886Z class=Client connection=4lO-LmP8pWerjdOek4MeJg hub=main.testHubHandler level=info event="handshake sent" msg="{\"protocol\":\"messagepack\",\"version\":1}\u001e" error="failed to write msg: WebSocket closed: failed to read frame header: read tcp 127.0.0.1:61265->127.0.0.1:5000: wsarecv: An existing connection was forcibly closed by the remote host."
ts=2021-11-02T20:36:43.6834886Z class=Client connection=4lO-LmP8pWerjdOek4MeJg hub=main.testHubHandler level=info event="handshake sent" msg="{\"protocol\":\"messagepack\",\"version\":1}\u001e" error="failed to write msg: WebSocket closed: failed to read frame header: read tcp 127.0.0.1:61265->127.0.0.1:5000: wsarecv: An existing connection was forcibly closed by the remote host."
... mutiple thousand lines until the client is closed via CTRL+C

client code

package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/google/uuid"
	"github.com/philippseith/signalr"
)

func main() {
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
	defer cancel()

	client, err := signalr.NewClient(ctx,
		signalr.TransferFormat("Binary"),
		signalr.EnableDetailedErrors(true),
		signalr.WithReceiver(&testHubHandler{}),
		// signalr.WithConnection(conn),
		signalr.WithAutoReconnect(func() (signalr.Connection, error) {
			return signalr.NewHTTPConnection(ctx, "http://127.0.0.1:5000/hubs/services", signalr.WithHTTPHeaders(func() http.Header {
				return http.Header{
					"Service-Name":  {"one"},
					"Service-Id":    {uuid.New().String()},
					"Authorization": {"ApiKey 123456"},
				}
			}))
		}),
	)

	if err != nil {
		panic(err)
	}
	client.Start()
}

type testHubHandler struct {
	signalr.Receiver
	nmsg int
}

func (r *testHubHandler) Test() {
	fmt.Printf("test\n")
}
func (r *testHubHandler) Time(t time.Time) {
	r.nmsg++
	if r.nmsg%100 == 0 {
		fmt.Printf("From Server: %s\n", t.Format(time.RFC3339))
	}
}

Cannot create pull stream / missing method

I'm trying to rebuild this example using the go client: https://developer.easee.cloud/page/signalr-code-examples

I'm assuming (I'm new to to SignalR) that on is synonym with PullStream?

This is what I have:

client, err := signalr.NewClient(ctx, conn)
if err != nil {
	return c, err
}

if err := client.Start(); err != nil {
	return c, err
}

if err := <-client.Send("SubscribeWithCurrentState", c.charger, true); err != nil {
	return c, err
}

go func() {
	productUpdateChan := client.PullStream("ProductUpdate")
	for res := range productUpdateChan {
		if res.Error != nil {
			c.log.ERROR.Println("ProductUpdate", res.Error)
		} else {
			c.log.TRACE.Printf("ProductUpdate %s", res.Value)
		}
	}
}()

Unfortunately, this always errors:

class=Client connection=Dbd8yRimrV-ORMlmZj6SLQ level=debug caller=loop.go:186 event="message received" message="signalr.invocationMessage{Type:1, Target:\"ProductUpdate\", InvocationID:\"\", Arguments:[]interface {}{json.RawMessage{0x7b, 0x22, 0x6d, 0x69, 0x64, 0x22, 0x3a, 0x22, 0x45, 0x48, 0x36, 0x33, 0x35, 0x38, 0x37, 0x30, 0x22, 0x2c, 0x22, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x32, 0x2c, 0x22, 0x69, 0x64, 0x22, 0x3a, 0x31, 0x36, 0x2c, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x3a, 0x22, 0x32, 0x30, 0x32, 0x31, 0x2d, 0x30, 0x39, 0x2d, 0x31, 0x37, 0x54, 0x30, 0x36, 0x3a, 0x30, 0x36, 0x3a, 0x34, 0x32, 0x5a, 0x22, 0x2c, 0x22, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3a, 0x22, 0x31, 0x22, 0x7d}}, StreamIds:[]string(nil)}"
class=Client connection=Dbd8yRimrV-ORMlmZj6SLQ level=info event=getMethod error="missing method" name=ProductUpdate reaction="send completion with error"

It seems I have no way getting around the missing method error?

Allow client to act as server and vice versa

Extension to #2
After the handshake is completed the client should be able to act as server for his connection.
On the other hand, a server started with one hub per connection policy could act as client for this connection.

message failed to read: read limited at 32769 bytes and the client doesn't reconnect

Hi,
sometimes I get this error

level=info ts=2022-11-07T13:22:52.054734Z class=Client connection=1Fdtl_ixQZvEL6Mbhw3mgQ
hub=main.receiver ts=2022-11-07T13:22:52.054735Z
error="*signalr.webSocketConnection: failed to read: read limited at 32769 bytes"
message=<nil> reaction="close connection"
level=info ts=2022-11-07T13:22:52.05502Z
connect="*signalr.webSocketConnection: failed to read: read limited at 32769 bytes

after that, the client doesn't reconnect.

I create the client in this way

        client, err := signalr.NewClient(
                context.Background(), nil,
                signalr.WithReceiver(rcv),
                signalr.WithConnector(func() (signalr.Connection, error) {
                        logger.Debug("signalr client connecting...")

                        creationCtx, _ := context.WithTimeout(context.Background(), 10*time.Second)
                        if err != nil {
                                logger.Error(err)
                        }

                        return conn, err
                }),
                signalr.Logger(kitlog.NewLogfmtLogger(os.Stderr), true))
        if err != nil {
                logger.Error(err.Error())
        }

  
        client.Start()

        go func() {
                select {
                case result := <-client.Invoke("SubscribeToOperations", invokeData):
                        logger.Debugf("invk operations [%#v]", result)
                }
        }()

When it happens I can see the log signalr client connecting... although the client doesn't reconnect or I don't receive more messages from the server.

Any suggestion?
Thanks in advance.

Getting started

I am having trouble getting started with this library and could use some more examples.

I am happy to contribute more documentation and examples as I am able to get this up and running in my project.

Better logging interoperability

The current StructuredLogger interface is too basic and requires manually filtering some data, especially the levels

Suggestion:

  • Drop "ts" from the key-value pair (is provided by used logger anyways)
  • Drop "level" from
  • Add "Debug", "Info", "Warn", "Error" functions to (maybe to a separate, more specific Logger interface)
    • These functions should as first parameter take the actual log message, afterwards a "key-value" of structured information, like the current interface
      this allows us to more comfortably use other loggers and simplify implementing the interface for certain use cases.

currently:

type signalRLogger struct {
	zap *zap.Logger
}

func (l *signalRLogger) Log(keyVals ...interface{}) error {
	if l.zap == nil {
		var err error
		l.zap, err = zap.NewDevelopment()
		if err != nil {
			return err
		}
	}
	lvl := level.DebugValue()
	logger := l.zap.Sugar()
	for i := 0; i < len(keyVals); i += 2 {
		_, isStr := keyVals[i].(string)
		if !isStr {
			// wtf
			continue
		}
		if keyVals[i] == "ts" {
			continue
		}
		if keyVals[i] == level.Key() {
			if gokit, ok := keyVals[i+1].(level.Value); ok {
				lvl = gokit
			}
			continue
		}
		logger = logger.With(keyVals[i], keyVals[i+1])
	}

	switch lvl {
	case level.DebugValue():
		logger.Debug()
	case level.InfoValue():
		logger.Info()
	case level.WarnValue():
		logger.Warn()
	case level.ErrorValue():
		logger.Error()
	default:
		logger.Error()
	}

	return nil
}

proposed:

type StructuredLogger interface {
	Debug(message string, keyVals interface{}) error
	Info(message string, keyVals interface{}) error
	Warn(message string, keyVals interface{}) error
	Error(message string, keyVals interface{}) error
}

// example implementation with zap

func (l *signalRLogger) Debug(message string, keyVals ...interface{}) error {
	l.zap.Sugar().Debugw(message, keyVals...)
	return nil
}

func (l *signalRLogger) Info(message string, keyVals ...interface{}) error {
	l.zap.Sugar().Infow(message, keyVals...)
	return nil
}

func (l *signalRLogger) Warn(message string, keyVals ...interface{}) error {
	l.zap.Sugar().Warnw(message, keyVals...)
	return nil
}

func (l *signalRLogger) Error(message string, keyVals ...interface{}) error {
	l.zap.Sugar().Errorw(message, keyVals...)
	return nil
}

// example in go-kit/log

func (l *signalRLogger) Debug(message string, keyVals ...interface{}) error {
	return gokit.With(level.Debug(gokitLogger), "message", message).Log(keyVals...)
}

func (l *signalRLogger) Info(message string, keyVals ...interface{}) error {
	return gokit.With(level.Info(gokitLogger), "message", message).Log(keyVals...)
}

func (l *signalRLogger) Warn(message string, keyVals ...interface{}) error {
	return gokit.With(level.Warn(gokitLogger), "message", message).Log(keyVals...)
}

func (l *signalRLogger) Error(message string, keyVals ...interface{}) error {
	return gokit.With(level.Error(gokitLogger), "message", message).Log(keyVals...)
}

Handshake failing when connecting to .NET signalr server

I'm trying to connect to a .NET server running signalr, but for some reason handshake is failing due to No Connection with that ID error.

I can successfully connect to that particular signalr server using signalr typescript client.

I've put some Println's in the code in order to see if connection id really isn't being passed along but from what I'm able to see it is.

Here are the Print outputs that I've added to httpconnection.go (signalr lib) and dial.go (websocket lib):

reqURL: https://server/path
negotiateURL url: https://server/path/negotiate
Negotiation response: {"connectionId":"XKoZcw-gWaEAz5WGMjigtA","availableTransports":[{"transport":"WebSockets","transferFormats":["Text","Binary"]},{"transport":"ServerSentEvents","transferFormats":["Text"]},{"transport":"LongPolling","transferFormats":["Text","Binary"]}]}
Req url after connection id: https://server/path?id=XKoZcw-gWaEAz5WGMjigtA
wsURL= wss://server/path?id=XKoZcw-gWaEAz5WGMjigtA
Before handshakeRequest: wss://server/path?id=XKoZcw-gWaEAz5WGMjigtA
secWebSocketKey: n/YW1icIRHIljCaKnveplw==
Handshake resp.body: No Connection with that ID
panic: failed to WebSocket dial: expected handshake response status code 101 but got 404

And this is how I instantiated the client:

type receiver struct {
	signalr.Receiver
}

func (r *receiver) Receive(msg string) {
	fmt.Println(msg)
	// The silly client urges the server to end his connection after 10 seconds
	r.Server().Send("abort")
}

func main() {
	address := "https://server/path"

	// Create a Connection (with timeout for the negotiation process)
	ctx := context.Background()
	creationCtx, _ := context.WithTimeout(ctx, 2*time.Second)
	conn, err := signalr.NewHTTPConnection(creationCtx, address)
	if err != nil {
		panic(err)
	}

	receiver := &receiver{}
	// Create the client and set a receiver for callbacks from the server
	client, err := signalr.NewClient(ctx, nil,
		signalr.WithConnection(conn),
		signalr.WithReceiver(receiver))
	if err != nil {
		fmt.Println(err)
		// panic(err)
	}
	// Start the client loop
	client.Start()
}

I didn't notice anything out of the ordinary with the constructed requests (investigated headers as well) hence I'm in the dark if I'm perhaps using the client in a wrong way?

go function leak in watchDog

Using 0.4.1
I think there is a go function leak in websocketconnection watchDog.

After running for 5 minutes with a few clients (browsers) connected I see a lot of pending go threads, and the list keep growing
goleak

One hub instance per connection

Currently it is only possible to use one hub instance for all connections or one hub instance per method call. It should be possible to have a 1:1 assignment between client and server

Re-add client.Stop()

After #76 (comment), Stop has been removed without a replacement:

To complete the API change, the Stop() method was removed, as stopping of the client could be done by canceling the context passed to NewClient()

and

I finally got the intention here :-) and changed the meaning of the context.

I'd suggest to re-add Stop.

Defend against downstream panics

I'm not sure how much of a real problem that is, but I've seem sporadic drops of connection with stack traces similar to this:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x102f328d4]

goroutine 259 [running]:
strings.(*Builder).WriteString(...)
	/opt/homebrew/Cellar/go/1.17.3/libexec/src/strings/builder.go:124
github.com/evcc-io/evcc/charger/easee.(*logger).Log(0x1400057f890, {0x140000a0300, 0xc, 0x10})
	/Users/andig/htdocs/evcc/charger/easee/log.go:36 +0x3b4
github.com/go-kit/log/level.(*logger).Log(0x1400034bc00, {0x140000a0300, 0xc, 0x10})
	/Users/andig/go/pkg/mod/github.com/go-kit/[email protected]/level/level.go:63 +0xe8
github.com/go-kit/log.(*context).Log(0x140008efe50, {0x14000742e80, 0x4, 0x4})
	/Users/andig/go/pkg/mod/github.com/go-kit/[email protected]/log.go:168 +0x39c
github.com/philippseith/signalr.(*jsonHubProtocol).ParseMessages(0x140006c9de0, {0x10458fc60, 0x140001af280}, 0x1400057fb90)
	/Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/jsonhubprotocol.go:76 +0x24c
github.com/philippseith/signalr.(*defaultHubConnection).Receive.func2(0x14000bf4000, {0x1045aff10, 0x1400034be00}, {0x10458fc60, 0x140001af280}, 0x14000bd7620, 0x140004dd3e0)
	/Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/hubconnection.go:137 +0xc4
created by github.com/philippseith/signalr.(*defaultHubConnection).Receive
	/Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/hubconnection.go:127 +0x1ac
exit status 2

In this case it was an NPE in the downstream logging code. Was wondering if hubconnection could defend better against downstream panics using recover? I don't have a concrete suggestion though, so feel free to close.

Switch to github.com/go-kit/log

Using github.com/go-kit/kit/log pulls the entire go-kit universe into the codebase. It would be great to switch to github.com/go-kit/log which is a minimal-footprint drop-in replacement.

Missing client example

The README mentions js clients but does not give an example for the Go client. It would be great to have end-to-end Go examples available.

Connection lost missing from client debug log

I'm creating the client with:

client, err := signalr.NewClient(context.Background(),
	signalr.WithAutoReconnect(c.connect(ts)),
	signalr.WithReceiver(c),
	signalr.Logger(easee.SignalrLogger(c.log.TRACE), true),
)

I expect that a dropped connection appears in the debug log. However, I don't see that the connection is dropped:

[easee ] TRACE 2021/11/30 21:44:12 connection=at72LoSuEwXQd7qK42Yoog level=debug caller=loop.go:285 event=message received message=signalr.hubMessage{Type:6}
[easee ] TRACE 2021/11/30 21:44:17 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:44:22 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:44:27 level=debug caller=jsonhubprotocol.go:76 event=read message={"type":6}
[easee ] TRACE 2021/11/30 21:44:27 connection=at72LoSuEwXQd7qK42Yoog level=debug caller=loop.go:285 event=message received message=signalr.hubMessage{Type:6}
[easee ] TRACE 2021/11/30 21:44:32 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:44:37 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:44:42 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}


[easee ] TRACE 2021/11/30 21:44:47 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:44:52 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:44:57 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:02 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:07 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:12 level=debug caller=jsonhubprotocol.go:211 event=write message={"type":6}
[easee ] TRACE 2021/11/30 21:45:13 level=debug caller=jsonhubprotocol.go:76 event=read message={"type":6}
[easee ] TRACE 2021/11/30 21:45:13 level=debug caller=jsonhubprotocol.go:76 event=read message={"type":6}

The empty lines is where I switched Wifi off/on. There is no dropped/reconnect message and the write loop seems to continue.

Client maintains HTTP connection when negotiate fails with a 404

I am using the SignalR client with the WithConnector option to maintain a connection to the SignalR server.
I have found that if the route the client tries to connect to returns a 404, the client maintains the HTTP connection to the server. Using TCPView, I can see multiple established connections for each connection attempt.

After doing some investigating, it appears the issue has to do with DisableKeepAlives not being set to true when creating the http.Client in httpconnection.go NewHTTPConnection.

Been also experiencing an issue where after about 5 minutes of attempts, NewHTTPConnection hangs on httpConn.client.Do, still trying to investigate this issue futher.

For reference, below is a sample application I've been using to reproduce this issue.

package main

import (
	"context"
	"github.com/philippseith/signalr"
	"time"
)

var connectionCtx context.Context
var connectionCancel context.CancelFunc
var client signalr.Client

func main() {
	connectionLoopCtx, _ := context.WithCancel(context.Background())

	var err error
	client, err = signalr.NewClient(connectionLoopCtx, signalr.WithConnector(getConnection))
	if err != nil {
		panic(err)
	}

	client.Start()

	for {
	}
}

func getConnection() (signalr.Connection, error) {
	if connectionCtx != nil {
		connectionCancel()
	}
	connectionCtx, connectionCancel = context.WithTimeout(context.Background(), 2*time.Second)

	return signalr.NewHTTPConnection(connectionCtx, "https://localhost:11001/signalr/badroute")
}

Client: Start and WaitForState are ambiguous

Both return an error. It is not clear which kind of error would be returned where. E.g. if a client is not able to connect- would I see this as part of Start or WaitForState?

[Demo]Websocket connection fails

Hello,

First of all, great tool !!

I am having some difficulties running the demo. I am having a problem with WebSocket failing to connect.

Any tips?

Thanks!

issue

0.6.1: connect=*signalr.webSocketConnection: context canceled

Since 0.6.1 I'm no longer able to connect:

[easee ] TRACE 2023/04/05 16:10:05 POST https://api.easee.cloud/hubs/chargers/negotiate
[easee ] TRACE 2023/04/05 16:10:05 {"negotiateVersion":0,"connectionId":"NhkJRDibpdJNrtcaf78kQQ","availableTransports":[{"transport":"WebSockets","transferFormats":["Text","Binary"]},{"transport":"ServerSentEvents","transferFormats":["Text"]},{"transport":"LongPolling","transferFormats":["Text","Binary"]}]}
[easee ] TRACE 2023/04/05 16:10:05 level=info connection=NhkJRDibpdJNrtcaf78kQQ event=handshake sent msg={"protocol":"json","version":1} error=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:05 level=info connect=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:06 level=info connection=NhkJRDibpdJNrtcaf78kQQ event=handshake sent msg={"protocol":"json","version":1} error=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:06 level=info connect=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:07 level=info connection=NhkJRDibpdJNrtcaf78kQQ event=handshake sent msg={"protocol":"json","version":1} error=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:07 level=info connect=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:08 level=info connection=NhkJRDibpdJNrtcaf78kQQ event=handshake sent msg={"protocol":"json","version":1} error=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:08 level=info connect=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:09 level=info connection=NhkJRDibpdJNrtcaf78kQQ event=handshake sent msg={"protocol":"json","version":1} error=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:09 level=info connect=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:13 level=info connection=NhkJRDibpdJNrtcaf78kQQ event=handshake sent msg={"protocol":"json","version":1} error=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:13 level=info connect=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:17 level=info connection=NhkJRDibpdJNrtcaf78kQQ event=handshake sent msg={"protocol":"json","version":1} error=*signalr.webSocketConnection: context canceled
[easee ] TRACE 2023/04/05 16:10:17 level=info connect=*signalr.webSocketConnection: context canceled

Last known good is bd5ffb6

Noisy repeating debug messages in client

I'm currently trying to use only the client part of this library, the server is a .NET 6 application. This works in general, but I am getting some weird debug messages that I couldn't figure out how to stop. I've created a minimal example that still shows this behaviour for me. The real code is listening for events from the server using a receiver, but the issue appears even if I remove that part. The server is also not sending any messages in this case, this always happens when I connect with the client.

func connectSignalR(address string) {
	ctx := context.Background()
	conn, _ := signalr.NewHTTPConnection(ctx, address)

	client, _ := signalr.NewClient(ctx, signalr.WithConnection(conn))

	client.Start()

	<-client.WaitForState(ctx, signalr.ClientConnected)
	fmt.Printf("Connected\n")

	<-client.WaitForState(ctx, signalr.ClientClosed)
	fmt.Printf("Connection closed\n")
}

The result looks like the following in the terminal

level=debug caller=options.go:119
level=debug caller=options.go:119
level=debug caller=options.go:119
level=debug caller=options.go:119
Connected
level=debug caller=options.go:119

The output continues if I leave the program running. I looked at the options.go line mentioned, but I could not figure out what exactly is happening here and why I get this output.

The only mentions of configuring the logging I found in the documentation was the testLogConf.json and I didn't touch that. This message also seems kinda useless, so it probably isn't an intentional one anyway.

Any idea where this message is coming from and how to silence it?

InvalidDataException: Missing required property 'invocationId'.

Hi,
I am trying to connect to the Tezos API https://api.tzkt.io/

You can see a related javascript example here

https://api.tzkt.io/#section/JS-simple-client

Although when I invoke the operation called SubscribeToHead I received this error

level=debug ts=2022-11-04T16:43:25.828707Z protocol=JSON ts=2022-11-04T16:43:25.828709Z
caller=jsonhubprotocol.go:76 event=read
message="{\"type\":7,\"error\":\"Connection closed with an error. InvalidDataException: Missing required property 'invocationId'.\",\"allowReconnect\":true}"

this is the code

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/philippseith/signalr"
)


type resultReceiver struct {
}

func (r *resultReceiver) OnResult(result string) {
	log.Println("rslt rcv !!!!!!", result)
}

func main() {
	address := "https://api.tzkt.io/v1/ws"
	fmt.Println("starting...")

	ctx := context.Background()

	creationCtx, _ := context.WithTimeout(ctx, 10*time.Second)
	conn, err := signalr.NewHTTPConnection(creationCtx, address)
	if err != nil {
		log.Println(err.Error())
		return
	}

	rcv := new(resultReceiver)

	client, err := signalr.NewClient(
		ctx,
		signalr.WithConnection(conn),
		signalr.WithReceiver(rcv))

	go func() {
		client.Start()

		select {
		case result := <-client.Invoke("SubscribeToHead"):
			log.Printf("invk!!!! [%#v]", result)
		}
	}()

	done := make(chan os.Signal, 1)
	signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
	fmt.Println("Blocking, press ctrl+c to continue...")
	<-done
}

and this is the full log

starting...
level=debug ts=2022-11-04T16:43:25.117716Z caller=client.go:289 state=1
Blocking, press ctrl+c to continue...
level=debug ts=2022-11-04T16:43:25.11875Z class=Client connection=4WSUIddVSNDlijRKsZ6FZg hub=main.resultReceiver ts=2022-11-04T16:43:25.118751Z caller=client.go:557 event="handshake sent" msg="{\"protocol\":\"json\",\"version\":1}\u001e"
level=debug ts=2022-11-04T16:43:25.3589Z class=Client connection=4WSUIddVSNDlijRKsZ6FZg hub=main.resultReceiver ts=2022-11-04T16:43:25.3589Z caller=client.go:587 event="handshake received" msg="signalr.handshakeResponse{Error:\"\"}"
level=debug ts=2022-11-04T16:43:25.359004Z caller=client.go:289 state=2
level=debug ts=2022-11-04T16:43:25.359129Z protocol=JSON ts=2022-11-04T16:43:25.359129Z caller=jsonhubprotocol.go:211 event=write message="{\"type\":1,\"target\":\"SubscribeToHead\",\"invocationId\":\"1\",\"arguments\":[]}\u001e"
level=debug ts=2022-11-04T16:43:25.593891Z protocol=JSON ts=2022-11-04T16:43:25.593894Z caller=jsonhubprotocol.go:76 event=read message="{\"type\":1,\"target\":\"head\",\"arguments\":[{\"type\":0,\"state\":2855290}]}"
level=debug ts=2022-11-04T16:43:25.594163Z protocol=JSON ts=2022-11-04T16:43:25.594164Z caller=jsonhubprotocol.go:76 event=read message="{\"type\":3,\"invocationId\":\"1\",\"result\":2855290}"
level=debug ts=2022-11-04T16:43:25.594293Z class=Client connection=4WSUIddVSNDlijRKsZ6FZg hub=main.resultReceiver ts=2022-11-04T16:43:25.594293Z caller=loop.go:184 event="message received" message="signalr.invocationMessage{Type:1, Target:\"head\", InvocationID:\"\", Arguments:[]interface {}{\"{\\\"type\\\":0,\\\"state\\\":2855290}\"}, StreamIds:[]string(nil)}"
level=info ts=2022-11-04T16:43:25.594369Z class=Client connection=4WSUIddVSNDlijRKsZ6FZg hub=main.resultReceiver ts=2022-11-04T16:43:25.594369Z event=getMethod error="missing method" name=head reaction="send completion with error"
level=debug ts=2022-11-04T16:43:25.594497Z protocol=JSON ts=2022-11-04T16:43:25.594497Z caller=jsonhubprotocol.go:211 event=write message="{\"type\":3,\"invocationId\":\"\",\"error\":\"Unknown method head\"}\u001e"
level=debug ts=2022-11-04T16:43:25.594784Z class=Client connection=4WSUIddVSNDlijRKsZ6FZg hub=main.resultReceiver ts=2022-11-04T16:43:25.594785Z caller=loop.go:264 event="message received" message="signalr.completionMessage{Type:3, InvocationID:\"1\", Result:json.RawMessage{0x32, 0x38, 0x35, 0x35, 0x32, 0x39, 0x30}, Error:\"\"}"
level=debug ts=2022-11-04T16:43:25.594841Z protocol=JSON ts=2022-11-04T16:43:25.594841Z caller=jsonhubprotocol.go:60 event=UnmarshalArgument argument=2855290 value=2.85529e+06
2022/11/04 13:43:25 invk!!!! [signalr.InvokeResult{Value:2.85529e+06, Error:error(nil)}]
level=debug ts=2022-11-04T16:43:25.828707Z protocol=JSON ts=2022-11-04T16:43:25.828709Z caller=jsonhubprotocol.go:76 event=read message="{\"type\":7,\"error\":\"Connection closed with an error. InvalidDataException: Missing required property 'invocationId'.\",\"allowReconnect\":true}"
level=debug ts=2022-11-04T16:43:25.828946Z class=Client connection=4WSUIddVSNDlijRKsZ6FZg hub=main.resultReceiver ts=2022-11-04T16:43:25.828946Z caller=loop.go:91 message="signalr.closeMessage{Type:7, Error:\"Connection closed with an error. InvalidDataException: Missing required property 'invocationId'.\", AllowReconnect:true}"
level=debug ts=2022-11-04T16:43:25.829046Z protocol=JSON ts=2022-11-04T16:43:25.829047Z caller=jsonhubprotocol.go:211 event=write message="{\"type\":7,\"error\":\"Connection closed with an error. InvalidDataException: Missing required property 'invocationId'.\",\"allowReconnect\":false}\u001e"
level=debug ts=2022-11-04T16:43:25.829353Z class=Client connection=4WSUIddVSNDlijRKsZ6FZg hub=main.resultReceiver ts=2022-11-04T16:43:25.829354Z caller=loop.go:130 event="message loop ended"
level=info ts=2022-11-04T16:43:25.829409Z connect="Connection closed with an error. InvalidDataException: Missing required property 'invocationId'."
level=debug ts=2022-11-04T16:43:25.829422Z caller=client.go:289 state=3

Any suggestion? thanks in advance.

Code Complete Server/Client Example

👋 Hi, I'm pretty new to Go and came across this package.

I was working through the README trying to setup a local server and client, but kept running into issues. I was wondering if you could amend the example to have a code complete solution for both parts to showcase the project. As someone who is new to Go it's a bit confusing as what is actually required to create these components. Right now I have...

Server

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	kitlog "github.com/go-kit/log"
	"github.com/philippseith/signalr"
)

type replayserver struct {
	signalr.Hub
}

func Serve(ctx context.Context) (string, error) {
	address := "localhost:8080"
	hub := receiver{}

	server, _ := signalr.NewServer(
		context.TODO(),
		signalr.SimpleHubFactory(&hub),
		signalr.KeepAliveInterval(2*time.Second),
		signalr.Logger(kitlog.NewLogfmtLogger(os.Stdout), true),
	)

	router := http.NewServeMux()
	server.MapHTTP(signalr.WithHTTPServeMux(router), "/negotiate")  // README was problematic here, where attempting to connect would immediate result in a failed to connect to /negotiate in the Client

	fmt.Printf("Listening for websocket connections on http://%s\n", address)

	if err := http.ListenAndServe(address, router); err != nil {
		log.Fatal("ListenAndServe:", err)
	}

	time.Sleep(5 * time.Second)
	hub.Clients().Caller().Send("update", "Hello from server")

	return "", nil
}

func main() {
	ctx := context.Background()
	Serve(ctx)
}

Client

package main

import (
	"context"
	"fmt"
	"time"
	"github.com/philippseith/signalr"
)

type client struct {
	signalr.Hub
}

func Listen(ctx context.Context) (string, error) {
	address := "http://localhost:8080"
	creationCtx, _ := context.WithTimeout(ctx, 2*time.Second)
	conn, err := signalr.NewHTTPConnection(creationCtx, address)

	if err != nil {
		fmt.Println(err)
		return "Errored connecting to addr", err
	}

	client, err := signalr.NewClient(
		ctx,
		signalr.WithConnection(conn),
		signalr.WithReceiver(&client{}),
	)

	if err != nil {
		return "Errored creating client", err
	}

	client.Start()
	ch := <-client.Invoke("update", "/data")
	fmt.Println(ch)
	return "done.", nil
}

func main() {
	ctx := context.Background()
	Listen(ctx)
}

When I run the server in a terminal session we sit around Listening for websocket connections on http://localhost:8080, when I try to launch the client, I'm getting an error,

POST http://localhost:8080/negotiate -> 400 Bad Request

I'll note that in the server if I change the .MapHTTP to serve on something like /data as the README shows I'll get back the following from the Client,

POST http://localhost:8080/negotiate -> 404 Not Found

Send events from outside the Hub

I am trying to create the pattern below

Obviously my intent is to have the hub forward all events recived from the b.Events channel

For testing I've created the ticker, but its panicking with

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x20 pc=0x127d24a]

goroutine 8 [running]:
github.com/philippseith/signalr.(*Hub).Clients(0xc00006a050?)
        C:/Users/ps/go/pkg/mod/github.com/philippseith/[email protected]/hub.go:32 +0x8a

Apparently context is nil.

hub.go:29

func (h *Hub) Clients() HubClients {
	h.cm.RLock()
	defer h.cm.RUnlock()
	return h.context.Clients()
}

What is the preferred way of brodcasting events not triggered by a hub method?

This is my code

b := BusHardware{}
go b.WireUp()
hub := MyHub{}
go Serve(&hub)
go Ticker(&hub)
for event := range b.Events {
	hub.Clients().All().Send(event.a, event.b)
}

func Ticker(hub *MyHub) {
	uptimeTicker := time.NewTicker(5 * time.Second)

	for {
		<-uptimeTicker.C

		hub.Clients().All().Send("Bow", "")
	}
}

func Serve(hub *MyHub) {
	address := "localhost:5000"

	// create an instance of your hub

	// build a signalr.Server using your hub
	// and any server options you may need
	server, _ := signalr.NewServer(context.TODO(),
		signalr.UseHub(hub),
		// signalr.HubFactory(func() signalr.HubInterface {
		// 	return hub
		// }),
		signalr.KeepAliveInterval(2*time.Second))

Increase of goroutines after reconnect

Hi,
I am using WithConnector to reconnect, after reconnection, I have seen how the goroutines increase.

I used with dlv to identify which goroutines increased and I have seen how the goroutine websocket.(*Conn).timeoutLoop increased in every reconnection.

...
  Goroutine 49 - User: /Users/user/.asdf/installs/golang/
1.19.3/packages/pkg/mod/nhooyr.io/[email protected]/conn_notjs.go:153 nhooyr.io/websocket.(*Conn).timeoutLoop (0x100694f54) [select]
  Goroutine 50 - User: /Users/user/.asdf/installs/golang/
1.19.3/go/src/runtime/sigqueue.go:149 os/signal.signal_recv (0x10037896c) (thread 23879806)
  Goroutine 51 - User: /Users/user/.asdf/installs/golang/
1.19.3/packages/pkg/mod/nhooyr.io/[email protected]/conn_notjs.go:153 nhooyr.io/websocket.(*Conn).timeoutLoop (0x100694f54) [select]
  Goroutine 138 - User: /Users/user/.asdf/installs/golang/
1.19.3/packages/pkg/mod/github.com/philippseith/[email protected]/client.go:168 github.com/philippseith/signalr.(*client).Start.func1.1 (0x1006c6648) [chan receive]
  Goroutine 146 - User: /Users/user/.asdf/installs/golang/
1.19.3/packages/pkg/mod/nhooyr.io/[email protected]/conn_notjs.go:153 nhooyr.io/websocket.(*Conn).timeoutLoop (0x100694f54) [select]
  Goroutine 158 - User: /Users/user/.asdf/installs/golang/
1.19.3/packages/pkg/mod/nhooyr.io/[email protected]/conn_notjs.go:153 nhooyr.io/websocket.(*Conn).timeoutLoop (0x100694f54) [select]
...

I am using this code:

	clntCtx, _ := context.WithCancel(context.Background())

	client, err := signalr.NewClient(
		clntCtx, nil,
		signalr.WithReceiver(rcv),
		signalr.WithConnector(func() (signalr.Connection, error) {
			var err error
			var conn signalr.Connection

			for i := 0; i < 10; i++ {
				log.Println("connecting...")

				//nolint
				creationCtx, _ := context.WithTimeout(context.Background(), 10*time.Second)
				conn, err = signalr.NewHTTPConnection(creationCtx, address)
				if err != nil {
					log.Println(err.Error())

					time.Sleep(30 * time.Second)

					continue
				}

				break
			}

			log.Println("connected")

			fmt.Println("current gourtines", runtime.NumGoroutine())

			reInvoke <- 1

			return conn, err
		}),
		signalr.MaximumReceiveMessageSize(maxMessageSize),
		signalr.Logger(kitlog.NewLogfmtLogger(os.Stderr), false))
	if err != nil {
		log.Println(err.Error())
	}

Am I reconnecting in the wrong way?

Thanks in advance for your help.

Client: missing log messages with AutoReconnect option

I'm using the client with logging like this:

client, err := signalr.NewClient(context.Background(),
	signalr.WithAutoReconnect(c.connect(ts)),
	signalr.WithReceiver(c),
	signalr.Logger(easee.SignalrLogger(c.log.TRACE), true),
)

If the connect method errors, hence a connection is never established, the log will not contain any messages.

I would have expected that, at least after the initial connect, the failed reconnect attempts would appear in the logs.

How to integrate with /gorilla/mux ?

Can you give an example of how this can be integrated with the gorilla/mux router ?

Edit: I found a simple solution by simply wrapping handler.

func handleSigR(h http.Handler, wr http.ResponseWriter, req *http.Request) bool {
	url := string(req.URL.Path)
	if strings.HasPrefix(url, sigRRoute) {
		h.ServeHTTP(wr, req)
		return true
	}
	return false
}

func handleRequest(h http.Handler) http.Handler {
	return http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {
		handleCORS(wr, req)
		if req.Method == "OPTIONS" {
			return
		}
		if handleSigR(h, wr, req) {
			return
		}
		reqRouter.ServeHTTP(wr, req)
	})
}


func main()
...
	var services = internal.GetServices()
	reqRouter= internal.SetupRoutes(nil, services)

	sigRouter:= http.NewServeMux()
	setupSigRServer(sigRouter)

	server := &http.Server{Addr: listenAddress, Handler: handleRequest(sigRouter)}
	log.Fatal(server.ListenAndServe())

0.6.0 websocket connection broken with custom http client

In evcc-io/evcc#4246 we've noticed that we can no longer establish a SignalR connection after upgrading to 0.6.0. Git bisect identifies 2c9444d as root cause.

It seems that the websocket connection somehow gets stuck:

[easee ] TRACE 2022/08/28 12:32:49 POST https://api.easee.cloud/hubs/chargers/negotiate
[easee ] TRACE 2022/08/28 12:32:49 {"negotiateVersion":0,"connectionId":"xx","availableTransports":[{"transport":"WebSockets","transferFormats":["Text","Binary"]},{"transport":"ServerSentEvents","transferFormats":["Text"]},{"transport":"LongPolling","transferFormats":["Text","Binary"]}]}

SignalR client

Add a SignalR client. connection and protocol abstractions and implementations from the server can be reused. If possible, make it run with TinyGo

Invalid keyvals structure in custom logger

Sorry for asking here, but I couldn't figure it out. In evcc-io/evcc#4290 I'm running into rare panics with a custom signalr.StructuredLogger. Root cause is that keyvals appear to have non-string type for key. The implementation is quite simple, basically converting keyvals into string:

func (l *logger) Log(keyVals ...interface{}) error {
	b := new(strings.Builder)

	var skip bool
	for i, v := range keyVals {
		if skip {
			skip = false
			continue
		}

		if i%2 == 0 {
			// don't log if key is not a string or if key should be skipped
			if slices.Contains(skipped, v.(string)) {
				skip = true
				continue
			}

			if b.Len() > 0 {
				b.WriteRune(' ')
			}

			b.WriteString(fmt.Sprintf("%v", v))
			b.WriteRune('=')
		} else {
			b.WriteString(fmt.Sprintf("%v", v))
		}
	}

	l.log.Println(b.String())

	return nil
}

What is confusing is the keyvals that triggers the panic:

recovering from panic in logger: expected string, got []interface {} from 
[ts 2022-09-02T07:58:24.050162127Z class Client connection jUObeOndRM7CWMG5x2KtCg hub charger.Easee ts 2022-09-02T07:58:24.050165252Z level info event message send message signalr.hubMessage{Type:6} error *signalr.webSocketConnection: failed to write msg: failed to write frame: WebSocket closed: received close frame: status = StatusNormalClosure and reason = ""]

It seems as if this is really two log messages intermingled in one:

  • notice the duplicate timestamp
  • notice the panic, it seems as if keyvals of one log message is used as key of another one ([]interface {} instead of string)

Any idea where that might be generated on signaler side?

Pushing Stream sends improper StreamInvocation to SignalR server

This is a result of debugging with the SignalR team from here. Per the developers, the PushStream method uses a StreamInvocation, which should only be used when the hub method is returning a stream (i.e. server-to-client). I believe the PushStream method should instead be calling the InvocationMessage method with the proper streamIds, but I have not yet had the time to investigate this fix.

Data races in master

Note this using the client:

WARNING: DATA RACE
Write at 0x00c0000100c0 by goroutine 62:
  github.com/philippseith/signalr.(*webSocketConnection).watchDog()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/websocketconnection.go:99 +0x2b4

Previous read at 0x00c0000100c0 by goroutine 71:
  github.com/philippseith/signalr.(*webSocketConnection).watchDog.func3()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/websocketconnection.go:104 +0x48

Goroutine 62 (running) created at:
  github.com/philippseith/signalr.newWebSocketConnection()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/websocketconnection.go:28 +0x1dc
  github.com/philippseith/signalr.NewHTTPConnection()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/httpconnection.go:122 +0x980
  github.com/evcc-io/evcc/charger.(*Easee).connect.func1()
      /Users/andig/htdocs/evcc/charger/easee.go:153 +0x200
  github.com/philippseith/signalr.(*client).setupConnectionAndProtocol.func1()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/client.go:199 +0xcc
  github.com/philippseith/signalr.(*client).setupConnectionAndProtocol()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/client.go:210 +0x30
  github.com/philippseith/signalr.(*client).run()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/client.go:159 +0x30
  github.com/philippseith/signalr.(*client).Start.func1()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/client.go:128 +0xfc

Goroutine 71 (running) created at:
  github.com/philippseith/signalr.(*webSocketConnection).watchDog()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/websocketconnection.go:101 +0x364
==================
==================
WARNING: DATA RACE
Write at 0x00c0000100c8 by goroutine 62:
  github.com/philippseith/signalr.(*webSocketConnection).watchDog()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/websocketconnection.go:100 +0x30c

Previous read at 0x00c0000100c8 by goroutine 71:
  github.com/philippseith/signalr.(*webSocketConnection).watchDog.func3()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/websocketconnection.go:103 +0x30

Goroutine 62 (running) created at:
  github.com/philippseith/signalr.newWebSocketConnection()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/websocketconnection.go:28 +0x1dc
  github.com/philippseith/signalr.NewHTTPConnection()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/httpconnection.go:122 +0x980
  github.com/evcc-io/evcc/charger.(*Easee).connect.func1()
      /Users/andig/htdocs/evcc/charger/easee.go:153 +0x200
  github.com/philippseith/signalr.(*client).setupConnectionAndProtocol.func1()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/client.go:199 +0xcc
  github.com/philippseith/signalr.(*client).setupConnectionAndProtocol()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/client.go:210 +0x30
  github.com/philippseith/signalr.(*client).run()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/client.go:159 +0x30
  github.com/philippseith/signalr.(*client).Start.func1()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/client.go:128 +0xfc

Goroutine 71 (running) created at:
  github.com/philippseith/signalr.(*webSocketConnection).watchDog()
      /Users/andig/go/pkg/mod/github.com/philippseith/[email protected]/websocketconnection.go:101 +0x364
==================

I've also noticed https://github.com/philippseith/signalr/blob/master/client.go#L123 is not guarded by a lock.

Websocket connection closed with EOF leads to a busy loop

In some cases (reproduced relatively often if refreshing a page from the Firefox browser), the websocket connection can be closed with EOF before a full frame is received on it.
This leads to a busy loop in function readJSONFrames from jsonhubprotocol.go.

I reproduced this with additional logging added as captured in this pull request: https://github.com/sorfks/signalr/pull/1/files
And a log snippet for such a run is attached log-snippet.txt

Lost log data

logger = &recoverLogger{logger: logger}

ts=2021-12-28T10:48:24.4301642Z caller=loop.go:xxx

expect:

ts=2021-12-28T10:48:24.4301642Z caller=loop.go:xxx event write message
    server, _ := signalr.NewServer(context.TODO(),
        signalr.SimpleHubFactory(&hub),
        signalr.Logger(kitlog.NewLogfmtLogger(os.Stdout), false),
    )

CORS problem

Hello, I have encountered the following CORS problem, how can I deal with it? Thank you

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.