mirror of
https://github.com/kataras/iris.git
synced 2026-01-05 11:17:03 +00:00
Publish the new version ✈️ | Look description please!
# FAQ ### Looking for free support? http://support.iris-go.com https://kataras.rocket.chat/channel/iris ### Looking for previous versions? https://github.com/kataras/iris#version ### Should I upgrade my Iris? Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready. > Iris uses the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature, so you get truly reproducible builds, as this method guards against upstream renames and deletes. **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. For further installation support, please click [here](http://support.iris-go.com/d/16-how-to-install-iris-web-framework). ### About our new home page http://iris-go.com Thanks to [Santosh Anand](https://github.com/santoshanand) the http://iris-go.com has been upgraded and it's really awesome! [Santosh](https://github.com/santoshanand) is a freelancer, he has a great knowledge of nodejs and express js, Android, iOS, React Native, Vue.js etc, if you need a developer to find or create a solution for your problem or task, please contact with him. The amount of the next two or three donations you'll send they will be immediately transferred to his own account balance, so be generous please! Read more at https://github.com/kataras/iris/blob/master/HISTORY.md Former-commit-id: eec2d71bbe011d6b48d2526eb25919e36e5ad94e
This commit is contained in:
52
websocket/LICENSE
Normal file
52
websocket/LICENSE
Normal file
@@ -0,0 +1,52 @@
|
||||
Copyright (c) 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Gerasimos Maropoulos nor the name of his
|
||||
username, kataras, may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Third-Parties:
|
||||
|
||||
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
223
websocket/client.go
Normal file
223
websocket/client.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------------------
|
||||
// ----------------Client side websocket javascript source which is typescript compiled
|
||||
// ------------------------------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
// ClientSource the client-side javascript raw source code
|
||||
var ClientSource = []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) {
|
||||
return typeof obj === 'object';
|
||||
};
|
||||
//
|
||||
// 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, javascript-side should contains an empty second parameter.");
|
||||
}
|
||||
return this._msg(event, t, m);
|
||||
};
|
||||
Ws.prototype.decodeMessage = function (event, websocketMessage) {
|
||||
//q-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 qws message
|
||||
// if native message then calls the fireNativeMessage
|
||||
// else calls the fireMessage
|
||||
//
|
||||
// remember q 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 qws 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 q-custom websocket message
|
||||
Ws.prototype.Emit = function (event, data) {
|
||||
var messageStr = this.encodeMessage(event, data);
|
||||
this.EmitMessage(messageStr);
|
||||
};
|
||||
return Ws;
|
||||
}());
|
||||
`)
|
||||
265
websocket/client.ts
Normal file
265
websocket/client.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// ----------------Client side websocket commented typescript source code --------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
// export to client.go:clientSource []byte
|
||||
|
||||
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 {
|
||||
return typeof obj === 'object';
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// 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, javascript-side should contains an empty second parameter.");
|
||||
}
|
||||
|
||||
return this._msg(event, t, m);
|
||||
}
|
||||
|
||||
private decodeMessage<T>(event: string, websocketMessage: string): T | any {
|
||||
//q-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 qws message
|
||||
// if native message then calls the fireNativeMessage
|
||||
// else calls the fireMessage
|
||||
//
|
||||
// remember q 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 qws 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 q-custom websocket message
|
||||
Emit(event: string, data: any): void {
|
||||
let messageStr = this.encodeMessage(event, data);
|
||||
this.EmitMessage(messageStr);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
}
|
||||
|
||||
// node-modules export {Ws};
|
||||
146
websocket/config.go
Normal file
146
websocket/config.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultWebsocketWriteTimeout 0, no timeout
|
||||
DefaultWebsocketWriteTimeout = 0
|
||||
// DefaultWebsocketReadTimeout 0, no timeout
|
||||
DefaultWebsocketReadTimeout = 0
|
||||
// DefaultWebsocketPongTimeout 60 * time.Second
|
||||
DefaultWebsocketPongTimeout = 60 * time.Second
|
||||
// DefaultWebsocketPingPeriod (DefaultPongTimeout * 9) / 10
|
||||
DefaultWebsocketPingPeriod = (DefaultWebsocketPongTimeout * 9) / 10
|
||||
// DefaultWebsocketMaxMessageSize 1024
|
||||
DefaultWebsocketMaxMessageSize = 1024
|
||||
// DefaultWebsocketReadBufferSize 4096
|
||||
DefaultWebsocketReadBufferSize = 4096
|
||||
// DefaultWebsocketWriterBufferSize 4096
|
||||
DefaultWebsocketWriterBufferSize = 4096
|
||||
// DefaultClientSourcePath "/iris-ws.js"
|
||||
DefaultClientSourcePath = "/iris-ws.js"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultIDGenerator returns the result of 64
|
||||
// random combined characters as the id of a new connection.
|
||||
// Used when config.IDGenerator is nil
|
||||
DefaultIDGenerator = func(context.Context) string { return randomString(64) }
|
||||
)
|
||||
|
||||
// Config the websocket server configuration
|
||||
// all of these are optional.
|
||||
type Config struct {
|
||||
// Endpoint is the path which the websocket server will listen for clients/connections
|
||||
// Default value is empty string, if you don't set it the Websocket server is disabled.
|
||||
Endpoint string
|
||||
// ClientSourcePath is is the path which the client-side
|
||||
// will be auto-served when the server adapted to an Iris station.
|
||||
// Default value is "/iris-ws.js"
|
||||
ClientSourcePath string
|
||||
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
|
||||
CheckOrigin func(r *http.Request) bool
|
||||
// WriteTimeout time allowed to write a message to the connection.
|
||||
// 0 means no timeout.
|
||||
// Default value is 0
|
||||
WriteTimeout time.Duration
|
||||
// ReadTimeout time allowed to read a message from the connection.
|
||||
// 0 means no timeout.
|
||||
// Default value is 0
|
||||
ReadTimeout time.Duration
|
||||
// PongTimeout allowed to read the next pong message from the connection.
|
||||
// Default value is 60 * time.Second
|
||||
PongTimeout time.Duration
|
||||
// PingPeriod send ping messages to the connection with this period. Must be less than PongTimeout.
|
||||
// Default value is 60 *time.Second
|
||||
PingPeriod time.Duration
|
||||
// MaxMessageSize max message size allowed from connection.
|
||||
// Default value is 1024
|
||||
MaxMessageSize int64
|
||||
// BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text
|
||||
// compatible if you wanna use the Connection's EmitMessage to send a custom binary data to the client, like a native server-client communication.
|
||||
// defaults to false
|
||||
BinaryMessages bool
|
||||
// ReadBufferSize is the buffer size for the underline reader
|
||||
// Default value is 4096
|
||||
ReadBufferSize int
|
||||
// WriteBufferSize is the buffer size for the underline writer
|
||||
// Default value is 4096
|
||||
WriteBufferSize int
|
||||
// IDGenerator used to create (and later on, set)
|
||||
// an ID for each incoming websocket connections (clients).
|
||||
// The request is an argument which you can use to generate the ID (from headers for example).
|
||||
// If empty then the ID is generated by DefaultIDGenerator: randomString(64)
|
||||
IDGenerator func(ctx context.Context) string
|
||||
|
||||
// Subprotocols specifies the server's supported protocols in order of
|
||||
// preference. If this field is set, then the Upgrade method negotiates a
|
||||
// subprotocol by selecting the first match in this list with a protocol
|
||||
// requested by the client.
|
||||
Subprotocols []string
|
||||
}
|
||||
|
||||
// Validate validates the configuration
|
||||
func (c Config) Validate() Config {
|
||||
|
||||
if c.ClientSourcePath == "" {
|
||||
c.ClientSourcePath = DefaultClientSourcePath
|
||||
}
|
||||
|
||||
// 0 means no timeout.
|
||||
if c.WriteTimeout < 0 {
|
||||
c.WriteTimeout = DefaultWebsocketWriteTimeout
|
||||
}
|
||||
|
||||
if c.ReadTimeout < 0 {
|
||||
c.ReadTimeout = DefaultWebsocketReadTimeout
|
||||
}
|
||||
|
||||
if c.PongTimeout < 0 {
|
||||
c.PongTimeout = DefaultWebsocketPongTimeout
|
||||
}
|
||||
|
||||
if c.PingPeriod <= 0 {
|
||||
c.PingPeriod = DefaultWebsocketPingPeriod
|
||||
}
|
||||
|
||||
if c.MaxMessageSize <= 0 {
|
||||
c.MaxMessageSize = DefaultWebsocketMaxMessageSize
|
||||
}
|
||||
|
||||
if c.ReadBufferSize <= 0 {
|
||||
c.ReadBufferSize = DefaultWebsocketReadBufferSize
|
||||
}
|
||||
|
||||
if c.WriteBufferSize <= 0 {
|
||||
c.WriteBufferSize = DefaultWebsocketWriterBufferSize
|
||||
}
|
||||
|
||||
if c.Error == nil {
|
||||
c.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
||||
//empty
|
||||
}
|
||||
}
|
||||
|
||||
if c.CheckOrigin == nil {
|
||||
c.CheckOrigin = func(r *http.Request) bool {
|
||||
// allow all connections by default
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if c.IDGenerator == nil {
|
||||
c.IDGenerator = DefaultIDGenerator
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
523
websocket/connection.go
Normal file
523
websocket/connection.go
Normal file
@@ -0,0 +1,523 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
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 {
|
||||
// SetWriteDeadline sets the write deadline on the underlying network
|
||||
// connection. After a write has timed out, the websocket state is corrupt and
|
||||
// all future writes will return an error. A zero value for t means writes will
|
||||
// not time out.
|
||||
SetWriteDeadline(t time.Time) error
|
||||
// SetReadDeadline sets the read deadline on the underlying network connection.
|
||||
// After a read has timed out, the websocket connection state is corrupt and
|
||||
// all future reads will return an error. A zero value for t means reads will
|
||||
// not time out.
|
||||
SetReadDeadline(t time.Time) error
|
||||
// SetReadLimit sets the maximum size for a message read from the peer. If a
|
||||
// message exceeds the limit, the connection sends a close frame to the peer
|
||||
// and returns ErrReadLimit to the application.
|
||||
SetReadLimit(limit int64)
|
||||
// SetPongHandler sets the handler for pong messages received from the peer.
|
||||
// The appData argument to h is the PONG frame application data. The default
|
||||
// pong handler does nothing.
|
||||
SetPongHandler(h func(appData string) error)
|
||||
// SetPingHandler sets the handler for ping messages received from the peer.
|
||||
// The appData argument to h is the PING frame application data. The default
|
||||
// ping handler sends a pong to the peer.
|
||||
SetPingHandler(h func(appData string) error)
|
||||
// WriteControl writes a control message with the given deadline. The allowed
|
||||
// message types are CloseMessage, PingMessage and PongMessage.
|
||||
WriteControl(messageType int, data []byte, deadline time.Time) error
|
||||
// WriteMessage is a helper method for getting a writer using NextWriter,
|
||||
// writing the message and closing the writer.
|
||||
WriteMessage(messageType int, data []byte) error
|
||||
// ReadMessage is a helper method for getting a reader using NextReader and
|
||||
// reading from that reader to a buffer.
|
||||
ReadMessage() (messageType int, p []byte, err error)
|
||||
// NextWriter returns a writer for the next message to send. The writer's Close
|
||||
// method flushes the complete message to the network.
|
||||
//
|
||||
// There can be at most one open writer on a connection. NextWriter closes the
|
||||
// previous writer if the application has not already done so.
|
||||
NextWriter(messageType int) (io.WriteCloser, error)
|
||||
// Close closes the underlying network connection without sending or waiting for a close frame.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------Connection implementation-----------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
NativeMessageFunc func([]byte)
|
||||
// MessageFunc is the second argument to the Emitter's Emit functions.
|
||||
// A callback which should receives one parameter of type string, int, bool or any valid JSON/Go struct
|
||||
MessageFunc interface{}
|
||||
// Connection is the front-end API that you will use to communicate with the client side
|
||||
Connection interface {
|
||||
// Emitter implements EmitMessage & Emit
|
||||
Emitter
|
||||
// ID returns the connection's identifier
|
||||
ID() string
|
||||
|
||||
// Context returns the (upgraded) context.Context of this connection
|
||||
// avoid using it, you normally don't need it,
|
||||
// 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() context.Context
|
||||
|
||||
// OnDisconnect registers a callback which fires when this connection is closed by an error or manual
|
||||
OnDisconnect(DisconnectFunc)
|
||||
// OnStatusCode registers a callback which fires when this connection occurs an error
|
||||
OnStatusCode(ErrorFunc)
|
||||
// FireStatusCode can be used to send a custom error message to the connection
|
||||
//
|
||||
// It does nothing more than firing the OnStatusCode listeners. It doesn't sends anything to the client.
|
||||
FireStatusCode(errorMessage string)
|
||||
// To defines where server should send a message
|
||||
// returns an emitter to send messages
|
||||
To(string) Emitter
|
||||
// OnMessage registers a callback which fires when native websocket message received
|
||||
OnMessage(NativeMessageFunc)
|
||||
// On registers a callback to a particular event which fires when a message to this event received
|
||||
On(string, MessageFunc)
|
||||
// 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
|
||||
// Returns true if the connection has actually left from the particular room.
|
||||
Leave(string) bool
|
||||
// OnLeave registers 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 theoretical 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
|
||||
// SetValue sets a key-value pair on the connection's mem store.
|
||||
SetValue(key string, value interface{})
|
||||
// GetValue gets a value by its key from the connection's mem store.
|
||||
GetValue(key string) interface{}
|
||||
// GetValueArrString gets a value as []string by its key from the connection's mem store.
|
||||
GetValueArrString(key string) []string
|
||||
// GetValueString gets a value as string by its key from the connection's mem store.
|
||||
GetValueString(key string) string
|
||||
// GetValueInt gets a value as integer by its key from the connection's mem store.
|
||||
GetValueInt(key string) int
|
||||
}
|
||||
|
||||
connection struct {
|
||||
underline UnderlineConnection
|
||||
id string
|
||||
messageType int
|
||||
pinger *time.Ticker
|
||||
disconnected bool
|
||||
onDisconnectListeners []DisconnectFunc
|
||||
onRoomLeaveListeners []LeaveRoomFunc
|
||||
onErrorListeners []ErrorFunc
|
||||
onNativeMessageListeners []NativeMessageFunc
|
||||
onEventListeners map[string][]MessageFunc
|
||||
// these were maden for performance only
|
||||
self Emitter // pre-defined emitter than sends message to its self client
|
||||
broadcast Emitter // pre-defined emitter that sends message to all except this
|
||||
all Emitter // pre-defined emitter which sends message to all clients
|
||||
|
||||
// access to the Context, use with causion, you can't use response writer as you imagine.
|
||||
ctx context.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.
|
||||
writerMu sync.Mutex
|
||||
// same exists for reader look here: https://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages
|
||||
// but we only use one reader in one goroutine, so we are safe.
|
||||
// readerMu sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
var _ Connection = &connection{}
|
||||
|
||||
func newConnection(ctx context.Context, s *server, underlineConn UnderlineConnection, id string) *connection {
|
||||
c := &connection{
|
||||
underline: underlineConn,
|
||||
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),
|
||||
ctx: ctx,
|
||||
server: s,
|
||||
}
|
||||
|
||||
if s.config.BinaryMessages {
|
||||
c.messageType = websocket.BinaryMessage
|
||||
}
|
||||
|
||||
c.self = newEmitter(c, c.id)
|
||||
c.broadcast = newEmitter(c, Broadcast)
|
||||
c.all = newEmitter(c, All)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// write writes a raw websocket message with a specific type to the client
|
||||
// used by ping messages and any CloseMessage types.
|
||||
func (c *connection) write(websocketMessageType int, data []byte) {
|
||||
// for any-case the app tries to write from different goroutines,
|
||||
// we must protect them because they're reporting that as bug...
|
||||
c.writerMu.Lock()
|
||||
if writeTimeout := c.server.config.WriteTimeout; writeTimeout > 0 {
|
||||
// set the write deadline based on the configuration
|
||||
c.underline.SetWriteDeadline(time.Now().Add(writeTimeout))
|
||||
}
|
||||
|
||||
// .WriteMessage same as NextWriter and close (flush)
|
||||
err := c.underline.WriteMessage(websocketMessageType, data)
|
||||
c.writerMu.Unlock()
|
||||
if err != nil {
|
||||
// if failed then the connection is off, fire the disconnect
|
||||
c.Disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// writeDefault is the same as write but the message type is the configured by c.messageType
|
||||
// if BinaryMessages is enabled then it's raw []byte as you expected to work with protobufs
|
||||
func (c *connection) writeDefault(data []byte) {
|
||||
c.write(c.messageType, data)
|
||||
}
|
||||
|
||||
const (
|
||||
// WriteWait is 1 second at the internal implementation,
|
||||
// same as here but this can be changed at the future*
|
||||
WriteWait = 1 * time.Second
|
||||
)
|
||||
|
||||
func (c *connection) startPinger() {
|
||||
|
||||
// this is the default internal handler, we just change the writeWait because of the actions we must do before
|
||||
// the server sends the ping-pong.
|
||||
|
||||
pingHandler := func(message string) error {
|
||||
err := c.underline.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(WriteWait))
|
||||
if err == websocket.ErrCloseSent {
|
||||
return nil
|
||||
} else if e, ok := err.(net.Error); ok && e.Temporary() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
c.underline.SetPingHandler(pingHandler)
|
||||
|
||||
// start a new timer ticker based on the configuration
|
||||
c.pinger = time.NewTicker(c.server.config.PingPeriod)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// wait for each tick
|
||||
<-c.pinger.C
|
||||
// try to ping the client, if failed then it disconnects
|
||||
c.write(websocket.PingMessage, []byte{})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *connection) startReader() {
|
||||
conn := c.underline
|
||||
hasReadTimeout := c.server.config.ReadTimeout > 0
|
||||
|
||||
conn.SetReadLimit(c.server.config.MaxMessageSize)
|
||||
conn.SetPongHandler(func(s string) error {
|
||||
if hasReadTimeout {
|
||||
conn.SetReadDeadline(time.Now().Add(c.server.config.ReadTimeout))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
defer func() {
|
||||
c.Disconnect()
|
||||
}()
|
||||
|
||||
for {
|
||||
if hasReadTimeout {
|
||||
// set the read deadline based on the configuration
|
||||
conn.SetReadDeadline(time.Now().Add(c.server.config.ReadTimeout))
|
||||
}
|
||||
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
c.FireStatusCode(err.Error())
|
||||
}
|
||||
break
|
||||
} else {
|
||||
c.messageReceived(data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// messageReceived checks the incoming message and fire the nativeMessage listeners or the event listeners (ws custom message)
|
||||
func (c *connection) messageReceived(data []byte) {
|
||||
|
||||
if bytes.HasPrefix(data, websocketMessagePrefixBytes) {
|
||||
customData := string(data)
|
||||
//it's a custom 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 server 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 *connection) ID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (c *connection) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func (c *connection) Values() ConnectionValues {
|
||||
return c.values
|
||||
}
|
||||
|
||||
func (c *connection) fireDisconnect() {
|
||||
for i := range c.onDisconnectListeners {
|
||||
c.onDisconnectListeners[i]()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) OnDisconnect(cb DisconnectFunc) {
|
||||
c.onDisconnectListeners = append(c.onDisconnectListeners, cb)
|
||||
}
|
||||
|
||||
func (c *connection) OnStatusCode(cb ErrorFunc) {
|
||||
c.onErrorListeners = append(c.onErrorListeners, cb)
|
||||
}
|
||||
|
||||
func (c *connection) FireStatusCode(errorMessage string) {
|
||||
for _, cb := range c.onErrorListeners {
|
||||
cb(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) To(to string) Emitter {
|
||||
if to == Broadcast { // if send to all except me, then return the pre-defined emitter, and so on
|
||||
return c.broadcast
|
||||
} else if to == All {
|
||||
return c.all
|
||||
} else if to == c.id {
|
||||
return c.self
|
||||
}
|
||||
// is an emitter to another client/connection
|
||||
return newEmitter(c, to)
|
||||
}
|
||||
|
||||
func (c *connection) EmitMessage(nativeMessage []byte) error {
|
||||
return c.self.EmitMessage(nativeMessage)
|
||||
}
|
||||
|
||||
func (c *connection) Emit(event string, message interface{}) error {
|
||||
return c.self.Emit(event, message)
|
||||
}
|
||||
|
||||
func (c *connection) OnMessage(cb NativeMessageFunc) {
|
||||
c.onNativeMessageListeners = append(c.onNativeMessageListeners, cb)
|
||||
}
|
||||
|
||||
func (c *connection) On(event string, cb MessageFunc) {
|
||||
if c.onEventListeners[event] == nil {
|
||||
c.onEventListeners[event] = make([]MessageFunc, 0)
|
||||
}
|
||||
|
||||
c.onEventListeners[event] = append(c.onEventListeners[event], cb)
|
||||
}
|
||||
|
||||
func (c *connection) Join(roomName string) {
|
||||
c.server.Join(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 {
|
||||
return c.server.Disconnect(c.ID())
|
||||
}
|
||||
|
||||
// mem per-conn store
|
||||
|
||||
func (c *connection) SetValue(key string, value interface{}) {
|
||||
c.values.Set(key, value)
|
||||
}
|
||||
|
||||
func (c *connection) GetValue(key string) interface{} {
|
||||
return c.values.Get(key)
|
||||
}
|
||||
|
||||
func (c *connection) GetValueArrString(key string) []string {
|
||||
if v := c.values.Get(key); v != nil {
|
||||
if arrString, ok := v.([]string); ok {
|
||||
return arrString
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *connection) GetValueString(key string) string {
|
||||
if v := c.values.Get(key); v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *connection) GetValueInt(key string) int {
|
||||
if v := c.values.Get(key); v != nil {
|
||||
if i, ok := v.(int); ok {
|
||||
return i
|
||||
} else if s, ok := v.(string); ok {
|
||||
if iv, err := strconv.Atoi(s); err == nil {
|
||||
return iv
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
53
websocket/emitter.go
Normal file
53
websocket/emitter.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// --------------------------------Emitter implementation-------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
// All is the string which the Emitter use to send a message to all
|
||||
All = ""
|
||||
// Broadcast is the string which the Emitter use to send a message to all except this connection
|
||||
Broadcast = ";gowebsocket;to;all;except;me;"
|
||||
)
|
||||
|
||||
type (
|
||||
// Emitter is the message/or/event manager
|
||||
Emitter interface {
|
||||
// EmitMessage sends a native websocket message
|
||||
EmitMessage([]byte) error
|
||||
// Emit sends a message on a particular event
|
||||
Emit(string, interface{}) error
|
||||
}
|
||||
|
||||
emitter struct {
|
||||
conn *connection
|
||||
to string
|
||||
}
|
||||
)
|
||||
|
||||
var _ Emitter = &emitter{}
|
||||
|
||||
func newEmitter(c *connection, to string) *emitter {
|
||||
return &emitter{conn: c, to: to}
|
||||
}
|
||||
|
||||
func (e *emitter) EmitMessage(nativeMessage []byte) error {
|
||||
e.conn.server.emitMessage(e.conn.id, e.to, nativeMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emitter) Emit(event string, data interface{}) error {
|
||||
message, err := websocketMessageSerialize(event, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.EmitMessage([]byte(message))
|
||||
return nil
|
||||
}
|
||||
193
websocket/message.go
Normal file
193
websocket/message.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/core/errors"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -----------------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 = bytebufferpool.Pool{}
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
var errInvalidTypeMessage = errors.New("Type %s is invalid for message: %s")
|
||||
|
||||
// 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, errInvalidTypeMessage.Format(_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
|
||||
}
|
||||
|
||||
const (
|
||||
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
// random takes a parameter (int) and returns random slice of byte
|
||||
// ex: var randomstrbytes []byte; randomstrbytes = utils.Random(32)
|
||||
func random(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = src.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
b[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// randomString accepts a number(10 for example) and returns a random string using simple but fairly safe random algorithm
|
||||
func randomString(n int) string {
|
||||
return string(random(n))
|
||||
}
|
||||
433
websocket/server.go
Normal file
433
websocket/server.go
Normal file
@@ -0,0 +1,433 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
// Server is the websocket server,
|
||||
// listens on the config's port, the critical part is the event OnConnection
|
||||
type Server interface {
|
||||
// Attach adapts the websocket server to an Iris instance.
|
||||
// see websocket.go
|
||||
Attach(app *iris.Application)
|
||||
|
||||
// Handler returns the iris.Handler
|
||||
// which is setted to the 'Websocket Endpoint path',
|
||||
// the client should target to this handler's developer's custom path
|
||||
// ex: app.Any("/myendpoint", mywebsocket.Handler())
|
||||
Handler() context.Handler
|
||||
|
||||
// OnConnection this is the main event you, as developer, will work with each of the websocket connections
|
||||
OnConnection(cb ConnectionFunc)
|
||||
|
||||
/*
|
||||
connection actions, same as the connection's method,
|
||||
but these methods accept the connection ID,
|
||||
which is useful when the developer maps
|
||||
this id with a database field (using config.IDGenerator).
|
||||
*/
|
||||
|
||||
// IsConnected returns true if the connection with that ID is connected to the server
|
||||
// useful when you have defined a custom connection id generator (based on a database)
|
||||
// and you want to check if that connection is already connected (on multiple tabs)
|
||||
IsConnected(connID string) bool
|
||||
|
||||
// Join joins a websocket client to a room,
|
||||
// first parameter is the room name and the second the connection.ID()
|
||||
//
|
||||
// You can use connection.Join("room name") instead.
|
||||
Join(roomName string, connID string)
|
||||
|
||||
// LeaveAll kicks out a connection from ALL of its joined rooms
|
||||
LeaveAll(connID string)
|
||||
|
||||
// Leave leaves a websocket client from a room,
|
||||
// first parameter is the room name and the second the connection.ID()
|
||||
//
|
||||
// You can use connection.Leave("room name") instead.
|
||||
// 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()
|
||||
// What it does?
|
||||
// 1. remove the connection from the list
|
||||
// 2. leave from all joined rooms
|
||||
// 3. fire the disconnect callbacks, if any
|
||||
// 4. close the underline connection and return its error, if any.
|
||||
//
|
||||
// You can use the connection.Disconnect() instead.
|
||||
Disconnect(connID string) error
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// --------------------------------Connection key-based list----------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
type connectionKV struct {
|
||||
key string // the connection ID
|
||||
value *connection
|
||||
}
|
||||
|
||||
type connections []connectionKV
|
||||
|
||||
func (cs *connections) add(key string, value *connection) {
|
||||
args := *cs
|
||||
n := len(args)
|
||||
// check if already id/key exist, if yes replace the conn
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &args[i]
|
||||
if kv.key == key {
|
||||
kv.value = value
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c := cap(args)
|
||||
// make the connections slice bigger and put the conn
|
||||
if c > n {
|
||||
args = args[:n+1]
|
||||
kv := &args[n]
|
||||
kv.key = key
|
||||
kv.value = value
|
||||
*cs = args
|
||||
return
|
||||
}
|
||||
// append to the connections slice and put the conn
|
||||
kv := connectionKV{}
|
||||
kv.key = key
|
||||
kv.value = value
|
||||
*cs = append(args, kv)
|
||||
}
|
||||
|
||||
func (cs *connections) get(key string) *connection {
|
||||
args := *cs
|
||||
n := len(args)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &args[i]
|
||||
if kv.key == key {
|
||||
return kv.value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// returns the connection which removed and a bool value of found or not
|
||||
// the connection is useful to fire the disconnect events, we use that form in order to
|
||||
// make work things faster without the need of get-remove, just -remove should do the job.
|
||||
func (cs *connections) remove(key string) (*connection, bool) {
|
||||
args := *cs
|
||||
n := len(args)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &args[i]
|
||||
if kv.key == key {
|
||||
conn := kv.value
|
||||
// we found the index,
|
||||
// let's remove the item by appending to the temp and
|
||||
// after set the pointer of the slice to this temp args
|
||||
args = append(args[:i], args[i+1:]...)
|
||||
*cs = args
|
||||
return conn, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// --------------------------------Server implementation--------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
type (
|
||||
// ConnectionFunc is the callback which fires when a client/connection is connected to the server.
|
||||
// Receives one parameter which is the Connection
|
||||
ConnectionFunc func(Connection)
|
||||
|
||||
// websocketRoomPayload is used as payload from the connection to the server
|
||||
websocketRoomPayload struct {
|
||||
roomName string
|
||||
connectionID string
|
||||
}
|
||||
|
||||
// payloads, connection -> server
|
||||
websocketMessagePayload struct {
|
||||
from string
|
||||
to string
|
||||
data []byte
|
||||
}
|
||||
|
||||
server struct {
|
||||
config Config
|
||||
connections connections
|
||||
rooms map[string][]string // by default a connection is joined to a room which has the connection id as its name
|
||||
mu sync.Mutex // for rooms
|
||||
onConnectionListeners []ConnectionFunc
|
||||
//connectionPool *sync.Pool // sadly I can't make this because the websocket connection is live until is closed.
|
||||
}
|
||||
)
|
||||
|
||||
var _ Server = &server{}
|
||||
|
||||
// server implementation
|
||||
|
||||
func (s *server) Handler() context.Handler {
|
||||
// build the upgrader once
|
||||
c := s.config
|
||||
|
||||
upgrader := websocket.Upgrader{
|
||||
ReadBufferSize: c.ReadBufferSize,
|
||||
WriteBufferSize: c.WriteBufferSize,
|
||||
Error: c.Error,
|
||||
CheckOrigin: c.CheckOrigin,
|
||||
Subprotocols: c.Subprotocols,
|
||||
}
|
||||
return func(ctx context.Context) {
|
||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
||||
//
|
||||
// The responseHeader is included in the response to the client's upgrade
|
||||
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
|
||||
// application negotiated subprotocol (Sec--Protocol).
|
||||
//
|
||||
// If the upgrade fails, then Upgrade replies to the client with an HTTP error
|
||||
// response.
|
||||
conn, err := upgrader.Upgrade(ctx.ResponseWriter(), ctx.Request(), ctx.ResponseWriter().Header())
|
||||
if err != nil {
|
||||
ctx.Application().Log("websocket error: %v", err)
|
||||
ctx.StatusCode(iris.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
s.handleConnection(ctx, conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection creates & starts to listening to a new connection
|
||||
func (s *server) handleConnection(ctx context.Context, websocketConn UnderlineConnection) {
|
||||
// use the config's id generator (or the default) to create a websocket client/connection id
|
||||
cid := s.config.IDGenerator(ctx)
|
||||
// create the new connection
|
||||
c := newConnection(ctx, s, websocketConn, cid)
|
||||
// add the connection to the server's list
|
||||
s.connections.add(cid, c)
|
||||
|
||||
// join to itself
|
||||
s.Join(c.ID(), c.ID())
|
||||
|
||||
// NOTE TO ME: fire these first BEFORE startReader and startPinger
|
||||
// in order to set the events and any messages to send
|
||||
// the startPinger will send the OK to the client and only
|
||||
// then the client is able to send and receive from server
|
||||
// when all things are ready and only then. DO NOT change this order.
|
||||
|
||||
// fire the on connection event callbacks, if any
|
||||
for i := range s.onConnectionListeners {
|
||||
s.onConnectionListeners[i](c)
|
||||
}
|
||||
|
||||
// start the ping
|
||||
c.startPinger()
|
||||
|
||||
// start the messages reader
|
||||
c.startReader()
|
||||
}
|
||||
|
||||
/* Notes:
|
||||
We use the id as the signature of the connection because with the custom IDGenerator
|
||||
the developer can share this ID with a database field, so we want to give the oportunnity to handle
|
||||
his/her websocket connections without even use the connection itself.
|
||||
|
||||
Another question may be:
|
||||
Q: Why you use server as the main actioner for all of the connection actions?
|
||||
For example the server.Disconnect(connID) manages the connection internal fields, is this code-style correct?
|
||||
A: It's the correct code-style for these type of applications and libraries, server manages all, the connnection's functions
|
||||
should just do some internal checks (if needed) and push the action to its parent, which is the server, the server is able to
|
||||
remove a connection, the rooms of its connected and all these things, so in order to not split the logic, we have the main logic
|
||||
here, in the server, and let the connection with some exported functions whose exists for the per-connection action user's code-style.
|
||||
|
||||
Ok my english are s** I can feel it, but these comments are mostly for me.
|
||||
*/
|
||||
|
||||
// OnConnection this is the main event you, as developer, will work with each of the websocket connections
|
||||
func (s *server) OnConnection(cb ConnectionFunc) {
|
||||
s.onConnectionListeners = append(s.onConnectionListeners, cb)
|
||||
}
|
||||
|
||||
// IsConnected returns true if the connection with that ID is connected to the server
|
||||
// useful when you have defined a custom connection id generator (based on a database)
|
||||
// and you want to check if that connection is already connected (on multiple tabs)
|
||||
func (s *server) IsConnected(connID string) bool {
|
||||
c := s.connections.get(connID)
|
||||
return c != nil
|
||||
}
|
||||
|
||||
// Join joins a websocket client to a room,
|
||||
// first parameter is the room name and the second the connection.ID()
|
||||
//
|
||||
// You can use connection.Join("room name") instead.
|
||||
func (s *server) Join(roomName string, connID string) {
|
||||
s.mu.Lock()
|
||||
s.join(roomName, connID)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// join used internally, no locks used.
|
||||
func (s *server) join(roomName string, connID string) {
|
||||
if s.rooms[roomName] == nil {
|
||||
s.rooms[roomName] = make([]string, 0)
|
||||
}
|
||||
s.rooms[roomName] = append(s.rooms[roomName], connID)
|
||||
}
|
||||
|
||||
// LeaveAll kicks out a connection from ALL of its joined rooms
|
||||
func (s *server) LeaveAll(connID string) {
|
||||
s.mu.Lock()
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Leave leaves a websocket client from a room,
|
||||
// first parameter is the room name and the second the connection.ID()
|
||||
//
|
||||
// You can use connection.Leave("room name") instead.
|
||||
// Returns true if the connection has actually left from the particular room.
|
||||
func (s *server) Leave(roomName string, connID string) bool {
|
||||
s.mu.Lock()
|
||||
left := s.leave(roomName, connID)
|
||||
s.mu.Unlock()
|
||||
return left
|
||||
}
|
||||
|
||||
// leave used internally, no locks used.
|
||||
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 {
|
||||
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]
|
||||
left = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(s.rooms[roomName]) == 0 { // if room is empty then delete it
|
||||
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
|
||||
// this is the main function which writes the RAW websocket messages to the client.
|
||||
// It sends them(messages) to the correct room (self, broadcast or to specific client)
|
||||
//
|
||||
// You don't have to use this generic method, exists only for extreme
|
||||
// apps which you have an external goroutine with a list of custom connection list.
|
||||
//
|
||||
// You SHOULD use connection.EmitMessage/Emit/To().Emit/EmitMessage instead.
|
||||
// let's keep it unexported for the best.
|
||||
func (s *server) emitMessage(from, to string, data []byte) {
|
||||
if to != All && to != Broadcast && s.rooms[to] != nil {
|
||||
// it suppose to send the message to a specific room/or a user inside its own room
|
||||
for _, connectionIDInsideRoom := range s.rooms[to] {
|
||||
if c := s.connections.get(connectionIDInsideRoom); c != nil {
|
||||
c.writeDefault(data) //send the message to the client(s)
|
||||
} else {
|
||||
// the connection is not connected but it's inside the room, we remove it on disconnect but for ANY CASE:
|
||||
cid := connectionIDInsideRoom
|
||||
if c != nil {
|
||||
cid = c.id
|
||||
}
|
||||
s.Leave(cid, to)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// it suppose to send the message to all opened connections or to all except the sender
|
||||
for _, cKV := range s.connections {
|
||||
connID := cKV.key
|
||||
if to != All && to != connID { // if it's not suppose to send to all connections (including itself)
|
||||
if to == Broadcast && from == connID { // if broadcast to other connections except this
|
||||
continue //here we do the opossite of previous block,
|
||||
// just skip this connection when it's suppose to send the message to all connections except the sender
|
||||
}
|
||||
|
||||
}
|
||||
// send to the client(s) when the top validators passed
|
||||
cKV.value.writeDefault(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect force-disconnects a websocket connection based on its connection.ID()
|
||||
// What it does?
|
||||
// 1. remove the connection from the list
|
||||
// 2. leave from all joined rooms
|
||||
// 3. fire the disconnect callbacks, if any
|
||||
// 4. close the underline connection and return its error, if any.
|
||||
//
|
||||
// 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()
|
||||
|
||||
// fire the disconnect callbacks, if any
|
||||
c.fireDisconnect()
|
||||
// close the underline connection and return its error, if any.
|
||||
err = c.underline.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
63
websocket/websocket.go
Normal file
63
websocket/websocket.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
)
|
||||
|
||||
// New returns a new websocket server policy adaptor.
|
||||
func New(cfg Config) Server {
|
||||
return &server{
|
||||
config: cfg.Validate(),
|
||||
rooms: make(map[string][]string, 0),
|
||||
onConnectionListeners: make([]ConnectionFunc, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func fixPath(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if s[0] != '/' {
|
||||
s = "/" + s
|
||||
}
|
||||
|
||||
s = strings.Replace(s, "//", "/", -1)
|
||||
return s
|
||||
}
|
||||
|
||||
// Attach adapts the websocket server to one or more Iris instances.
|
||||
func (s *server) Attach(app *iris.Application) {
|
||||
wsPath := fixPath(s.config.Endpoint)
|
||||
if wsPath == "" {
|
||||
app.Log("websocket's configuration field 'Endpoint' cannot be empty, websocket server stops")
|
||||
return
|
||||
}
|
||||
|
||||
wsClientSidePath := fixPath(s.config.ClientSourcePath)
|
||||
if wsClientSidePath == "" {
|
||||
app.Log("websocket's configuration field 'ClientSourcePath' cannot be empty, websocket server stops")
|
||||
return
|
||||
}
|
||||
|
||||
// set the routing for client-side source (javascript) (optional)
|
||||
clientSideLookupName := "iris-websocket-client-side"
|
||||
wsHandler := s.Handler()
|
||||
app.Get(wsPath, wsHandler)
|
||||
// check if client side doesn't already exists
|
||||
if app.GetRoute(clientSideLookupName) == nil {
|
||||
// serve the client side on domain:port/iris-ws.js
|
||||
r, err := app.StaticContent(wsClientSidePath, "application/javascript", ClientSource)
|
||||
if err != nil {
|
||||
app.Log("websocket's route for javascript client-side library failed with: %v", err)
|
||||
return
|
||||
}
|
||||
r.Name = clientSideLookupName
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user