mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
1216 lines
42 KiB
Go
1216 lines
42 KiB
Go
package iris
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/iris-contrib/logger"
|
|
"github.com/iris-contrib/websocket"
|
|
"github.com/kataras/iris/config"
|
|
"github.com/kataras/iris/utils"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------------------------------------
|
|
// --------------------------------Websocket implementation-------------------------------------------------
|
|
// Global functions in order to be able to use unlimitted number of websocket servers on each iris station--
|
|
// ---------------------------------------------------------------------------------------------------------
|
|
|
|
// NewWebsocketServer creates a websocket server and returns it
|
|
func NewWebsocketServer(c *config.Websocket) WebsocketServer {
|
|
return newWebsocketServer(c)
|
|
}
|
|
|
|
// RegisterWebsocketServer registers the handlers for the websocket server
|
|
// it's a bridge between station and websocket server
|
|
func RegisterWebsocketServer(station FrameworkAPI, server WebsocketServer, logger *logger.Logger) {
|
|
c := server.Config()
|
|
if c.Endpoint == "" {
|
|
return
|
|
}
|
|
|
|
websocketHandler := func(ctx *Context) {
|
|
if err := server.Upgrade(ctx); err != nil {
|
|
logger.Panic(err)
|
|
}
|
|
}
|
|
|
|
if c.Headers != nil && len(c.Headers) > 0 { // only for performance matter just re-create the websocketHandler if we have headers to set
|
|
websocketHandler = func(ctx *Context) {
|
|
for k, v := range c.Headers {
|
|
ctx.SetHeader(k, v)
|
|
}
|
|
|
|
if err := server.Upgrade(ctx); err != nil {
|
|
logger.Panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
station.Get(c.Endpoint, websocketHandler)
|
|
// serve the client side on domain:port/iris-ws.js
|
|
station.StaticContent("/iris-ws.js", "application/json", websocketClientSource)
|
|
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// --------------------------------WebsocketServer implementation-----------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
type (
|
|
// WebsocketConnectionFunc is the callback which fires when a client/websocketConnection is connected to the websocketServer.
|
|
// Receives one parameter which is the WebsocketConnection
|
|
WebsocketConnectionFunc func(WebsocketConnection)
|
|
// WebsocketRooms is just a map with key a string and value slice of string
|
|
WebsocketRooms map[string][]string
|
|
|
|
// websocketRoomPayload is used as payload from the websocketConnection to the websocketServer
|
|
websocketRoomPayload struct {
|
|
roomName string
|
|
websocketConnectionID string
|
|
}
|
|
|
|
// payloads, websocketConnection -> websocketServer
|
|
websocketMessagePayload struct {
|
|
from string
|
|
to string
|
|
data []byte
|
|
}
|
|
|
|
// WebsocketServer is the websocket server, listens on the config's port, the critical part is the event OnConnection
|
|
WebsocketServer interface {
|
|
Config() *config.Websocket
|
|
Upgrade(ctx *Context) error
|
|
OnConnection(cb WebsocketConnectionFunc)
|
|
}
|
|
|
|
websocketServer struct {
|
|
config *config.Websocket
|
|
upgrader websocket.Upgrader
|
|
put chan *websocketConnection
|
|
free chan *websocketConnection
|
|
websocketConnections map[string]*websocketConnection
|
|
join chan websocketRoomPayload
|
|
leave chan websocketRoomPayload
|
|
rooms WebsocketRooms // by default a websocketConnection is joined to a room which has the websocketConnection id as its name
|
|
mu sync.Mutex // for rooms
|
|
messages chan websocketMessagePayload
|
|
onConnectionListeners []WebsocketConnectionFunc
|
|
//websocketConnectionPool *sync.Pool // sadly I can't make this because the websocket websocketConnection is live until is closed.
|
|
}
|
|
)
|
|
|
|
var _ WebsocketServer = &websocketServer{}
|
|
|
|
// websocketServer implementation
|
|
|
|
// newWebsocketServer creates a websocket websocketServer and returns it
|
|
func newWebsocketServer(c *config.Websocket) *websocketServer {
|
|
s := &websocketServer{
|
|
config: c,
|
|
put: make(chan *websocketConnection),
|
|
free: make(chan *websocketConnection),
|
|
websocketConnections: make(map[string]*websocketConnection),
|
|
join: make(chan websocketRoomPayload, 1), // buffered because join can be called immediately on websocketConnection connected
|
|
leave: make(chan websocketRoomPayload),
|
|
rooms: make(WebsocketRooms),
|
|
messages: make(chan websocketMessagePayload, 1), // buffered because messages can be sent/received immediately on websocketConnection connected
|
|
onConnectionListeners: make([]WebsocketConnectionFunc, 0),
|
|
}
|
|
|
|
s.upgrader = websocket.Custom(s.handleWebsocketConnection, s.config.ReadBufferSize, s.config.WriteBufferSize, false)
|
|
go s.serve() // start the websocketServer automatically
|
|
return s
|
|
}
|
|
|
|
func (s *websocketServer) Config() *config.Websocket {
|
|
return s.config
|
|
}
|
|
|
|
func (s *websocketServer) Upgrade(ctx *Context) error {
|
|
return s.upgrader.Upgrade(ctx)
|
|
}
|
|
|
|
func (s *websocketServer) handleWebsocketConnection(websocketConn *websocket.Conn) {
|
|
c := newWebsocketConnection(websocketConn, s)
|
|
s.put <- c
|
|
go c.writer()
|
|
c.reader()
|
|
}
|
|
|
|
func (s *websocketServer) OnConnection(cb WebsocketConnectionFunc) {
|
|
s.onConnectionListeners = append(s.onConnectionListeners, cb)
|
|
}
|
|
|
|
func (s *websocketServer) joinRoom(roomName string, connID string) {
|
|
s.mu.Lock()
|
|
if s.rooms[roomName] == nil {
|
|
s.rooms[roomName] = make([]string, 0)
|
|
}
|
|
s.rooms[roomName] = append(s.rooms[roomName], connID)
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *websocketServer) leaveRoom(roomName string, connID string) {
|
|
s.mu.Lock()
|
|
if s.rooms[roomName] != nil {
|
|
for i := range s.rooms[roomName] {
|
|
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]
|
|
break
|
|
}
|
|
}
|
|
if len(s.rooms[roomName]) == 0 { // if room is empty then delete it
|
|
delete(s.rooms, roomName)
|
|
}
|
|
}
|
|
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *websocketServer) serve() {
|
|
for {
|
|
select {
|
|
case c := <-s.put: // websocketConnection connected
|
|
s.websocketConnections[c.id] = c
|
|
// make and join a room with the websocketConnection's id
|
|
s.rooms[c.id] = make([]string, 0)
|
|
s.rooms[c.id] = []string{c.id}
|
|
for i := range s.onConnectionListeners {
|
|
s.onConnectionListeners[i](c)
|
|
}
|
|
case c := <-s.free: // websocketConnection closed
|
|
if _, found := s.websocketConnections[c.id]; found {
|
|
// leave from all rooms
|
|
for roomName := range s.rooms {
|
|
s.leaveRoom(roomName, c.id)
|
|
}
|
|
delete(s.websocketConnections, c.id)
|
|
close(c.send)
|
|
c.fireDisconnect()
|
|
|
|
}
|
|
case join := <-s.join:
|
|
s.joinRoom(join.roomName, join.websocketConnectionID)
|
|
case leave := <-s.leave:
|
|
if _, found := s.websocketConnections[leave.websocketConnectionID]; found {
|
|
s.leaveRoom(leave.roomName, leave.websocketConnectionID)
|
|
}
|
|
case msg := <-s.messages: // message received from the websocketConnection
|
|
if msg.to != All && msg.to != NotMe && s.rooms[msg.to] != nil {
|
|
// it suppose to send the message to a room
|
|
for _, websocketConnectionIDInsideRoom := range s.rooms[msg.to] {
|
|
if c, connected := s.websocketConnections[websocketConnectionIDInsideRoom]; connected {
|
|
c.send <- msg.data //here we send it without need to continue below
|
|
} else {
|
|
// the websocketConnection is not connected but it's inside the room, we remove it on disconnect but for ANY CASE:
|
|
s.leaveRoom(c.id, msg.to)
|
|
}
|
|
}
|
|
|
|
} else { // it suppose to send the message to all opened websocketConnections or to all except the sender
|
|
for connID, c := range s.websocketConnections {
|
|
if msg.to != All { // if it's not suppose to send to all websocketConnections (including itself)
|
|
if msg.to == NotMe && msg.from == connID { // if broadcast to other websocketConnections except this
|
|
continue //here we do the opossite of previous block, just skip this websocketConnection when it's suppose to send the message to all websocketConnections except the sender
|
|
}
|
|
}
|
|
select {
|
|
case s.websocketConnections[connID].send <- msg.data: //send the message back to the websocketConnection in order to send it to the client
|
|
default:
|
|
close(c.send)
|
|
delete(s.websocketConnections, connID)
|
|
c.fireDisconnect()
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// --------------------------------WebsocketEmmiter implementation----------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
const (
|
|
// All is the string which the WebsocketEmmiter use to send a message to all
|
|
All = ""
|
|
// NotMe is the string which the WebsocketEmmiter use to send a message to all except this websocketConnection
|
|
NotMe = ";iris;to;all;except;me;"
|
|
// Broadcast is the string which the WebsocketEmmiter use to send a message to all except this websocketConnection, same as 'NotMe'
|
|
Broadcast = NotMe
|
|
)
|
|
|
|
type (
|
|
// WebsocketEmmiter is the message/or/event manager
|
|
WebsocketEmmiter interface {
|
|
// EmitMessage sends a native websocket message
|
|
EmitMessage([]byte) error
|
|
// Emit sends a message on a particular event
|
|
Emit(string, interface{}) error
|
|
}
|
|
|
|
websocketEmmiter struct {
|
|
conn *websocketConnection
|
|
to string
|
|
}
|
|
)
|
|
|
|
var _ WebsocketEmmiter = &websocketEmmiter{}
|
|
|
|
func newWebsocketEmmiter(c *websocketConnection, to string) *websocketEmmiter {
|
|
return &websocketEmmiter{conn: c, to: to}
|
|
}
|
|
|
|
func (e *websocketEmmiter) EmitMessage(nativeMessage []byte) error {
|
|
mp := websocketMessagePayload{e.conn.id, e.to, nativeMessage}
|
|
e.conn.websocketServer.messages <- mp
|
|
return nil
|
|
}
|
|
|
|
func (e *websocketEmmiter) Emit(event string, data interface{}) error {
|
|
message, err := websocketMessageSerialize(event, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.EmitMessage([]byte(message))
|
|
return nil
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// --------------------------------WebsocketWebsocketConnection implementation-------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
type (
|
|
// WebsocketDisconnectFunc is the callback which fires when a client/websocketConnection closed
|
|
WebsocketDisconnectFunc func()
|
|
// WebsocketErrorFunc is the callback which fires when an error happens
|
|
WebsocketErrorFunc (func(string))
|
|
// WebsocketNativeMessageFunc is the callback for native websocket messages, receives one []byte parameter which is the raw client's message
|
|
WebsocketNativeMessageFunc func([]byte)
|
|
// WebsocketMessageFunc is the second argument to the WebsocketEmmiter's Emit functions.
|
|
// A callback which should receives one parameter of type string, int, bool or any valid JSON/Go struct
|
|
WebsocketMessageFunc interface{}
|
|
// WebsocketConnection is the client
|
|
WebsocketConnection interface {
|
|
// WebsocketEmmiter implements EmitMessage & Emit
|
|
WebsocketEmmiter
|
|
// ID returns the websocketConnection's identifier
|
|
ID() string
|
|
// OnDisconnect registers a callback which fires when this websocketConnection is closed by an error or manual
|
|
OnDisconnect(WebsocketDisconnectFunc)
|
|
// OnError registers a callback which fires when this websocketConnection occurs an error
|
|
OnError(WebsocketErrorFunc)
|
|
// EmitError can be used to send a custom error message to the websocketConnection
|
|
//
|
|
// It does nothing more than firing the OnError listeners. It doesn't sends anything to the client.
|
|
EmitError(errorMessage string)
|
|
// To defines where websocketServer should send a message
|
|
// returns an emmiter to send messages
|
|
To(string) WebsocketEmmiter
|
|
// OnMessage registers a callback which fires when native websocket message received
|
|
OnMessage(WebsocketNativeMessageFunc)
|
|
// On registers a callback to a particular event which fires when a message to this event received
|
|
On(string, WebsocketMessageFunc)
|
|
// Join join a websocketConnection to a room, it doesn't check if websocketConnection is already there, so care
|
|
Join(string)
|
|
// Leave removes a websocketConnection from a room
|
|
Leave(string)
|
|
// 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
|
|
}
|
|
|
|
websocketConnection struct {
|
|
underline *websocket.Conn
|
|
id string
|
|
send chan []byte
|
|
onDisconnectListeners []WebsocketDisconnectFunc
|
|
onErrorListeners []WebsocketErrorFunc
|
|
onNativeMessageListeners []WebsocketNativeMessageFunc
|
|
onEventListeners map[string][]WebsocketMessageFunc
|
|
// these were maden for performance only
|
|
self WebsocketEmmiter // pre-defined emmiter than sends message to its self client
|
|
broadcast WebsocketEmmiter // pre-defined emmiter that sends message to all except this
|
|
all WebsocketEmmiter // pre-defined emmiter which sends message to all clients
|
|
|
|
websocketServer *websocketServer
|
|
}
|
|
)
|
|
|
|
var _ WebsocketConnection = &websocketConnection{}
|
|
|
|
func newWebsocketConnection(websocketConn *websocket.Conn, s *websocketServer) *websocketConnection {
|
|
c := &websocketConnection{
|
|
id: utils.RandomString(64),
|
|
underline: websocketConn,
|
|
send: make(chan []byte, 256),
|
|
onDisconnectListeners: make([]WebsocketDisconnectFunc, 0),
|
|
onErrorListeners: make([]WebsocketErrorFunc, 0),
|
|
onNativeMessageListeners: make([]WebsocketNativeMessageFunc, 0),
|
|
onEventListeners: make(map[string][]WebsocketMessageFunc, 0),
|
|
websocketServer: s,
|
|
}
|
|
|
|
c.self = newWebsocketEmmiter(c, c.id)
|
|
c.broadcast = newWebsocketEmmiter(c, NotMe)
|
|
c.all = newWebsocketEmmiter(c, All)
|
|
|
|
return c
|
|
}
|
|
|
|
func (c *websocketConnection) write(websocketMessageType int, data []byte) error {
|
|
c.underline.SetWriteDeadline(time.Now().Add(c.websocketServer.config.WriteTimeout))
|
|
return c.underline.WriteMessage(websocketMessageType, data)
|
|
}
|
|
|
|
func (c *websocketConnection) writer() {
|
|
ticker := time.NewTicker(c.websocketServer.config.PingPeriod)
|
|
defer func() {
|
|
ticker.Stop()
|
|
c.Disconnect()
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case msg, ok := <-c.send:
|
|
if !ok {
|
|
defer func() {
|
|
|
|
// FIX FOR: https://github.com/kataras/iris/issues/175
|
|
// AS I TESTED ON TRIDENT ENGINE (INTERNET EXPLORER/SAFARI):
|
|
// NAVIGATE TO SITE, CLOSE THE TAB, NOTHING HAPPENS
|
|
// CLOSE THE WHOLE BROWSER, THEN THE c.conn is NOT NILL BUT ALL ITS FUNCTIONS PANICS, MEANS THAT IS THE STRUCT IS NOT NIL BUT THE WRITER/READER ARE NIL
|
|
// THE ONLY SOLUTION IS TO RECOVER HERE AT ANY PANIC
|
|
// THE FRAMETYPE = 8, c.closeSend = true
|
|
// NOTE THAT THE CLIENT IS NOT DISCONNECTED UNTIL THE WHOLE WINDOW BROWSER CLOSED, this is engine's bug.
|
|
//
|
|
if err := recover(); err != nil {
|
|
ticker.Stop()
|
|
c.Disconnect()
|
|
}
|
|
}()
|
|
c.write(websocket.CloseMessage, []byte{})
|
|
return
|
|
}
|
|
|
|
c.underline.SetWriteDeadline(time.Now().Add(c.websocketServer.config.WriteTimeout))
|
|
res, err := c.underline.NextWriter(websocket.TextMessage)
|
|
if err != nil {
|
|
return
|
|
}
|
|
res.Write(msg)
|
|
|
|
n := len(c.send)
|
|
for i := 0; i < n; i++ {
|
|
res.Write(<-c.send)
|
|
}
|
|
|
|
if err := res.Close(); err != nil {
|
|
return
|
|
}
|
|
|
|
// if err := c.write(websocket.TextMessage, msg); err != nil {
|
|
// return
|
|
// }
|
|
|
|
case <-ticker.C:
|
|
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *websocketConnection) reader() {
|
|
defer func() {
|
|
c.Disconnect()
|
|
}()
|
|
conn := c.underline
|
|
|
|
conn.SetReadLimit(c.websocketServer.config.MaxMessageSize)
|
|
conn.SetReadDeadline(time.Now().Add(c.websocketServer.config.PongTimeout))
|
|
conn.SetPongHandler(func(s string) error {
|
|
conn.SetReadDeadline(time.Now().Add(c.websocketServer.config.PongTimeout))
|
|
return nil
|
|
})
|
|
|
|
for {
|
|
if _, data, err := conn.ReadMessage(); err != nil {
|
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
|
c.EmitError(err.Error())
|
|
}
|
|
break
|
|
} else {
|
|
c.messageReceived(data)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// messageReceived checks the incoming message and fire the nativeMessage listeners or the event listeners (iris-ws custom message)
|
|
func (c *websocketConnection) messageReceived(data []byte) {
|
|
|
|
if bytes.HasPrefix(data, websocketMessagePrefixBytes) {
|
|
customData := string(data)
|
|
//it's a custom iris-ws message
|
|
receivedEvt := getWebsocketCustomEvent(customData)
|
|
listeners := c.onEventListeners[receivedEvt]
|
|
if listeners == nil { // if not listeners for this event exit from here
|
|
return
|
|
}
|
|
customMessage, err := websocketMessageDeserialize(receivedEvt, customData)
|
|
if customMessage == nil || err != nil {
|
|
return
|
|
}
|
|
|
|
for i := range listeners {
|
|
if fn, ok := listeners[i].(func()); ok { // its a simple func(){} callback
|
|
fn()
|
|
} else if fnString, ok := listeners[i].(func(string)); ok {
|
|
|
|
if msgString, is := customMessage.(string); is {
|
|
fnString(msgString)
|
|
} else if msgInt, is := customMessage.(int); is {
|
|
// here if websocketServer side waiting for string but client side sent an int, just convert this int to a string
|
|
fnString(strconv.Itoa(msgInt))
|
|
}
|
|
|
|
} else if fnInt, ok := listeners[i].(func(int)); ok {
|
|
fnInt(customMessage.(int))
|
|
} else if fnBool, ok := listeners[i].(func(bool)); ok {
|
|
fnBool(customMessage.(bool))
|
|
} else if fnBytes, ok := listeners[i].(func([]byte)); ok {
|
|
fnBytes(customMessage.([]byte))
|
|
} else {
|
|
listeners[i].(func(interface{}))(customMessage)
|
|
}
|
|
|
|
}
|
|
} else {
|
|
// it's native websocket message
|
|
for i := range c.onNativeMessageListeners {
|
|
c.onNativeMessageListeners[i](data)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (c *websocketConnection) ID() string {
|
|
return c.id
|
|
}
|
|
|
|
func (c *websocketConnection) fireDisconnect() {
|
|
for i := range c.onDisconnectListeners {
|
|
c.onDisconnectListeners[i]()
|
|
}
|
|
}
|
|
|
|
func (c *websocketConnection) OnDisconnect(cb WebsocketDisconnectFunc) {
|
|
c.onDisconnectListeners = append(c.onDisconnectListeners, cb)
|
|
}
|
|
|
|
func (c *websocketConnection) OnError(cb WebsocketErrorFunc) {
|
|
c.onErrorListeners = append(c.onErrorListeners, cb)
|
|
}
|
|
|
|
func (c *websocketConnection) EmitError(errorMessage string) {
|
|
for _, cb := range c.onErrorListeners {
|
|
cb(errorMessage)
|
|
}
|
|
}
|
|
|
|
func (c *websocketConnection) To(to string) WebsocketEmmiter {
|
|
if to == NotMe { // if send to all except me, then return the pre-defined emmiter, and so on
|
|
return c.broadcast
|
|
} else if to == All {
|
|
return c.all
|
|
} else if to == c.id {
|
|
return c.self
|
|
}
|
|
// is an emmiter to another client/websocketConnection
|
|
return newWebsocketEmmiter(c, to)
|
|
}
|
|
|
|
func (c *websocketConnection) EmitMessage(nativeMessage []byte) error {
|
|
return c.self.EmitMessage(nativeMessage)
|
|
}
|
|
|
|
func (c *websocketConnection) Emit(event string, message interface{}) error {
|
|
return c.self.Emit(event, message)
|
|
}
|
|
|
|
func (c *websocketConnection) OnMessage(cb WebsocketNativeMessageFunc) {
|
|
c.onNativeMessageListeners = append(c.onNativeMessageListeners, cb)
|
|
}
|
|
|
|
func (c *websocketConnection) On(event string, cb WebsocketMessageFunc) {
|
|
if c.onEventListeners[event] == nil {
|
|
c.onEventListeners[event] = make([]WebsocketMessageFunc, 0)
|
|
}
|
|
|
|
c.onEventListeners[event] = append(c.onEventListeners[event], cb)
|
|
}
|
|
|
|
func (c *websocketConnection) Join(roomName string) {
|
|
payload := websocketRoomPayload{roomName, c.id}
|
|
c.websocketServer.join <- payload
|
|
}
|
|
|
|
func (c *websocketConnection) Leave(roomName string) {
|
|
payload := websocketRoomPayload{roomName, c.id}
|
|
c.websocketServer.leave <- payload
|
|
}
|
|
|
|
func (c *websocketConnection) Disconnect() error {
|
|
c.websocketServer.free <- c // leaves from all rooms, fires the disconnect listeners and finally remove from conn list
|
|
return c.underline.Close()
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -----------------websocket messages and de/serialization implementation--------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
/*
|
|
serializer, [de]websocketMessageSerialize the messages from the client to the websocketServer and from the websocketServer to the client
|
|
*/
|
|
|
|
// The same values are exists on client side also
|
|
const (
|
|
websocketStringMessageType websocketMessageType = iota
|
|
websocketIntMessageType
|
|
websocketBoolMessageType
|
|
websocketBytesMessageType
|
|
websocketJSONMessageType
|
|
)
|
|
|
|
const (
|
|
websocketMessagePrefix = "iris-websocket-message:"
|
|
websocketMessageSeparator = ";"
|
|
websocketMessagePrefixLen = len(websocketMessagePrefix)
|
|
websocketMessageSeparatorLen = len(websocketMessageSeparator)
|
|
websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1
|
|
websocketMessagePrefixIdx = websocketMessagePrefixLen - 1
|
|
websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1
|
|
)
|
|
|
|
var (
|
|
websocketMessageSeparatorByte = websocketMessageSeparator[0]
|
|
websocketMessageBuffer = utils.NewBufferPool(256)
|
|
websocketMessagePrefixBytes = []byte(websocketMessagePrefix)
|
|
)
|
|
|
|
type (
|
|
websocketMessageType uint8
|
|
)
|
|
|
|
func (m websocketMessageType) String() string {
|
|
return strconv.Itoa(int(m))
|
|
}
|
|
|
|
func (m websocketMessageType) Name() string {
|
|
if m == websocketStringMessageType {
|
|
return "string"
|
|
} else if m == websocketIntMessageType {
|
|
return "int"
|
|
} else if m == websocketBoolMessageType {
|
|
return "bool"
|
|
} else if m == websocketBytesMessageType {
|
|
return "[]byte"
|
|
} else if m == websocketJSONMessageType {
|
|
return "json"
|
|
}
|
|
|
|
return "Invalid(" + m.String() + ")"
|
|
|
|
}
|
|
|
|
// websocketMessageSerialize serializes a custom websocket message from websocketServer to be delivered to the client
|
|
// returns the string form of the message
|
|
// Supported data types are: string, int, bool, bytes and JSON.
|
|
func websocketMessageSerialize(event string, data interface{}) (string, error) {
|
|
var msgType websocketMessageType
|
|
var dataMessage string
|
|
|
|
if s, ok := data.(string); ok {
|
|
msgType = websocketStringMessageType
|
|
dataMessage = s
|
|
} else if i, ok := data.(int); ok {
|
|
msgType = websocketIntMessageType
|
|
dataMessage = strconv.Itoa(i)
|
|
} else if b, ok := data.(bool); ok {
|
|
msgType = websocketBoolMessageType
|
|
dataMessage = strconv.FormatBool(b)
|
|
} else if by, ok := data.([]byte); ok {
|
|
msgType = websocketBytesMessageType
|
|
dataMessage = string(by)
|
|
} else {
|
|
//we suppose is json
|
|
res, err := json.Marshal(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
msgType = websocketJSONMessageType
|
|
dataMessage = string(res)
|
|
}
|
|
|
|
b := websocketMessageBuffer.Get()
|
|
b.WriteString(websocketMessagePrefix)
|
|
b.WriteString(event)
|
|
b.WriteString(websocketMessageSeparator)
|
|
b.WriteString(msgType.String())
|
|
b.WriteString(websocketMessageSeparator)
|
|
b.WriteString(dataMessage)
|
|
dataMessage = b.String()
|
|
websocketMessageBuffer.Put(b)
|
|
|
|
return dataMessage, nil
|
|
|
|
}
|
|
|
|
// websocketMessageDeserialize deserializes a custom websocket message from the client
|
|
// ex: iris-websocket-message;chat;4;themarshaledstringfromajsonstruct will return 'hello' as string
|
|
// Supported data types are: string, int, bool, bytes and JSON.
|
|
func websocketMessageDeserialize(event string, websocketMessage string) (message interface{}, err error) {
|
|
t, formaterr := strconv.Atoi(websocketMessage[websocketMessagePrefixAndSepIdx+len(event)+1 : websocketMessagePrefixAndSepIdx+len(event)+2]) // in order to iris-websocket-message;user;-> 4
|
|
if formaterr != nil {
|
|
return nil, formaterr
|
|
}
|
|
_type := websocketMessageType(t)
|
|
_message := websocketMessage[websocketMessagePrefixAndSepIdx+len(event)+3:] // in order to iris-websocket-message;user;4; -> themarshaledstringfromajsonstruct
|
|
|
|
if _type == websocketStringMessageType {
|
|
message = string(_message)
|
|
} else if _type == websocketIntMessageType {
|
|
message, err = strconv.Atoi(_message)
|
|
} else if _type == websocketBoolMessageType {
|
|
message, err = strconv.ParseBool(_message)
|
|
} else if _type == websocketBytesMessageType {
|
|
message = []byte(_message)
|
|
} else if _type == websocketJSONMessageType {
|
|
err = json.Unmarshal([]byte(_message), message)
|
|
} else {
|
|
return nil, fmt.Errorf("Type %s is invalid for message: %s", _type.Name(), websocketMessage)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// getWebsocketCustomEvent return empty string when the websocketMessage is native message
|
|
func getWebsocketCustomEvent(websocketMessage string) string {
|
|
if len(websocketMessage) < websocketMessagePrefixAndSepIdx {
|
|
return ""
|
|
}
|
|
s := websocketMessage[websocketMessagePrefixAndSepIdx:]
|
|
evt := s[:strings.IndexByte(s, websocketMessageSeparatorByte)]
|
|
return evt
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// ----------------Client side websocket javascript source code ------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
var websocketClientSource = []byte(`var websocketStringMessageType = 0;
|
|
var websocketIntMessageType = 1;
|
|
var websocketBoolMessageType = 2;
|
|
// bytes is missing here for reasons I will explain somewhen
|
|
var websocketJSONMessageType = 4;
|
|
var websocketMessagePrefix = "iris-websocket-message:";
|
|
var websocketMessageSeparator = ";";
|
|
var websocketMessagePrefixLen = websocketMessagePrefix.length;
|
|
var websocketMessageSeparatorLen = websocketMessageSeparator.length;
|
|
var websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1;
|
|
var websocketMessagePrefixIdx = websocketMessagePrefixLen - 1;
|
|
var websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1;
|
|
var Ws = (function () {
|
|
//
|
|
function Ws(endpoint, protocols) {
|
|
var _this = this;
|
|
// events listeners
|
|
this.connectListeners = [];
|
|
this.disconnectListeners = [];
|
|
this.nativeMessageListeners = [];
|
|
this.messageListeners = {};
|
|
if (!window["WebSocket"]) {
|
|
return;
|
|
}
|
|
if (endpoint.indexOf("ws") == -1) {
|
|
endpoint = "ws://" + endpoint;
|
|
}
|
|
if (protocols != null && protocols.length > 0) {
|
|
this.conn = new WebSocket(endpoint, protocols);
|
|
}
|
|
else {
|
|
this.conn = new WebSocket(endpoint);
|
|
}
|
|
this.conn.onopen = (function (evt) {
|
|
_this.fireConnect();
|
|
_this.isReady = true;
|
|
return null;
|
|
});
|
|
this.conn.onclose = (function (evt) {
|
|
_this.fireDisconnect();
|
|
return null;
|
|
});
|
|
this.conn.onmessage = (function (evt) {
|
|
_this.messageReceivedFromConn(evt);
|
|
});
|
|
}
|
|
//utils
|
|
Ws.prototype.isNumber = function (obj) {
|
|
return !isNaN(obj - 0) && obj !== null && obj !== "" && obj !== false;
|
|
};
|
|
Ws.prototype.isString = function (obj) {
|
|
return Object.prototype.toString.call(obj) == "[object String]";
|
|
};
|
|
Ws.prototype.isBoolean = function (obj) {
|
|
return typeof obj === 'boolean' ||
|
|
(typeof obj === 'object' && typeof obj.valueOf() === 'boolean');
|
|
};
|
|
Ws.prototype.isJSON = function (obj) {
|
|
try {
|
|
JSON.parse(obj);
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
//
|
|
// messages
|
|
Ws.prototype._msg = function (event, websocketMessageType, dataMessage) {
|
|
return websocketMessagePrefix + event + websocketMessageSeparator + String(websocketMessageType) + websocketMessageSeparator + dataMessage;
|
|
};
|
|
Ws.prototype.encodeMessage = function (event, data) {
|
|
var m = "";
|
|
var t = 0;
|
|
if (this.isNumber(data)) {
|
|
t = websocketIntMessageType;
|
|
m = data.toString();
|
|
}
|
|
else if (this.isBoolean(data)) {
|
|
t = websocketBoolMessageType;
|
|
m = data.toString();
|
|
}
|
|
else if (this.isString(data)) {
|
|
t = websocketStringMessageType;
|
|
m = data.toString();
|
|
}
|
|
else if (this.isJSON(data)) {
|
|
//propably json-object
|
|
t = websocketJSONMessageType;
|
|
m = JSON.stringify(data);
|
|
}
|
|
else {
|
|
console.log("Invalid");
|
|
}
|
|
return this._msg(event, t, m);
|
|
};
|
|
Ws.prototype.decodeMessage = function (event, websocketMessage) {
|
|
//iris-websocket-message;user;4;themarshaledstringfromajsonstruct
|
|
var skipLen = websocketMessagePrefixLen + websocketMessageSeparatorLen + event.length + 2;
|
|
if (websocketMessage.length < skipLen + 1) {
|
|
return null;
|
|
}
|
|
var websocketMessageType = parseInt(websocketMessage.charAt(skipLen - 2));
|
|
var theMessage = websocketMessage.substring(skipLen, websocketMessage.length);
|
|
if (websocketMessageType == websocketIntMessageType) {
|
|
return parseInt(theMessage);
|
|
}
|
|
else if (websocketMessageType == websocketBoolMessageType) {
|
|
return Boolean(theMessage);
|
|
}
|
|
else if (websocketMessageType == websocketStringMessageType) {
|
|
return theMessage;
|
|
}
|
|
else if (websocketMessageType == websocketJSONMessageType) {
|
|
return JSON.parse(theMessage);
|
|
}
|
|
else {
|
|
return null; // invalid
|
|
}
|
|
};
|
|
Ws.prototype.getWebsocketCustomEvent = function (websocketMessage) {
|
|
if (websocketMessage.length < websocketMessagePrefixAndSepIdx) {
|
|
return "";
|
|
}
|
|
var s = websocketMessage.substring(websocketMessagePrefixAndSepIdx, websocketMessage.length);
|
|
var evt = s.substring(0, s.indexOf(websocketMessageSeparator));
|
|
return evt;
|
|
};
|
|
Ws.prototype.getCustomMessage = function (event, websocketMessage) {
|
|
var eventIdx = websocketMessage.indexOf(event + websocketMessageSeparator);
|
|
var s = websocketMessage.substring(eventIdx + event.length + websocketMessageSeparator.length + 2, websocketMessage.length);
|
|
return s;
|
|
};
|
|
//
|
|
// Ws Events
|
|
// messageReceivedFromConn this is the func which decides
|
|
// if it's a native websocket message or a custom iris-ws message
|
|
// if native message then calls the fireNativeMessage
|
|
// else calls the fireMessage
|
|
//
|
|
// remember Iris gives you the freedom of native websocket messages if you don't want to use this client side at all.
|
|
Ws.prototype.messageReceivedFromConn = function (evt) {
|
|
//check if iris-ws message
|
|
var message = evt.data;
|
|
if (message.indexOf(websocketMessagePrefix) != -1) {
|
|
var event_1 = this.getWebsocketCustomEvent(message);
|
|
if (event_1 != "") {
|
|
// it's a custom message
|
|
this.fireMessage(event_1, this.getCustomMessage(event_1, message));
|
|
return;
|
|
}
|
|
}
|
|
// it's a native websocket message
|
|
this.fireNativeMessage(message);
|
|
};
|
|
Ws.prototype.OnConnect = function (fn) {
|
|
if (this.isReady) {
|
|
fn();
|
|
}
|
|
this.connectListeners.push(fn);
|
|
};
|
|
Ws.prototype.fireConnect = function () {
|
|
for (var i = 0; i < this.connectListeners.length; i++) {
|
|
this.connectListeners[i]();
|
|
}
|
|
};
|
|
Ws.prototype.OnDisconnect = function (fn) {
|
|
this.disconnectListeners.push(fn);
|
|
};
|
|
Ws.prototype.fireDisconnect = function () {
|
|
for (var i = 0; i < this.disconnectListeners.length; i++) {
|
|
this.disconnectListeners[i]();
|
|
}
|
|
};
|
|
Ws.prototype.OnMessage = function (cb) {
|
|
this.nativeMessageListeners.push(cb);
|
|
};
|
|
Ws.prototype.fireNativeMessage = function (websocketMessage) {
|
|
for (var i = 0; i < this.nativeMessageListeners.length; i++) {
|
|
this.nativeMessageListeners[i](websocketMessage);
|
|
}
|
|
};
|
|
Ws.prototype.On = function (event, cb) {
|
|
if (this.messageListeners[event] == null || this.messageListeners[event] == undefined) {
|
|
this.messageListeners[event] = [];
|
|
}
|
|
this.messageListeners[event].push(cb);
|
|
};
|
|
Ws.prototype.fireMessage = function (event, message) {
|
|
for (var key in this.messageListeners) {
|
|
if (this.messageListeners.hasOwnProperty(key)) {
|
|
if (key == event) {
|
|
for (var i = 0; i < this.messageListeners[key].length; i++) {
|
|
this.messageListeners[key][i](message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
//
|
|
// Ws Actions
|
|
Ws.prototype.Disconnect = function () {
|
|
this.conn.close();
|
|
};
|
|
// EmitMessage sends a native websocket message
|
|
Ws.prototype.EmitMessage = function (websocketMessage) {
|
|
this.conn.send(websocketMessage);
|
|
};
|
|
// Emit sends an iris-custom websocket message
|
|
Ws.prototype.Emit = function (event, data) {
|
|
var messageStr = this.encodeMessage(event, data);
|
|
this.EmitMessage(messageStr);
|
|
};
|
|
return Ws;
|
|
}());
|
|
`)
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// ----------------Client side websocket commented typescript source code --------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
/*
|
|
const websocketStringMessageType = 0;
|
|
const websocketIntMessageType = 1;
|
|
const websocketBoolMessageType = 2;
|
|
// bytes is missing here for reasons I will explain somewhen
|
|
const websocketJSONMessageType = 4;
|
|
|
|
const websocketMessagePrefix = "iris-websocket-message:";
|
|
const websocketMessageSeparator = ";";
|
|
|
|
const websocketMessagePrefixLen = websocketMessagePrefix.length;
|
|
var websocketMessageSeparatorLen = websocketMessageSeparator.length;
|
|
var websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1;
|
|
var websocketMessagePrefixIdx = websocketMessagePrefixLen - 1;
|
|
var websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1;
|
|
|
|
type onConnectFunc = () => void;
|
|
type onWebsocketDisconnectFunc = () => void;
|
|
type onWebsocketNativeMessageFunc = (websocketMessage: string) => void;
|
|
type onMessageFunc = (message: any) => void;
|
|
|
|
class Ws {
|
|
private conn: WebSocket;
|
|
private isReady: boolean;
|
|
|
|
// events listeners
|
|
|
|
private connectListeners: onConnectFunc[] = [];
|
|
private disconnectListeners: onWebsocketDisconnectFunc[] = [];
|
|
private nativeMessageListeners: onWebsocketNativeMessageFunc[] = [];
|
|
private messageListeners: { [event: string]: onMessageFunc[] } = {};
|
|
|
|
//
|
|
|
|
constructor(endpoint: string, protocols?: string[]) {
|
|
if (!window["WebSocket"]) {
|
|
return;
|
|
}
|
|
|
|
if (endpoint.indexOf("ws") == -1) {
|
|
endpoint = "ws://" + endpoint;
|
|
}
|
|
if (protocols != null && protocols.length > 0) {
|
|
this.conn = new WebSocket(endpoint, protocols);
|
|
} else {
|
|
this.conn = new WebSocket(endpoint);
|
|
}
|
|
|
|
this.conn.onopen = ((evt: Event): any => {
|
|
this.fireConnect();
|
|
this.isReady = true;
|
|
return null;
|
|
});
|
|
|
|
this.conn.onclose = ((evt: Event): any => {
|
|
this.fireDisconnect();
|
|
return null;
|
|
});
|
|
|
|
this.conn.onmessage = ((evt: MessageEvent) => {
|
|
this.messageReceivedFromConn(evt);
|
|
});
|
|
}
|
|
|
|
//utils
|
|
|
|
private isNumber(obj: any): boolean {
|
|
return !isNaN(obj - 0) && obj !== null && obj !== "" && obj !== false;
|
|
}
|
|
|
|
private isString(obj: any): boolean {
|
|
return Object.prototype.toString.call(obj) == "[object String]";
|
|
}
|
|
|
|
private isBoolean(obj: any): boolean {
|
|
return typeof obj === 'boolean' ||
|
|
(typeof obj === 'object' && typeof obj.valueOf() === 'boolean');
|
|
}
|
|
|
|
private isJSON(obj: any): boolean {
|
|
try {
|
|
JSON.parse(obj);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//
|
|
|
|
// messages
|
|
private _msg(event: string, websocketMessageType: number, dataMessage: string): string {
|
|
|
|
return websocketMessagePrefix + event + websocketMessageSeparator + String(websocketMessageType) + websocketMessageSeparator + dataMessage;
|
|
}
|
|
|
|
private encodeMessage(event: string, data: any): string {
|
|
let m = "";
|
|
let t = 0;
|
|
if (this.isNumber(data)) {
|
|
t = websocketIntMessageType;
|
|
m = data.toString();
|
|
} else if (this.isBoolean(data)) {
|
|
t = websocketBoolMessageType;
|
|
m = data.toString();
|
|
} else if (this.isString(data)) {
|
|
t = websocketStringMessageType;
|
|
m = data.toString();
|
|
} else if (this.isJSON(data)) {
|
|
//propably json-object
|
|
t = websocketJSONMessageType;
|
|
m = JSON.stringify(data);
|
|
} else {
|
|
console.log("Invalid");
|
|
}
|
|
|
|
return this._msg(event, t, m);
|
|
}
|
|
|
|
private decodeMessage<T>(event: string, websocketMessage: string): T | any {
|
|
//iris-websocket-message;user;4;themarshaledstringfromajsonstruct
|
|
let skipLen = websocketMessagePrefixLen + websocketMessageSeparatorLen + event.length + 2;
|
|
if (websocketMessage.length < skipLen + 1) {
|
|
return null;
|
|
}
|
|
let websocketMessageType = parseInt(websocketMessage.charAt(skipLen - 2));
|
|
let theMessage = websocketMessage.substring(skipLen, websocketMessage.length);
|
|
if (websocketMessageType == websocketIntMessageType) {
|
|
return parseInt(theMessage);
|
|
} else if (websocketMessageType == websocketBoolMessageType) {
|
|
return Boolean(theMessage);
|
|
} else if (websocketMessageType == websocketStringMessageType) {
|
|
return theMessage;
|
|
} else if (websocketMessageType == websocketJSONMessageType) {
|
|
return JSON.parse(theMessage);
|
|
} else {
|
|
return null; // invalid
|
|
}
|
|
}
|
|
|
|
private getWebsocketCustomEvent(websocketMessage: string): string {
|
|
if (websocketMessage.length < websocketMessagePrefixAndSepIdx) {
|
|
return "";
|
|
}
|
|
let s = websocketMessage.substring(websocketMessagePrefixAndSepIdx, websocketMessage.length);
|
|
let evt = s.substring(0, s.indexOf(websocketMessageSeparator));
|
|
|
|
return evt;
|
|
}
|
|
|
|
private getCustomMessage(event: string, websocketMessage: string): string {
|
|
let eventIdx = websocketMessage.indexOf(event + websocketMessageSeparator);
|
|
let s = websocketMessage.substring(eventIdx + event.length + websocketMessageSeparator.length+2, websocketMessage.length);
|
|
return s;
|
|
}
|
|
|
|
//
|
|
|
|
// Ws Events
|
|
|
|
// messageReceivedFromConn this is the func which decides
|
|
// if it's a native websocket message or a custom iris-ws message
|
|
// if native message then calls the fireNativeMessage
|
|
// else calls the fireMessage
|
|
//
|
|
// remember Iris gives you the freedom of native websocket messages if you don't want to use this client side at all.
|
|
private messageReceivedFromConn(evt: MessageEvent): void {
|
|
//check if iris-ws message
|
|
let message = <string>evt.data;
|
|
if (message.indexOf(websocketMessagePrefix) != -1) {
|
|
let event = this.getWebsocketCustomEvent(message);
|
|
if (event != "") {
|
|
// it's a custom message
|
|
this.fireMessage(event, this.getCustomMessage(event, message));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// it's a native websocket message
|
|
this.fireNativeMessage(message);
|
|
}
|
|
|
|
OnConnect(fn: onConnectFunc): void {
|
|
if (this.isReady) {
|
|
fn();
|
|
}
|
|
this.connectListeners.push(fn);
|
|
}
|
|
|
|
fireConnect(): void {
|
|
for (let i = 0; i < this.connectListeners.length; i++) {
|
|
this.connectListeners[i]();
|
|
}
|
|
}
|
|
|
|
OnDisconnect(fn: onWebsocketDisconnectFunc): void {
|
|
this.disconnectListeners.push(fn);
|
|
}
|
|
|
|
fireDisconnect(): void {
|
|
for (let i = 0; i < this.disconnectListeners.length; i++) {
|
|
this.disconnectListeners[i]();
|
|
}
|
|
}
|
|
|
|
OnMessage(cb: onWebsocketNativeMessageFunc): void {
|
|
this.nativeMessageListeners.push(cb);
|
|
}
|
|
|
|
fireNativeMessage(websocketMessage: string): void {
|
|
for (let i = 0; i < this.nativeMessageListeners.length; i++) {
|
|
this.nativeMessageListeners[i](websocketMessage);
|
|
}
|
|
}
|
|
|
|
On(event: string, cb: onMessageFunc): void {
|
|
if (this.messageListeners[event] == null || this.messageListeners[event] == undefined) {
|
|
this.messageListeners[event] = [];
|
|
}
|
|
this.messageListeners[event].push(cb);
|
|
}
|
|
|
|
fireMessage(event: string, message: any): void {
|
|
for (let key in this.messageListeners) {
|
|
if (this.messageListeners.hasOwnProperty(key)) {
|
|
if (key == event) {
|
|
for (let i = 0; i < this.messageListeners[key].length; i++) {
|
|
this.messageListeners[key][i](message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
|
|
// Ws Actions
|
|
|
|
Disconnect(): void {
|
|
this.conn.close();
|
|
}
|
|
|
|
// EmitMessage sends a native websocket message
|
|
EmitMessage(websocketMessage: string): void {
|
|
this.conn.send(websocketMessage);
|
|
}
|
|
|
|
// Emit sends an iris-custom websocket message
|
|
Emit(event: string, data: any): void {
|
|
let messageStr = this.encodeMessage(event, data);
|
|
this.EmitMessage(messageStr);
|
|
}
|
|
|
|
//
|
|
|
|
}
|
|
|
|
// node-modules export {Ws};
|
|
*/
|