njones / socketio Goto Github PK
View Code? Open in Web Editor NEWA Modern SocketIO library for go
License: MIT License
A Modern SocketIO library for go
License: MIT License
As it stands:
ok github.com/njones/socketio/protocol 0.005s coverage: 53.3% of statements
So this can be improved.
Hi,
I noticed that sometimes when my application (which implements the socketio server) is running for some time, I experience a panic which states the following:
panic: send on closed channel
goroutine 6198 [running]:
github.com/njones/socketio/transport.(*Transport).Receive.func1()
.../go/pkg/mod/github.com/njones/[email protected]/transport/transport.go:154 +0x826
created by github.com/njones/socketio/transport.(*Transport).Receive
.../go/pkg/mod/github.com/njones/[email protected]/transport/transport.go:111 +0x76
I use the ServerV4 with PollingTransport.
Is that a known issue?
Even when the application is idle, it happens sometimes and crashes the whole app.
How can I prevent this from happening?
While the SIO server tests will cover most EIO server functionality, we currently have this much covered:
ok github.com/njones/socketio/engineio 0.004s coverage: 2.3% of statements
Uhh, no comment.
Hello github.com/njones/socketio users (and future users),
As my commit history and issue updates indicate, I took an unannounced break from the project over the past year due to burnout. I realize this is a very long time for many of you to wait for answers to questions and to bug fixes. I believe I've found a solution that can prioritize both my well-being and community contributions.
Moving forward, I'm implementing a new work schedule to ensure consistent progress while maintaining balance. I'll be working on the project for two months (starting January-February) followed by a one-month break (March). This pattern will continue throughout the year, providing me with the necessary breathers to keep up with everything and deliver my best work. I'll re-evaluate this plan Jan 2025 to see if it makes sense moving forward.
I understand that some questions, bug reports, and proposals have been waiting for answers for a while. I'm eager to address them as soon as possible.
I want to expressing my sincere gratitude for those that have continued passion and support for this project even through the break. Thank you for your patience and understanding. Let's make 2024 a fantastic year!
Nika
As it stands we currently have this much coverage:
ok github.com/njones/socketio/engineio/protocol 0.005s coverage: 56.8% of statements
Well, this can be increased.
Author(s): Nika Jones (@njones)
Last updated: 2022-Aug-27
The SocketIO library should have a client developed alongside the server. This will allow backend services developed in Go the ability to call any SocketIO server implementation.
Although the general SocketIO client use case is from a browser, which can't natively run Go. However there are lots of situations where a backend service could use a client to talk to a SocketIO server.
Examples:
Create a Client object. (The zero defaults should be valid)
client = NewClient(options...) <*Client>
or
client = &Client{}
The simplest Client is one that can have .On and .Emit with a Dialer that can have any transport necessary.
OnConnect
, OnDisconnect
, On
and Emit
handlersTransport
or Dialer
worksLatest version at the main branch (12ccfd1) doesn't allow clients to connect to multiple namespaces.
To reproduce:
Client (Python):
requirements.txt
python-socketio
python-socketio[client]==4.6.1
main.py
import socketio
NAMESPACES = ["/channel"]
URL = "http://127.0.0.1:8003/"
sio = socketio.Client(logger=True, engineio_logger=True)
@sio.event
def connect():
print('connection established')
@sio.event
def connect_error(data):
print("The connection failed!")
@sio.event
def disconnect():
print('disconnected from server')
sio.connect(URL,
transports="websocket",
namespaces=NAMESPACES)
sio.wait()
Server:
main.go
package main
import (
"log"
"net/http"
"time"
sio "github.com/njones/socketio"
eio "github.com/njones/socketio/engineio"
eiot "github.com/njones/socketio/engineio/transport"
)
func main() {
port := "localhost:8003"
server := sio.NewServerV2(
eio.WithSessionShave(1*time.Millisecond),
eio.WithPingInterval(5*time.Second),
eio.WithPingTimeout(1*time.Minute),
eio.WithMaxPayload(1000000),
eio.WithTransportOption(eiot.WithGovernor(1500*time.Microsecond, 500*time.Microsecond)),
)
// Root namespace
server.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/ %s connected.", socket.ID().String())
return nil
})
// Channel namespace
channel := server.Of("/channel")
channel.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/channel %s connected.", socket.ID().String())
return nil
})
log.Printf("Serving port %s...\n", port)
log.Fatal(http.ListenAndServe(port, server))
}
If you use v0.1.1 -- go get github.com/njones/[email protected]
, it works, it connects to "/" and "/channel".
But if you use the latest version -- go get github.com/njones/socketio@main
, it doesn't work, it just connects to "/".
Let me know if you need anything else. Keep up the great work!
Hi! I'm currently trying to swap out go-socket.io with your library (because of the v4 compatibility), but I have problems due to the private structs and private callback typings.
When I want to create an interface for a Namespace / sio.inSocketV4 that's impossible, because I cannot make an On
Method definition conforming to the private eventCallback
interface definition, even if it matches the signature.
So I would propose to either expose interfaces (probably eventCallback
would be good enough) to public or to not return private structs which can't be even embedded in self-defined data types.
But maybe there is already a solution to this which I didn't see yet.
how to close the connection in server side?
When defining a specific path on the server and the client, the connection is not established by the client.
Node client:
const io = require("socket.io-client");
const socket = io("http://localhost:8003/channel", {
transports: ["websocket"],
reconnection: true,
autoConnect: true,
pingInterval: 8000,
pingTimeout: 10000,
connectTimeout: 10000,
query: {
token: "token!",
},
path: "/testing/",
});
socket.on("connect", () => {
console.log("Connected.");
});
socket.on("message", (msg) => {
console.log("Message: ", msg)
})
socket.on("error", (error) => {
console.log("Error: ", error);
});
socket.on("disconnect", (error) => {
console.log("Disconnected: ", error);
});
socket.on("connect_error", (error) => {
console.log("connect_error: ", error);
});
socket.connect();
Go server:
import (
"log"
"net/http"
"time"
sio "github.com/njones/socketio"
eio "github.com/njones/socketio/engineio"
eiot "github.com/njones/socketio/engineio/transport"
)
func main() {
port := "localhost:8003"
server := sio.NewServerV2(
eio.WithPath("/testing/"),
eio.WithSessionShave(1*time.Millisecond),
eio.WithPingInterval(5*time.Second),
eio.WithPingTimeout(1*time.Minute),
eio.WithMaxPayload(1000000),
eio.WithTransportOption(eiot.WithGovernor(1500*time.Microsecond, 500*time.Microsecond)),
)
//////////////////////////////////////////////////
// ROOT
server.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/ %s connected.", socket.ID().String())
log.Printf("/ token: %s", socket.Request().URL.Query().Get("token"))
return nil
})
//////////////////////////////////////////////////
// CHANNEL
channel := server.Of("/channel")
channel.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/channel %s connected.", socket.ID().String())
log.Printf("/channel token: %s", socket.Request().URL.Query().Get("token"))
return nil
})
//////////////////////////////////////////////////
log.Printf("Serving port %s...\n", port)
log.Fatal(http.ListenAndServe(port, server))
}
Hello this library is really useful as it's the only library that is currently implementing latest from Socket.IO
I just would like to ask how can I access the SocketV4 object like I need the ID for example to check if that ID is part of a room, how do I get that information from an event trigger like so:
serverV4.Of("/myNamespace").On("myEvent", callback.FuncString(func(i string) {
// access socketID of who triggered the event
})
Is there any way to do this?
v0.1.1 doesn't allow to connect to a specific namespace while using socket.io JS library.
npm install [email protected]
const io = require("socket.io-client");
const socket = io("http://localhost:8003/channel", {
transports: ["websocket"],
reconnection: true,
autoConnect: true,
pingInterval: 8000,
pingTimeout: 10000,
connectTimeout: 10000,
});
socket.on("connect", () => {
console.log("Connected.");
});
socket.on("message", (msg) => {
console.log("Message: ", msg)
})
socket.on("error", (error) => {
console.log("Error: ", error);
});
socket.on("disconnect", (error) => {
console.log("Disconnected: ", error);
});
socket.on("connect_error", (error) => {
console.log("connect_error: ", error);
});
socket.connect();
node test.js
package main
import (
"log"
"net/http"
"time"
sio "github.com/njones/socketio"
eio "github.com/njones/socketio/engineio"
eiot "github.com/njones/socketio/engineio/transport"
)
func main() {
port := "localhost:8003"
server := sio.NewServerV2(
eio.WithSessionShave(1*time.Millisecond),
eio.WithPingInterval(5*time.Second),
eio.WithPingTimeout(1*time.Minute),
eio.WithMaxPayload(1000000),
eio.WithTransportOption(eiot.WithGovernor(1500*time.Microsecond, 500*time.Microsecond)),
)
// Root namespace
server.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/ %s connected.", socket.ID().String())
return nil
})
// Channel namespace
channel := server.Of("/channel")
channel.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/channel %s connected.", socket.ID().String())
return nil
})
log.Printf("Serving port %s...\n", port)
log.Fatal(http.ListenAndServe(port, server))
}
Hello! I have been trying to use this library but I have run into issues with using it in a real world example. It would be really helpful to see a client example similar to this https://github.com/googollee/go-socket.io/tree/master/_examples. This would help to show how this could be validated and provide context for the server example provided in the README.
The card prefixing used when printing a socket ID can be quite distracting in logging. I propose that the prefix value (or possibly a function) could be passed to the NewServer function on instantiation.
Upon exploring the library, I noticed that the OnDisconnect event does not trigger when closing the browser tab of an active socket connection.
However calling socket.disconnect()
on client side triggers the OnDisconnect event.
Hello. Thanks for the library
I can't sort out the callback for On (subscription events). In server. I received empty map[string]interface.
Can you help me this problem, maybe. thanks
I sending test message from Postman as JSON. Setting v4 or v3
{
"key": "value"
}
server.On("event", callback.Wrap{
Parameters: []ser.Serializable{ser.MapParam},
Func: func() interface{} {
return func(msg map[string]interface{}) error {
fmt.Println(msg)
return nil
}
},
})
I getting empty map. map[]
if I set StrParam
server.On("event", callback.Wrap{
Parameters: []ser.Serializable{ser. StrParam},
Func: func() interface{} {
return func(msg string) error {
fmt.Println(msg)
return nil
}
},
})
then I getting string "map[key:value]"
What I'm trying to achieve:
I'm setting up a socketio server in Go with this package, and using multiple .net clients to connect and communicate with each other. The .net client is not fully developed yet, so I'm using Postman v10.17 to test the connection with my server (which is running locally at the moment).
For my use-case, it's important that some events coming from one side are acknowledged by the other side, with additional arguments. To make it a bit less abstract, an example would be that the client emits an event with one or more arguments, the server processes these arguments, and returns some other arguments in an acknowledgement.
The issue
I'm just starting to learn about socketio so I'm not sure if this is the correct way to do this. The code below is how I get to return an acknowledgement from the Go server to Postman:
socket.On("test", callback.FuncAnyAck(func(i ...interface{}) []ser.Serializable {
fmt.Printf("\nnumber of input arguments: %d", len(i))
output := []ser.Serializable{}
returnArgmunents := 2
for i := 0; i < returnArgmunents; i++ {
output = append(output, ser.String(fmt.Sprintf("return argument %d", i)))
}
return output
}))
The code above does the behavior that I expect when the number of input arguments is equal to the number of output arguments.
However, when I have more input arguments than output arguments, let's say three input vs. two output arguments, there are three arguments being returned instead of two, the last one being null
.
And when I have less input arguments than output arguments, the program panics on the CallbackAck
function in callback/callback.go:
runtime/debug.Stack()
/usr/local/go/src/runtime/debug/stack.go:24 +0x7a
panic({0x111a2e0, 0xc000320048})
/usr/local/go/src/runtime/panic.go:890 +0x267
github.com/njones/socketio/callback.FuncAnyAck.CallbackAck(0x11d6b30, {0xc00034c090, 0x1, 0x3})
/go/pkg/mod/github.com/njones/[email protected]/callback/callback.go:33 +0x2b5
When I change callback/callback.go line 30 from
out := make([]interface{}, len(v))
to
out := make([]interface{}, len(slice))
it does work as expected. However I'm not sure if this is the correct way to go, or that I'm going to mess it up any further.
I've searched to find if the number of input and output arguments in an acknowledgement should be equal, but I couldn't find anything about it.
Am I on the right path here regarding ackowledgements? And if so, is the behavior described above correct or am I missing something?
Thanks in advance!
Hi Nika!
So, I'm running some load tests here and noticed the server is not releasing allocated memory.
Here is what I found:
After running the load test, I got around 5Gib of allocated memory that is not released even after GC execution.
To reproduce the test:
go run main.go
(code below);k6 run loadTest.js
(code below);pprof
:$ go tool pprof http://localhost:8003/debug/pprof/heap
main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"runtime"
"strings"
"time"
sio "github.com/njones/socketio"
"github.com/njones/socketio/callback"
eio "github.com/njones/socketio/engineio"
ser "github.com/njones/socketio/serialize"
)
const (
httpServerPort = ":8003"
)
func main() {
var ruTimerQuitChan chan struct{}
sioServer := sio.NewServerV2(
sio.WithPath("/websocket/api/socket.io/"),
eio.WithCors(eio.CORSorigin{"*"}),
eio.WithSessionShave(1*time.Millisecond),
eio.WithPingInterval(5*time.Second),
eio.WithPingTimeout(1*time.Minute),
eio.WithMaxPayload(1000000),
)
// /
sioServer.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/ connected: %s", socket.ID().String())
log.Printf("/ token: %s", socket.Request().URL.Query().Get("token"))
return nil
})
// CHANNEL
channel := sioServer.Of("/channel")
channel.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/channel connected: %s", socket.ID().String())
log.Printf("/channel token: %s", socket.Request().URL.Query().Get("token"))
return nil
})
// CHAT
chat := sioServer.Of("/chat")
chat.OnConnect(func(socket *sio.SocketV2) error {
log.Printf("/chat connected: %s", socket.ID().String())
log.Printf("/chat token: %s", socket.Request().URL.Query().Get("token"))
socket.On("join", callback.Wrap{
Parameters: []ser.Serializable{ser.StrParam},
Func: func() interface{} {
return func(room string) error {
log.Print("/chat join event")
return nil
}
},
})
socket.On("leave", callback.Wrap{
Parameters: []ser.Serializable{ser.StrParam},
Func: func() interface{} {
return func(room string) error {
log.Print("/chat leave event")
return nil
}
},
})
return nil
})
// Debug
ruTicker := time.NewTicker(time.Second * 10)
ruTimerQuitChan = make(chan struct{})
go func() {
for {
select {
case <-ruTicker.C:
printMemStats()
case <-ruTimerQuitChan:
ruTicker.Stop()
return
}
}
}()
defaultMux := http.NewServeMux()
// Socket.io setup
defaultMux.Handle("/", sioServer)
// Pprof setup
defaultMux.HandleFunc("/debug/pprof/", pprof.Index)
sioHTTPServer := &http.Server{
Addr: httpServerPort,
Handler: defaultMux,
ReadHeaderTimeout: 2 * time.Second,
}
// Start server
log.Printf("Server running at %s.", httpServerPort)
go func() {
if err := sioHTTPServer.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
<-stop
close(ruTimerQuitChan)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := sioHTTPServer.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}
func printMemStats() {
var stats strings.Builder
var m runtime.MemStats
runtime.ReadMemStats(&m)
stats.WriteString(fmt.Sprintf("Alloc=%vMiB; TotalAlloc=%v MiB; Sys=%vMiB; GoRoutines=%d; NumGC=%v",
bToMb(m.Alloc),
bToMb(m.TotalAlloc),
bToMb(m.Sys),
runtime.NumGoroutine(),
m.NumGC))
log.Print(stats.String())
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}
loadTest.js
import { check, group, sleep } from 'k6';
import ws from 'k6/ws';
export const options = {
thresholds: {
http_req_failed: ['rate<0.50'],
http_req_duration: ['p(90)<5000'],
},
stages: [
{ duration: '2m', target: 100 },
{ duration: '4m', target: 100 },
{ duration: '2m', target: 0 },
],
};
const nameSpaces = ['', 'channel', 'chat'];
const token = '01234567890123456789012345';
const url = 'ws://localhost:8003/websocket/api/socket.io/?token=' + token + '&EIO=3&transport=websocket';
export default function () {
nameSpaces.forEach((nameSpace) => {
group("Test '" + nameSpace + "'", function () {
let response = ws.connect(url, {}, function (socket) {
socket.on('open', function open() {
if (nameSpace.length > 0) {
socket.send('40/' + nameSpace);
}
if (nameSpace === 'chat') {
sleep(1);
socket.send('42/chat,["join","room-id"]');
sleep(1);
socket.send('42/chat,["leave","room-id"]');
}
sleep(1);
socket.close();
});
});
check(response, {
'status is 101': (r) => r && r.status === 101,
});
});
});
}
Hello, I'm trying to use this package for different namespaces
Here's the working version using github.com/googollee/go-socket.io
func main() {
server := socketio.NewServer(&engineio.Options{
Transports: []transport.Transport{
&polling.Transport{
CheckOrigin: allowOriginFunc,
},
&websocket.Transport{
CheckOrigin: allowOriginFunc,
},
},
})
server.OnConnect("/", func(s socketio.Conn) error {
s.SetContext("")
log.Println("connected:", s.ID())
return nil
})
server.OnEvent("/", "notice", func(s socketio.Conn, msg string) {
log.Println("notice:", msg)
s.Emit("reply", "have "+msg)
})
server.OnEvent("/chat", "msg", func(s socketio.Conn, msg string) string {
log.Println("chat:", msg)
s.SetContext(msg)
return "recv " + msg
})
server.OnEvent("/", "bye", func(s socketio.Conn) string {
last := s.Context().(string)
s.Emit("bye", last)
s.Close()
return last
})
server.OnError("/", func(s socketio.Conn, e error) {
log.Println("meet error:", e)
})
server.OnDisconnect("/", func(s socketio.Conn, reason string) {
log.Println("closed", reason)
})
go func() {
if err := server.Serve(); err != nil {
log.Fatalf("socketio listen error: %s\n", err)
}
}()
defer server.Close()
http.Handle("/socket.io/", server)
http.Handle("/", http.FileServer(http.Dir("asset")))
log.Println("Serving at http://localhost:8001...")
log.Fatal(http.ListenAndServe(":8001", nil))
}
When I try to use this package I'm getting error. Not sure what might be the issue
func main() {
port := ":8001"
server := sio.NewServer(
eio.WithPingInterval(300*1*time.Millisecond),
eio.WithPingTimeout(200*1*time.Millisecond),
eio.WithMaxPayload(1000000),
eio.WithTransportOption(eiot.WithGovernor(1500*time.Microsecond, 500*time.Microsecond)),
)
server.OnConnect(func(s *sio.SocketV4) error {
log.Println("connected:", s.ID())
s.Of("/").On("notice", CustomWrap(func(a string) error {
return s.Emit("reply", seri.String("have "+a))
}))
s.Of("/").On("bye", CustomWrap(func(a string) error {
return s.Emit("bye", seri.String(a))
}))
s.Of("/chat").On("msg", CustomWrap(func(a string) error {
fmt.Println("msg", a)
return nil
}))
return nil
})
http.Handle("/socket.io/", server)
http.Handle("/", http.FileServer(http.Dir("asset")))
log.Printf("serving port %s...\n", port)
log.Fatal(http.ListenAndServe(port, nil))
}
// Define a custom wrapper
type CustomWrap func(string) error
// Define your callback
func (cc CustomWrap) Callback(data ...interface{}) error {
a, aOK := data[0].(string)
if !aOK {
return fmt.Errorf("bad parameters")
}
return cc(a)
}
Error:
44{"message":"expected an []interface{} or []string, found []interface {}\t{"do":"eventPacket"}"}
index.html
<!doctype html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
var socket = io("http://127.0.0.1:8001", {transports: ["websocket"]});
socket.on('reply', function(msg){
$('#messages').append($('<li>').text(msg));
});
$('form').submit(function(){
socket.emit('msg', $('#m').val(), function(data){
$('#messages').append($('<li>').text('ACK CALLBACK: ' + data));
});
socket.emit('notice', $('#m').val());
$('#m').val('');
return false;
});
</script>
</body>
</html>
When I use the OnDisconnect function ,I found it do not work when clients dissconneted
As it stands for the SIO servers we have this much coverage:
ok github.com/njones/socketio 0.007s coverage: 18.9% of statements
We can do better.
Add some tests to flesh out testing the map SIO transport.
This project looks really cool!
But the usage is not clear. The readme example does not compile. Even after adjusting it I could not get the server to run.
Currently the SocketID has a generated prefix from one source. In order to allow custom prefixes the ID should contain two parts the ID and the Prefix. This way there can't be custom generators that can generate fake-able ID's in the system. The two parts could look like something as simple as:
type ID struct { prefix, id string }
This needs to be threaded throughout the system, and must convert to a String() for logging and debugging.
As it stands we currently have this much coverage:
ok github.com/njones/socketio/engineio/transport 0.033s coverage: 46.0% of statements
More can be added.
Hi,
It seems like concurrent handling of events on the same socket is not working.
If a specific client sends multiple events to the server, events are only handled if the previous event is completely finished.
This is not expected behavior and my implementation is not very complex.
Is this a bug? Or could it be that my own code is the culprit.
Thanks
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.