1
0
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:
kataras
2017-06-03 23:22:52 +03:00
parent 03bcadadec
commit 5e4b63acb2
330 changed files with 35786 additions and 17316 deletions

52
websocket/LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}