Giter Club home page Giter Club logo

sseserver's Introduction

sseserver ๐Ÿ„โ€โ™‚๏ธ

PkgGoDev Build Status CodeFactor Go Report Card

A high performance and thread-safe Server-Sent Events server for Go with hierarchical namespacing support.

This library has powered the streaming API endpoint for :dizzy: Emojitracker in production since 2014, where it routinely handles dispatching hundreds of messages per second to thousands of simultaneous clients, on a single Heroku dyno.

Introduction

Hierarchical Namespaced Channels*

A client can subscribe to channels reflecting the content they are interested in. For example, say you are broadcasting events to the namespaces /pets/cats and /pets/dogs. A client could subscribe to the parent channel /pets in the hierarchy and receive all messages for either.

In sseserver, channels have infinite depth and are automatically created on the fly with zero setup -- just broadcast and it works.

(*There's probably a more accurate term for this. If you know it, let me know.)

Performance

Designed for high throughput as primary performance consideration. In my preliminary benchmarking (on v1.0, circa 2014) this can handle ~100K/sec messages broadcast across ~1000 open HTTP connections on a 3.4GHz Intel Core i7 (using a single core, e.g. with GOMAXPROCS=1). There still remains quite a bit of optimization to be done so it should be able to get faster if needed.

SSE vs Websockets

SSE is the oft-overlooked uni-directional cousin of websockets. Being "just HTTP" it has some benefits:

  • Trvially easier to understand, plaintext format. Can be debugged by a human with curl.
  • Supported in most major browsers for a long time now. Everything except IE/Edge, but an easy polyfill!
  • Built-in standard support for automatic reconnection, event binding, etc.
  • Works with HTTP/2.

See also Smashing Magazine: "Using SSE Instead Of WebSockets For Unidirectional Data Flow Over HTTP/2".

Documentation

GoDoc

Namespaces are URLs

For clients, no need to think about protocol. To subscribe to one of the above namespaces from the previous example, just connect to http://$SERVER/pets/dogs. Done.

Example Usage

A simple Go program utilizing this package:

package main

import (
    "time"
    "github.com/mroth/sseserver"
)

func main() {
    s := sseserver.NewServer() // create a new server instance

    // broadcast the time every second to the "/time" namespace
    go func() {
        ticker := time.Tick(time.Duration(1 * time.Second))
        for {
            // wait for the ticker to fire
            t := <-ticker
            // create the message payload, can be any []byte value
            data := []byte(t.Format("3:04:05 pm (MST)"))
            // send a message without an event on the "/time" namespace
            s.Broadcast <- sseserver.SSEMessage{"", data, "/time"}
        }
    }()

    // simulate sending some scoped events on the "/pets" namespace
    go func() {
        time.Sleep(5 * time.Second)
        s.Broadcast <- sseserver.SSEMessage{"new-dog", []byte("Corgi"), "/pets/dogs"}
        s.Broadcast <- sseserver.SSEMessage{"new-cat", []byte("Persian"), "/pets/cats"}
        time.Sleep(1 * time.Second)
        s.Broadcast <- sseserver.SSEMessage{"new-dog", []byte("Terrier"), "/pets/dogs"}
        s.Broadcast <- sseserver.SSEMessage{"new-dog", []byte("Dauchsand"), "/pets/cats"}
        time.Sleep(2 * time.Second)
        s.Broadcast <- sseserver.SSEMessage{"new-cat", []byte("LOLcat"), "/pets/cats"}
    }()

    s.Serve(":8001") // bind to port and begin serving connections
}

All these event namespaces are exposed via HTTP endpoint in the /subscribe/:namespace route.

On the client, we can easily connect to those endpoints using built-in functions in JS:

// connect to an event source endpoint and print results
var es1 = new EventSource("http://localhost:8001/subscribe/time");
es1.onmessage = function(event) {
    console.log("TICK! The time is currently: " + event.data);
};

// connect to a different event source endpoint and register event handlers
// note that by subscribing to the "parent" namespace, we get all events
// contained within the pets hierarchy.
var es2 = new EventSource("http://localhost:8001/subscribe/pets")
es2.addEventListener("new-dog", function(event) {
    console.log("WOOF! Hello " + event.data);
}, false);

es2.addEventListener("new-cat", function(event) {
    console.log("MEOW! Hello " + event.data);
}, false);

Which when connecting to the server would yield results:

TICK! The time is currently: 6:07:17 pm (EDT)
TICK! The time is currently: 6:07:18 pm (EDT)
TICK! The time is currently: 6:07:19 pm (EDT)
TICK! The time is currently: 6:07:20 pm (EDT)
WOOF! Hello Corgi
MEOW! Hello Persian
TICK! The time is currently: 6:07:21 pm (EDT)
WOOF! Hello Terrier
WOOF! Hello Dauchsand
TICK! The time is currently: 6:07:22 pm (EDT)
TICK! The time is currently: 6:07:23 pm (EDT)
MEOW! Hello LOLcat
TICK! The time is currently: 6:07:24 pm (EDT)

Of course you could easily send JSON objects in the data payload instead, and most likely will be doing this often.

Another advantage of the SSE protocol is that the wire-format is so simple. Unlike WebSockets, we can connect with curl to an endpoint directly and just read what's going on:

$ curl http://localhost:8001/subscribe/pets
event:new-dog
data:Corgi

event:new-cat
data:Persian

event:new-dog
data:Terrier

event:new-dog
data:Dauchsand

event:new-cat
data:LOLcat

Yep, it's that simple.

Keep-Alives

All connections will send periodic :keepalive messages as recommended in the WHATWG spec (by default, every 15 seconds). Any library adhering to the EventSource standard should already automatically ignore and filter out these messages for you.

Admin Page

By default, an admin status page is available for easy monitoring.

screenshot

It's powered by a simple JSON API endpoint, which you can also use to build your own reporting. These endpoints can be disabled in the settings (see Server.Options).

HTTP Middleware

sseserver.Server implements the standard Go http.Handler interface, so you can easily integrate it with most existing HTTP middleware, and easily plug it into whatever you are using for your current routing, etc.

License

AGPL-3.0. Dual commercial licensing available upon request.

sseserver's People

Contributors

dependabot[bot] avatar jobin212 avatar mroth 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  avatar  avatar  avatar  avatar

sseserver's Issues

Https support?

I'm running my application with https, with ListenAndServeTLS().

server.ListenAndServeTLS(certsDirectory+"/cert.pem", certsDirectory+"/key.pem")

Any calls that go through to http are redirected through to https with:

func redirectHttpToHttps() {
	_ = http.ListenAndServe(":http", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently)
	}))
}

