Websocket additions

Former-commit-id: 662bdd01a7cd403d1f7b8f0d17bed16ed1f06562
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-02-22 20:32:38 +02:00
parent 7533e2bf8b
commit 4e1d64c5c0
6 changed files with 136 additions and 12 deletions

View File

@ -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"))` - 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. - 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: Fixes:
- Websocket improvements and fix errors when using custom golang client - Websocket improvements and fix errors when using custom golang client

View File

@ -93,7 +93,7 @@ var Ws = (function () {
m = JSON.stringify(data); m = JSON.stringify(data);
} }
else { else {
console.log("Invalid"); console.log("Invalid, javascript-side should contains an empty second parameter.");
} }
return this._msg(event, t, m); return this._msg(event, t, m);
}; };

View File

@ -113,7 +113,7 @@ class Ws {
t = websocketJSONMessageType; t = websocketJSONMessageType;
m = JSON.stringify(data); m = JSON.stringify(data);
} else { } else {
console.log("Invalid"); console.log("Invalid, javascript-side should contains an empty second parameter.");
} }
return this._msg(event, t, m); return this._msg(event, t, m);

View File

@ -12,6 +12,61 @@ import (
"gopkg.in/kataras/iris.v6" "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 // UnderlineConnection is used for compatible with fasthttp and net/http underline websocket libraries
// we only need ~8 funcs from websocket.Conn so: // we only need ~8 funcs from websocket.Conn so:
type UnderlineConnection interface { type UnderlineConnection interface {
@ -65,6 +120,10 @@ type UnderlineConnection interface {
type ( type (
// DisconnectFunc is the callback which fires when a client/connection closed // DisconnectFunc is the callback which fires when a client/connection closed
DisconnectFunc func() 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 is the callback which fires when an error happens
ErrorFunc (func(string)) ErrorFunc (func(string))
// NativeMessageFunc is the callback for native websocket messages, receives one []byte parameter which is the raw client's message // 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 // 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 // then you use it to receive user information, for example: from headers
Context() *iris.Context 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 registers a callback which fires when this connection is closed by an error or manual
OnDisconnect(DisconnectFunc) 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 join a connection to a room, it doesn't check if connection is already there, so care
Join(string) Join(string)
// Leave removes a connection from a room // 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 // 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 // returns the error, if any, from the underline connection
Disconnect() error Disconnect() error
@ -116,6 +185,7 @@ type (
pinger *time.Ticker pinger *time.Ticker
disconnected bool disconnected bool
onDisconnectListeners []DisconnectFunc onDisconnectListeners []DisconnectFunc
onRoomLeaveListeners []LeaveRoomFunc
onErrorListeners []ErrorFunc onErrorListeners []ErrorFunc
onNativeMessageListeners []NativeMessageFunc onNativeMessageListeners []NativeMessageFunc
onEventListeners map[string][]MessageFunc 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. // access to the Context, use with causion, you can't use response writer as you imagine.
ctx *iris.Context ctx *iris.Context
values ConnectionValues
server *server server *server
// #119 , websocket writers are not protected by locks inside the gorilla's websocket code // #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. // 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, id: id,
messageType: websocket.TextMessage, messageType: websocket.TextMessage,
onDisconnectListeners: make([]DisconnectFunc, 0), onDisconnectListeners: make([]DisconnectFunc, 0),
onRoomLeaveListeners: make([]LeaveRoomFunc, 0),
onErrorListeners: make([]ErrorFunc, 0), onErrorListeners: make([]ErrorFunc, 0),
onNativeMessageListeners: make([]NativeMessageFunc, 0), onNativeMessageListeners: make([]NativeMessageFunc, 0),
onEventListeners: make(map[string][]MessageFunc, 0), onEventListeners: make(map[string][]MessageFunc, 0),
@ -317,6 +389,10 @@ func (c *connection) Context() *iris.Context {
return c.ctx return c.ctx
} }
func (c *connection) Values() ConnectionValues {
return c.values
}
func (c *connection) fireDisconnect() { func (c *connection) fireDisconnect() {
for i := range c.onDisconnectListeners { for i := range c.onDisconnectListeners {
c.onDisconnectListeners[i]() c.onDisconnectListeners[i]()
@ -373,8 +449,20 @@ func (c *connection) Join(roomName string) {
c.server.Join(roomName, c.id) c.server.Join(roomName, c.id)
} }
func (c *connection) Leave(roomName string) { func (c *connection) Leave(roomName string) bool {
c.server.Leave(roomName, c.id) 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 { func (c *connection) Disconnect() error {

View File

@ -48,7 +48,12 @@ type Server interface {
// first parameter is the room name and the second the connection.ID() // first parameter is the room name and the second the connection.ID()
// //
// You can use connection.Leave("room name") instead. // 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 // Disconnect force-disconnects a websocket connection
// based on its connection.ID() // based on its connection.ID()
@ -282,6 +287,8 @@ func (s *server) LeaveAll(connID string) {
for name, connectionIDs := range s.rooms { for name, connectionIDs := range s.rooms {
for i := range connectionIDs { for i := range connectionIDs {
if connectionIDs[i] == connID { 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 // the connection is inside this room, lets remove it
s.rooms[name][i] = s.rooms[name][len(s.rooms[name])-1] s.rooms[name][i] = s.rooms[name][len(s.rooms[name])-1]
s.rooms[name] = 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() // first parameter is the room name and the second the connection.ID()
// //
// You can use connection.Leave("room name") instead. // 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.mu.Lock()
s.leave(roomName, connID) left := s.leave(roomName, connID)
s.mu.Unlock() s.mu.Unlock()
return left
} }
// leave used internally, no locks used. // 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 ///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... // 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 { if s.rooms[roomName] != nil {
@ -310,6 +319,7 @@ func (s *server) leave(roomName string, connID string) {
if s.rooms[roomName][i] == connID { if s.rooms[roomName][i] == connID {
s.rooms[roomName][i] = s.rooms[roomName][len(s.rooms[roomName])-1] s.rooms[roomName][i] = s.rooms[roomName][len(s.rooms[roomName])-1]
s.rooms[roomName] = s.rooms[roomName][:len(s.rooms[roomName])-1] s.rooms[roomName] = s.rooms[roomName][:len(s.rooms[roomName])-1]
left = true
break break
} }
} }
@ -317,6 +327,27 @@ func (s *server) leave(roomName string, connID string) {
delete(s.rooms, roomName) 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 // 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. // You can use the connection.Disconnect() instead.
func (s *server) Disconnect(connID string) (err error) { 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 // remove the connection from the list
if c, ok := s.connections.remove(connID); ok { if c, ok := s.connections.remove(connID); ok {
if !c.disconnected { if !c.disconnected {
c.disconnected = true c.disconnected = true
// stop the ping timer // stop the ping timer
c.pinger.Stop() c.pinger.Stop()
// leave from all joined rooms
s.LeaveAll(connID)
// fire the disconnect callbacks, if any // fire the disconnect callbacks, if any
c.fireDisconnect() c.fireDisconnect()
// close the underline connection and return its error, if any. // close the underline connection and return its error, if any.

View File

@ -1,5 +1,5 @@
// Package websocket provides an easy way to setup server and client side rich websocket experience for Iris // 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 package websocket
import ( import (