diff --git a/HISTORY.md b/HISTORY.md index e5a814da..98962275 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -23,6 +23,8 @@ to adapt the new changes to your application, it contains an overview of the new - Developers can use a `yaml` files for the configuration using the `iris.YAML` function: `app := iris.New(iris.YAML("myconfiguration.yaml"))` - Add `.Regex` middleware which does path validation using the `regexp` package, i.e `.Regex("param", "[0-9]+$")`. Useful for routers that don't support regex route path validation out-of-the-box. +- Websocket additions: `c.Context() *iris.Context`, `ws.GetConnectionsByRoom("room name") []websocket.Connection`, `c.OnLeave(func(roomName string){})`, `c.Values().Set(key,value)/.Get(key).Reset()` (where ws:websocket.Server insance, where c:websocket.Connection instance) + Fixes: - Websocket improvements and fix errors when using custom golang client diff --git a/adaptors/websocket/client.go b/adaptors/websocket/client.go index 08bf8f2b..73e38b20 100644 --- a/adaptors/websocket/client.go +++ b/adaptors/websocket/client.go @@ -93,7 +93,7 @@ var Ws = (function () { m = JSON.stringify(data); } else { - console.log("Invalid"); + console.log("Invalid, javascript-side should contains an empty second parameter."); } return this._msg(event, t, m); }; diff --git a/adaptors/websocket/client.ts b/adaptors/websocket/client.ts index 865808af..f2cbf059 100644 --- a/adaptors/websocket/client.ts +++ b/adaptors/websocket/client.ts @@ -113,7 +113,7 @@ class Ws { t = websocketJSONMessageType; m = JSON.stringify(data); } else { - console.log("Invalid"); + console.log("Invalid, javascript-side should contains an empty second parameter."); } return this._msg(event, t, m); diff --git a/adaptors/websocket/connection.go b/adaptors/websocket/connection.go index 1434211f..110b2965 100644 --- a/adaptors/websocket/connection.go +++ b/adaptors/websocket/connection.go @@ -12,6 +12,61 @@ import ( "gopkg.in/kataras/iris.v6" ) +type ( + connectionValue struct { + key []byte + value interface{} + } + // ConnectionValues is the temporary connection's memory store + ConnectionValues []connectionValue +) + +// Set sets a value based on the key +func (r *ConnectionValues) Set(key string, value interface{}) { + args := *r + n := len(args) + for i := 0; i < n; i++ { + kv := &args[i] + if string(kv.key) == key { + kv.value = value + return + } + } + + c := cap(args) + if c > n { + args = args[:n+1] + kv := &args[n] + kv.key = append(kv.key[:0], key...) + kv.value = value + *r = args + return + } + + kv := connectionValue{} + kv.key = append(kv.key[:0], key...) + kv.value = value + *r = append(args, kv) +} + +// Get returns a value based on its key +func (r *ConnectionValues) Get(key string) interface{} { + args := *r + n := len(args) + for i := 0; i < n; i++ { + kv := &args[i] + if string(kv.key) == key { + return kv.value + } + } + return nil +} + +// Reset clears the values +func (r *ConnectionValues) Reset() { + *r = (*r)[:0] +} + // UnderlineConnection is used for compatible with fasthttp and net/http underline websocket libraries // we only need ~8 funcs from websocket.Conn so: type UnderlineConnection interface { @@ -65,6 +120,10 @@ type UnderlineConnection interface { type ( // DisconnectFunc is the callback which fires when a client/connection closed DisconnectFunc func() + // LeaveRoomFunc is the callback which fires when a client/connection leaves from any room. + // This is called automatically when client/connection disconnected + // (because websocket server automatically leaves from all joined rooms) + LeaveRoomFunc func(roomName string) // ErrorFunc is the callback which fires when an error happens ErrorFunc (func(string)) // NativeMessageFunc is the callback for native websocket messages, receives one []byte parameter which is the raw client's message @@ -84,6 +143,8 @@ type ( // websocket has everything you need to authenticate the user BUT if it's necessary // then you use it to receive user information, for example: from headers Context() *iris.Context + // Values returns the temporary lock-free connection's data store + Values() ConnectionValues // OnDisconnect registers a callback which fires when this connection is closed by an error or manual OnDisconnect(DisconnectFunc) @@ -103,7 +164,15 @@ type ( // Join join a connection to a room, it doesn't check if connection is already there, so care Join(string) // Leave removes a connection from a room - Leave(string) + // Returns true if the connection has actually left from the particular room. + Leave(string) bool + // OnLeave registeres a callback which fires when this connection left from any joined room. + // This callback is called automatically on Disconnected client, because websocket server automatically + // deletes the disconnected connection from any joined rooms. + // + // Note: the callback(s) called right before the server deletes the connection from the room + // so the connection theoritical can still send messages to its room right before it is being disconnected. + OnLeave(roomLeaveCb LeaveRoomFunc) // Disconnect disconnects the client, close the underline websocket conn and removes it from the conn list // returns the error, if any, from the underline connection Disconnect() error @@ -116,6 +185,7 @@ type ( pinger *time.Ticker disconnected bool onDisconnectListeners []DisconnectFunc + onRoomLeaveListeners []LeaveRoomFunc onErrorListeners []ErrorFunc onNativeMessageListeners []NativeMessageFunc onEventListeners map[string][]MessageFunc @@ -126,6 +196,7 @@ type ( // access to the Context, use with causion, you can't use response writer as you imagine. ctx *iris.Context + values ConnectionValues server *server // #119 , websocket writers are not protected by locks inside the gorilla's websocket code // so we must protect them otherwise we're getting concurrent connection error on multi writers in the same time. @@ -144,6 +215,7 @@ func newConnection(s *server, ctx *iris.Context, underlineConn UnderlineConnecti id: id, messageType: websocket.TextMessage, onDisconnectListeners: make([]DisconnectFunc, 0), + onRoomLeaveListeners: make([]LeaveRoomFunc, 0), onErrorListeners: make([]ErrorFunc, 0), onNativeMessageListeners: make([]NativeMessageFunc, 0), onEventListeners: make(map[string][]MessageFunc, 0), @@ -317,6 +389,10 @@ func (c *connection) Context() *iris.Context { return c.ctx } +func (c *connection) Values() ConnectionValues { + return c.values +} + func (c *connection) fireDisconnect() { for i := range c.onDisconnectListeners { c.onDisconnectListeners[i]() @@ -373,8 +449,20 @@ func (c *connection) Join(roomName string) { c.server.Join(roomName, c.id) } -func (c *connection) Leave(roomName string) { - c.server.Leave(roomName, c.id) +func (c *connection) Leave(roomName string) bool { + return c.server.Leave(roomName, c.id) +} + +func (c *connection) OnLeave(roomLeaveCb LeaveRoomFunc) { + c.onRoomLeaveListeners = append(c.onRoomLeaveListeners, roomLeaveCb) + // note: the callbacks are called from the server on the '.leave' and '.LeaveAll' funcs. +} + +func (c *connection) fireOnLeave(roomName string) { + // fire the onRoomLeaveListeners + for i := range c.onRoomLeaveListeners { + c.onRoomLeaveListeners[i](roomName) + } } func (c *connection) Disconnect() error { diff --git a/adaptors/websocket/server.go b/adaptors/websocket/server.go index 300c1a11..9a722df1 100644 --- a/adaptors/websocket/server.go +++ b/adaptors/websocket/server.go @@ -48,7 +48,12 @@ type Server interface { // first parameter is the room name and the second the connection.ID() // // You can use connection.Leave("room name") instead. - Leave(roomName string, connID string) + // Returns true if the connection has actually left from the particular room. + Leave(roomName string, connID string) bool + + // GetConnectionsByRoom returns a list of Connection + // are joined to this room. + GetConnectionsByRoom(roomName string) []Connection // Disconnect force-disconnects a websocket connection // based on its connection.ID() @@ -282,6 +287,8 @@ func (s *server) LeaveAll(connID string) { for name, connectionIDs := range s.rooms { for i := range connectionIDs { if connectionIDs[i] == connID { + // fire the on room leave connection's listeners + s.connections.get(connID).fireOnLeave(name) // the connection is inside this room, lets remove it s.rooms[name][i] = s.rooms[name][len(s.rooms[name])-1] s.rooms[name] = s.rooms[name][:len(s.rooms[name])-1] @@ -295,14 +302,16 @@ func (s *server) LeaveAll(connID string) { // first parameter is the room name and the second the connection.ID() // // You can use connection.Leave("room name") instead. -func (s *server) Leave(roomName string, connID string) { +// Returns true if the connection has actually left from the particular room. +func (s *server) Leave(roomName string, connID string) bool { s.mu.Lock() - s.leave(roomName, connID) + left := s.leave(roomName, connID) s.mu.Unlock() + return left } // leave used internally, no locks used. -func (s *server) leave(roomName string, connID string) { +func (s *server) leave(roomName string, connID string) (left bool) { ///THINK: we could add locks to its room but we still use the lock for the whole rooms or we can just do what we do with connections // I will think about it on the next revision, so far we use the locks only for rooms so we are ok... if s.rooms[roomName] != nil { @@ -310,6 +319,7 @@ func (s *server) leave(roomName string, connID string) { if s.rooms[roomName][i] == connID { s.rooms[roomName][i] = s.rooms[roomName][len(s.rooms[roomName])-1] s.rooms[roomName] = s.rooms[roomName][:len(s.rooms[roomName])-1] + left = true break } } @@ -317,6 +327,27 @@ func (s *server) leave(roomName string, connID string) { delete(s.rooms, roomName) } } + + if left { + // fire the on room leave connection's listeners + s.connections.get(connID).fireOnLeave(roomName) + } + return +} + +// GetConnectionsByRoom returns a list of Connection +// which are joined to this room. +func (s *server) GetConnectionsByRoom(roomName string) []Connection { + s.mu.Lock() + var conns []Connection + if connIDs, found := s.rooms[roomName]; found { + for _, connID := range connIDs { + conns = append(conns, s.connections.get(connID)) + } + + } + s.mu.Unlock() + return conns } // emitMessage is the main 'router' of the messages coming from the connection @@ -369,14 +400,17 @@ func (s *server) emitMessage(from, to string, data []byte) { // // You can use the connection.Disconnect() instead. func (s *server) Disconnect(connID string) (err error) { + // leave from all joined rooms before remove the actual connection from the list. + // note: we cannot use that to send data if the client is actually closed. + s.LeaveAll(connID) + // remove the connection from the list if c, ok := s.connections.remove(connID); ok { if !c.disconnected { c.disconnected = true // stop the ping timer c.pinger.Stop() - // leave from all joined rooms - s.LeaveAll(connID) + // fire the disconnect callbacks, if any c.fireDisconnect() // close the underline connection and return its error, if any. diff --git a/adaptors/websocket/websocket.go b/adaptors/websocket/websocket.go index 0578b790..e54ba3b1 100644 --- a/adaptors/websocket/websocket.go +++ b/adaptors/websocket/websocket.go @@ -1,5 +1,5 @@ // Package websocket provides an easy way to setup server and client side rich websocket experience for Iris -// As originally written by me at https://github.com/kataras/go-websocket +// As originally written by me at https://github.com/kataras/go-websocket based on v0.1.1 package websocket import (