As a result I can't use http in the browser:

image

Can I use this with my https server somehow?

Provide stats per subscription channel

I'd love for a way to create an API endpoint when I can report how many active connections there are per listening subscirption, eg pets/cat and pets/dog separate.

I am running my owner server that in summary operates as per the code below. However, despite what the docs says, queries to /admin/ endpoints all return a 404 error:

func main() {
	fmt.Println("Starting pusher server")
	s := sseserver.NewServer()
	mux := http.NewServeMux()

	// Function called from pusher realtime microservice to update live mints
	mux.HandleFunc("/updatelive", func(w http.ResponseWriter, r *http.Request) {
        ...
        }

	mux.Handle("/subscribe/", s)
	http.ListenAndServe(":8110", mux)
}

add LICENSE information

Realized I forgot to do this years ago, even though the intention was for the project to be freely licensed. So might as well use this as an opp. to research and really pick the best license here.

Upstream go.zipexe dependency issue causes panic in Docker container

I've spent quite a bit of time last month trying to containerize my Go project that is using SSE server. Nothing I have tried works, leading me to believe SSE server must be partially implemented in C, and that C is throwing an exception up to Go (this is what the logs state).

I can build the image without errors, but can't get it to run within a container either on Docker or Kubernetes.

These are my current imports:

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"

	"github.com/mroth/sseserver"
)

This is my Dockerfile:

# syntax=docker/dockerfile:1
FROM golang:1.19

RUN apt-get update && apt-get install -y git

# Set destination for COPY
WORKDIR /app

# Copy the source code. Note the slash at the end, as explained in
COPY *.go ./

COPY go.sum go.mod ./

RUN go mod download

# Run
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /pusher-server .
# RUN go build -o /pusher-server

EXPOSE 8110

# Run
CMD ["/pusher-server"]

And this is the error I get running it in a Docker Container. It works just fine with 'go run pusher-server.go':

 โ ฟ Container pusher-server-pusher-server-1  Created                                                                                                                                              0.0s
Attaching to pusher-server-pusher-server-1
pusher-server-pusher-server-1  | panic: runtime error: invalid memory address or nil pointer dereference
pusher-server-pusher-server-1  | [signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x64d2e6]
pusher-server-pusher-server-1  | 
pusher-server-pusher-server-1  | goroutine 1 [running]:
pusher-server-pusher-server-1  | debug/elf.(*Section).ReadAt(0xc000124000?, {0xc000132000?, 0x1c8?, 0x17?}, 0x40?)
pusher-server-pusher-server-1  |        <autogenerated>:1 +0x26
pusher-server-pusher-server-1  | archive/zip.readDirectoryEnd({0x76bde0, 0xc0000ca700}, 0x210)
pusher-server-pusher-server-1  |        /usr/local/go/src/archive/zip/reader.go:526 +0xf5
pusher-server-pusher-server-1  | archive/zip.(*Reader).init(0xc0000b6540, {0x76bde0?, 0xc0000ca700}, 0x210)
pusher-server-pusher-server-1  |        /usr/local/go/src/archive/zip/reader.go:97 +0x5c
pusher-server-pusher-server-1  | archive/zip.NewReader({0x76bde0, 0xc0000ca700}, 0x210)
pusher-server-pusher-server-1  |        /usr/local/go/src/archive/zip/reader.go:90 +0x5e
pusher-server-pusher-server-1  | github.com/daaku/go%2ezipexe.zipExeReaderElf({0x76c480?, 0xc000012048}, 0x752c87)
pusher-server-pusher-server-1  |        /go/pkg/mod/github.com/daaku/[email protected]/zipexe.go:128 +0x8b
pusher-server-pusher-server-1  | github.com/daaku/go%2ezipexe.NewReader({0x76c480, 0xc000012048}, 0x0?)
pusher-server-pusher-server-1  |        /go/pkg/mod/github.com/daaku/[email protected]/zipexe.go:48 +0x98
pusher-server-pusher-server-1  | github.com/daaku/go%2ezipexe.OpenCloser({0xc0000182e0?, 0xc000087be0?})
pusher-server-pusher-server-1  |        /go/pkg/mod/github.com/daaku/[email protected]/zipexe.go:30 +0x57
pusher-server-pusher-server-1  | github.com/GeertJohan/go%2erice.init.0()
pusher-server-pusher-server-1  |        /go/pkg/mod/github.com/!geert!johan/[email protected]/appended.go:42 +0x65
pusher-server-pusher-server-1 exited with code 2

Unable to run in a docker container

I've been trying to get a simple web server script deployed into a docker container. The script called server.go will work fine locally and hosts the SSE server as expected.

The same script compiled into a docker container throws the following error. This is the same on my Mac or trying to run it on a Linux container in the cloud. No matter what I have tried I get the same error. Not even the very first fmt.Println is executed:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x650e86]

goroutine 1 [running]:
debug/elf.(*Section).ReadAt(0xc000200000?, {0xc0001b2480?, 0x270?, 0x24?}, 0x40?)
:1 +0x26
archive/zip.readDirectoryEnd({0x76ff40, 0xc00019ad00}, 0x210)
/usr/local/go/src/archive/zip/reader.go:526 +0xf5
archive/zip.(*Reader).init(0xc000186a10, {0x76ff40?, 0xc00019ad00}, 0x210)
/usr/local/go/src/archive/zip/reader.go:97 +0x5c
archive/zip.NewReader({0x76ff40, 0xc00019ad00}, 0x210)
/usr/local/go/src/archive/zip/reader.go:90 +0x5e

Query on "Performance" information from README

You mentioned:

In my preliminary benchmarking (on v1.0, circa 2014) this can handle ~100K/sec messages broadcast across ~1000 open HTTP connections on a 3.4GHz Intel Core i7 (using a single core, e.g. with GOMAXPROCS=1)

You didn't mention RAM used?

  • Do you have a performance benchmark against "WebSocket"? for the same level of usage?

Catchup behaviour after sleeping

Sorry for raising this as an issue, I can see no other mechanism of asking questions on this project.

I've discovered that if I shut my laptop with a site subscribed to events, then re-open it 8 hours later the front-end seems to receive all the notifications that would have been missed for those 8 hours. My site is streaming realtime events so the user doesn't want to see events from 8 hours ago, they want to see only the most recent ones.

Is there a way to configure this behaviour? Many thanks

consider renaming the project

Reasons:

  • sseserver is very generic, and doesn't imply any of the nice functionality (namespacing support and fast performance) in this library.
  • sseserver can be confusing to spell, due to the overlapping collision (s[se)rver].
  • sseserver.Server stutters, a no-no in Go best practices.

I am actively interested in improving this code again (and making some decent strides in the develop branch which will eventually be cleaned up and squashed towards a v1.1), and with the proliferation of HTTP/2, SSE may see a resurgence of interest (hence my work on cleanups and the API currently). So if I'm going to rename the project, it should probably be done sooner rather than later, since that's easier when not many people are using it.

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.