Implement the websocket adaptor, a version of kataras/go-websocket, and refactor all of the previous websocket examples.

https://github.com/kataras/go-websocket/issues/27

Former-commit-id: 0b7e52e0a61150a8bba973ef653986d8b3ddd26b
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-02-15 08:40:43 +02:00
parent 26a5f9f3b0
commit 82afcc5aa6
30 changed files with 2692 additions and 330 deletions

View File

@ -37,6 +37,7 @@ Changes:
- Remove `context.RenderTemplateSource` you should make a new template file and use the `iris.Render` to specify an `io.Writer` like `bytes.Buffer` - Remove `context.RenderTemplateSource` you should make a new template file and use the `iris.Render` to specify an `io.Writer` like `bytes.Buffer`
- Remove `plugins`, replaced with more pluggable echosystem that I designed from zero on this release, named `Policy` [Adaptors](https://github.com/kataras/iris/tree/master/adaptors) (all plugins have been converted, fixed and improvement, except the iriscontrol). - Remove `plugins`, replaced with more pluggable echosystem that I designed from zero on this release, named `Policy` [Adaptors](https://github.com/kataras/iris/tree/master/adaptors) (all plugins have been converted, fixed and improvement, except the iriscontrol).
- `context.Log(string,...interface{})` -> `context.Log(iris.LogMode, string)` - `context.Log(string,...interface{})` -> `context.Log(iris.LogMode, string)`
- Remove `.Config.Websocket` , replaced with the `kataras/iris/adaptors/websocket.Config` adaptor.
- https://github.com/iris-contrib/plugin -> https://github.com/iris-contrib/adaptors - https://github.com/iris-contrib/plugin -> https://github.com/iris-contrib/adaptors
@ -859,18 +860,208 @@ editors worked before but I couldn't let some developers without support.
### Websockets ### Websockets
There are many internal improvements to the [websocket server](https://github.com/kataras/go-websocket), and it's There are many internal improvements to the websocket server, it
operating slighty faster. operates slighty faster to.
The kataras/go-websocket library, which `app.OnConnection` is refering to, will not be changed, its API will still remain.
I am not putting anything new there (I doubt if any bug is available to fix, it's very simple and it just works).
I started the kataras/go-websocket back then because I wanted a simple and fast websocket server for Websocket is an Adaptor too and you can edit more configuration fields than before.
the fasthttp iris' version and back then no one did that before. No Write and Read timeout by default, you have to set the fields if you want to enable timeout.
Now(after v6) iris is compatible with any net/http websocket library that already created by third-parties.
If the iris' websocket feature does not cover your app's needs, you can simple use any other Below you'll see the before and the after, keep note that the static and templates didn't changed, so I am not putting the whole
library for websockets, like the Golang's compatible to `socket.io`, example: html and javascript sources here, you can run the full examples from [here](https://github.com/kataras/iris/tree/6.2/adaptors/websocket/_examples).
**BEFORE:***
```go
package main
import (
"fmt" // optional
"github.com/kataras/iris"
)
type clientPage struct {
Title string
Host string
}
func main() {
iris.StaticWeb("/js", "./static/js")
iris.Get("/", func(ctx *iris.Context) {
ctx.Render("client.html", clientPage{"Client Page", ctx.Host()})
})
// the path which the websocket client should listen/registed to ->
iris.Config.Websocket.Endpoint = "/my_endpoint"
// by-default all origins are accepted, you can change this behavior by setting:
// iris.Config.Websocket.CheckOrigin
var myChatRoom = "room1"
iris.Websocket.OnConnection(func(c iris.WebsocketConnection) {
// Request returns the (upgraded) *http.Request 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.
// httpRequest := c.Request()
// fmt.Printf("Headers for the connection with ID: %s\n\n", c.ID())
// for k, v := range httpRequest.Header {
// fmt.Printf("%s = '%s'\n", k, strings.Join(v, ", "))
// }
// join to a room (optional)
c.Join(myChatRoom)
c.On("chat", func(message string) {
if message == "leave" {
c.Leave(myChatRoom)
c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.")
c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.")
return
}
// to all except this connection ->
// c.To(iris.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message)
// to all connected clients: c.To(iris.All)
// to the client itself ->
//c.Emit("chat", "Message from myself: "+message)
//send the message to the whole room,
//all connections are inside this room will receive this message
c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message)
})
// or create a new leave event
// c.On("leave", func() {
// c.Leave(myChatRoom)
// })
c.OnDisconnect(func() {
fmt.Printf("Connection with ID: %s has been disconnected!\n", c.ID())
})
})
iris.Listen(":8080")
}
```
**AFTER**
```go
package main
import (
"fmt" // optional
"gopkg.in/kataras/iris.v6"
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
"gopkg.in/kataras/iris.v6/adaptors/view"
"gopkg.in/kataras/iris.v6/adaptors/websocket"
)
type clientPage struct {
Title string
Host string
}
func main() {
app := iris.New()
app.Adapt(iris.DevLogger()) // enable all (error) logs
app.Adapt(httprouter.New()) // select the httprouter as the servemux
app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates
ws := websocket.New(websocket.Config{
// the path which the websocket client should listen/registed to,
Endpoint: "/my_endpoint",
// the client-side javascript static file path
// which will be served by Iris.
// default is /iris-ws.js
// if you change that you have to change the bottom of templates/client.html
// script tag:
ClientSourcePath: "/iris-ws.js",
//
// Set the timeouts, 0 means no timeout
// websocket has more configuration, go to ../../config.go for more:
// WriteTimeout: 0,
// ReadTimeout: 0,
// by-default all origins are accepted, you can change this behavior by setting:
// CheckOrigin: (r *http.Request ) bool {},
//
//
// 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 *iris.Context) string {},
})
app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint
app.StaticWeb("/js", "./static/js") // serve our custom javascript code
app.Get("/", func(ctx *iris.Context) {
ctx.Render("client.html", clientPage{"Client Page", ctx.Host()})
})
var myChatRoom = "room1"
ws.OnConnection(func(c websocket.Connection) {
// Context returns the (upgraded) *iris.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.
// ctx := c.Context()
// join to a room (optional)
c.Join(myChatRoom)
c.On("chat", func(message string) {
if message == "leave" {
c.Leave(myChatRoom)
c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.")
c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.")
return
}
// to all except this connection ->
// c.To(websocket.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message)
// to all connected clients: c.To(websocket.All)
// to the client itself ->
//c.Emit("chat", "Message from myself: "+message)
//send the message to the whole room,
//all connections are inside this room will receive this message
c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message)
})
// or create a new leave event
// c.On("leave", func() {
// c.Leave(myChatRoom)
// })
c.OnDisconnect(func() {
fmt.Printf("Connection with ID: %s has been disconnected!\n", c.ID())
})
})
app.Listen(":8080")
}
```
If the iris' websocket feature does not cover your app's needs, you can simply use any other
library for websockets that you used to use, like the Golang's compatible to `socket.io`, simple example:
```go ```go
package main package main

View File

@ -68,30 +68,30 @@ type (
var ( var (
errMuxEntryConflictsWildcard = errors.New(` errMuxEntryConflictsWildcard = errors.New(`
Router: '%s' in new path '%s' httprouter: '%s' in new path '%s'
conflicts with existing wildcarded route with path: '%s' conflicts with existing wildcarded route with path: '%s'
in existing prefix of'%s' `) in existing prefix of'%s' `)
errMuxEntryMiddlewareAlreadyExists = errors.New(` errMuxEntryMiddlewareAlreadyExists = errors.New(`
Router: Middleware were already registered for the path: '%s'`) httprouter: Middleware were already registered for the path: '%s'`)
errMuxEntryInvalidWildcard = errors.New(` errMuxEntryInvalidWildcard = errors.New(`
Router: More than one wildcard found in the path part: '%s' in route's path: '%s'`) httprouter: More than one wildcard found in the path part: '%s' in route's path: '%s'`)
errMuxEntryConflictsExistingWildcard = errors.New(` errMuxEntryConflictsExistingWildcard = errors.New(`
Router: Wildcard for route path: '%s' conflicts with existing children in route path: '%s'`) httprouter: Wildcard for route path: '%s' conflicts with existing children in route path: '%s'`)
errMuxEntryWildcardUnnamed = errors.New(` errMuxEntryWildcardUnnamed = errors.New(`
Router: Unnamed wildcard found in path: '%s'`) httprouter: Unnamed wildcard found in path: '%s'`)
errMuxEntryWildcardInvalidPlace = errors.New(` errMuxEntryWildcardInvalidPlace = errors.New(`
Router: Wildcard is only allowed at the end of the path, in the route path: '%s'`) httprouter: Wildcard is only allowed at the end of the path, in the route path: '%s'`)
errMuxEntryWildcardConflictsMiddleware = errors.New(` errMuxEntryWildcardConflictsMiddleware = errors.New(`
Router: Wildcard conflicts with existing middleware for the route path: '%s'`) httprouter: Wildcard conflicts with existing middleware for the route path: '%s'`)
errMuxEntryWildcardMissingSlash = errors.New(` errMuxEntryWildcardMissingSlash = errors.New(`
Router: No slash(/) were found before wildcard in the route path: '%s'`) httprouter: No slash(/) were found before wildcard in the route path: '%s'`)
) )
// getParamsLen returns the parameters length from a given path // getParamsLen returns the parameters length from a given path
@ -575,7 +575,7 @@ func New() iris.Policies {
// while ProdMode means that the iris should not continue running // while ProdMode means that the iris should not continue running
// by-default it panics on these errors, but to make sure let's introduce the fatalErr to stop visiting // by-default it panics on these errors, but to make sure let's introduce the fatalErr to stop visiting
fatalErr = true fatalErr = true
logger(iris.ProdMode, "fatal error on httprouter build adaptor: "+err.Error()) logger(iris.ProdMode, err.Error())
return return
} }

21
adaptors/view/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2017 Gerasimos Maropoulos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,44 @@
The MIT License (MIT)
Copyright (c) 2016-2017 Gerasimos Maropoulos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
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.

View File

@ -0,0 +1,99 @@
package main
import (
"fmt" // optional
"gopkg.in/kataras/iris.v6"
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
"gopkg.in/kataras/iris.v6/adaptors/view"
"gopkg.in/kataras/iris.v6/adaptors/websocket"
)
type clientPage struct {
Title string
Host string
}
func main() {
app := iris.New()
app.Adapt(iris.DevLogger()) // enable all (error) logs
app.Adapt(httprouter.New()) // select the httprouter as the servemux
app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates
ws := websocket.New(websocket.Config{
// the path which the websocket client should listen/registed to,
Endpoint: "/my_endpoint",
// the client-side javascript static file path
// which will be served by Iris.
// default is /iris-ws.js
// if you change that you have to change the bottom of templates/client.html
// script tag:
ClientSourcePath: "/iris-ws.js",
//
// Set the timeouts, 0 means no timeout
// websocket has more configuration, go to ../../config.go for more:
// WriteTimeout: 0,
// ReadTimeout: 0,
// by-default all origins are accepted, you can change this behavior by setting:
// CheckOrigin: (r *http.Request ) bool {},
//
//
// 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 *iris.Context) string {},
})
app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint
app.StaticWeb("/js", "./static/js") // serve our custom javascript code
app.Get("/", func(ctx *iris.Context) {
ctx.Render("client.html", clientPage{"Client Page", ctx.Host()})
})
var myChatRoom = "room1"
ws.OnConnection(func(c websocket.Connection) {
// Context returns the (upgraded) *iris.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.
// ctx := c.Context()
// join to a room (optional)
c.Join(myChatRoom)
c.On("chat", func(message string) {
if message == "leave" {
c.Leave(myChatRoom)
c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.")
c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.")
return
}
// to all except this connection ->
// c.To(websocket.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message)
// to all connected clients: c.To(websocket.All)
// to the client itself ->
//c.Emit("chat", "Message from myself: "+message)
//send the message to the whole room,
//all connections are inside this room will receive this message
c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message)
})
// or create a new leave event
// c.On("leave", func() {
// c.Leave(myChatRoom)
// })
c.OnDisconnect(func() {
fmt.Printf("Connection with ID: %s has been disconnected!\n", c.ID())
})
})
app.Listen(":8080")
}

View File

@ -0,0 +1,38 @@
var messageTxt;
var messages;
$(function () {
messageTxt = $("#messageTxt");
messages = $("#messages");
w = new Ws("ws://" + HOST + "/my_endpoint");
w.OnConnect(function () {
console.log("Websocket connection established");
});
w.OnDisconnect(function () {
appendMessage($("<div><center><h3>Disconnected</h3></center></div>"));
});
w.On("chat", function (message) {
appendMessage($("<div>" + message + "</div>"));
});
$("#sendBtn").click(function () {
w.Emit("chat", messageTxt.val().toString());
messageTxt.val("");
});
})
function appendMessage(messageDiv) {
var theDiv = messages[0];
var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight;
messageDiv.appendTo(messages);
if (doScroll) {
theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight;
}
}

View File

@ -0,0 +1,24 @@
<html>
<head>
<title>{{ .Title}}</title>
</head>
<body>
<div id="messages"
style="border-width: 1px; border-style: solid; height: 400px; width: 375px;">
</div>
<input type="text" id="messageTxt" />
<button type="button" id="sendBtn">Send</button>
<script type="text/javascript">
var HOST = {{.Host}}
</script>
<script src="js/vendor/jquery-2.2.3.min.js" type="text/javascript"></script>
<!-- This is auto-serving by the Iris, you don't need to have this file in your disk-->
<script src="/iris-ws.js" type="text/javascript"></script>
<!-- -->
<script src="js/chat.js" type="text/javascript"></script>
</body>
</html>

View File

@ -0,0 +1,113 @@
package main
import (
"fmt"
"sync"
"time"
"gopkg.in/kataras/iris.v6"
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
"gopkg.in/kataras/iris.v6/adaptors/view"
"gopkg.in/kataras/iris.v6/adaptors/websocket"
)
type clientPage struct {
Title string
Host string
}
func main() {
app := iris.New()
app.Adapt(iris.DevLogger()) // enable all (error) logs
app.Adapt(httprouter.New()) // select the httprouter as the servemux
app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates
ws := websocket.New(websocket.Config{
// the path which the websocket client should listen/registed to,
Endpoint: "/my_endpoint",
// the client-side javascript static file path
// which will be served by Iris.
// default is /iris-ws.js
// if you change that you have to change the bottom of templates/client.html
// script tag:
ClientSourcePath: "/iris-ws.js",
//
// Set the timeouts, 0 means no timeout
// websocket has more configuration, go to ../../config.go for more:
// WriteTimeout: 0,
// ReadTimeout: 0,
// by-default all origins are accepted, you can change this behavior by setting:
// CheckOrigin: (r *http.Request ) bool {},
//
//
// 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 *iris.Context) string {},
})
app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint
app.StaticWeb("/js", "./static/js") // serve our custom javascript code
app.Get("/", func(ctx *iris.Context) {
ctx.Render("client.html", clientPage{"Client Page", ctx.ServerHost()})
})
Conn := make(map[websocket.Connection]bool)
var myChatRoom = "room1"
var mutex = new(sync.Mutex)
ws.OnConnection(func(c websocket.Connection) {
c.Join(myChatRoom)
mutex.Lock()
Conn[c] = true
mutex.Unlock()
c.On("chat", func(message string) {
if message == "leave" {
c.Leave(myChatRoom)
c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.")
c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.")
return
}
})
c.OnDisconnect(func() {
mutex.Lock()
delete(Conn, c)
mutex.Unlock()
fmt.Printf("\nConnection with ID: %s has been disconnected!\n", c.ID())
})
})
var delay = 1 * time.Second
go func() {
i := 0
for {
mutex.Lock()
broadcast(Conn, fmt.Sprintf("aaaa %d\n", i))
mutex.Unlock()
time.Sleep(delay)
i++
}
}()
go func() {
i := 0
for {
mutex.Lock()
broadcast(Conn, fmt.Sprintf("aaaa2 %d\n", i))
mutex.Unlock()
time.Sleep(delay)
i++
}
}()
app.Listen(":8080")
}
func broadcast(Conn map[websocket.Connection]bool, message string) {
for k := range Conn {
k.To("room1").Emit("chat", message)
}
}

View File

@ -0,0 +1,38 @@
var messageTxt;
var messages;
$(function () {
messageTxt = $("#messageTxt");
messages = $("#messages");
w = new Ws("ws://" + HOST + "/my_endpoint");
w.OnConnect(function () {
console.log("Websocket connection established");
});
w.OnDisconnect(function () {
appendMessage($("<div><center><h3>Disconnected</h3></center></div>"));
});
w.On("chat", function (message) {
appendMessage($("<div>" + message + "</div>"));
});
$("#sendBtn").click(function () {
w.Emit("chat", messageTxt.val().toString());
messageTxt.val("");
});
})
function appendMessage(messageDiv) {
var theDiv = messages[0];
var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight;
messageDiv.appendTo(messages);
if (doScroll) {
theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight;
}
}

View File

@ -0,0 +1,24 @@
<html>
<head>
<title>{{ .Title}}</title>
</head>
<body>
<div id="messages"
style="border-width: 1px; border-style: solid; height: 400px; width: 375px;">
</div>
<input type="text" id="messageTxt" />
<button type="button" id="sendBtn">Send</button>
<script type="text/javascript">
var HOST = {{.Host}}
</script>
<script src="js/vendor/jquery-2.2.3.min.js" type="text/javascript"></script>
<!-- This is auto-serving by the Iris, you don't need to have this file in your disk-->
<script src="/iris-ws.js" type="text/javascript"></script>
<!-- -->
<script src="js/chat.js" type="text/javascript"></script>
</body>
</html>

View File

@ -0,0 +1,179 @@
package main
// Run first `go run main.go server`
// and `go run main.go client` as many times as you want.
// Originally written by: github.com/antlaw to describe an old kataras/go-websocket's issue
// which you can find here: https://github.com/kataras/go-websocket/issues/24
import (
"fmt"
"os"
"strings"
"time"
"gopkg.in/kataras/iris.v6"
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
"gopkg.in/kataras/iris.v6/adaptors/websocket"
xwebsocket "golang.org/x/net/websocket"
)
// WS is the current websocket connection
var WS *xwebsocket.Conn
func main() {
if len(os.Args) == 2 && strings.ToLower(os.Args[1]) == "server" {
ServerLoop()
} else if len(os.Args) == 2 && strings.ToLower(os.Args[1]) == "client" {
ClientLoop()
} else {
fmt.Println("wsserver [server|client]")
}
}
/////////////////////////////////////////////////////////////////////////
// client side
func sendUntilErr(sendInterval int) {
i := 1
for {
time.Sleep(time.Duration(sendInterval) * time.Second)
err := SendMessage("2", "all", "objectupdate", "2.UsrSchedule_v1_1")
if err != nil {
fmt.Println("failed to send join message", err.Error())
return
}
fmt.Println("objectupdate", i)
i++
}
}
func recvUntilErr() {
var msg = make([]byte, 2048)
var n int
var err error
i := 1
for {
if n, err = WS.Read(msg); err != nil {
fmt.Println(err.Error())
return
}
fmt.Printf("%v Received: %s.%v\n", time.Now(), string(msg[:n]), i)
i++
}
}
//ConnectWebSocket connect a websocket to host
func ConnectWebSocket() error {
var origin = "http://localhost/"
var url = "ws://localhost:9090/socket"
var err error
WS, err = xwebsocket.Dial(url, "", origin)
return err
}
// CloseWebSocket closes the current websocket connection
func CloseWebSocket() error {
if WS != nil {
return WS.Close()
}
return nil
}
// SendMessage broadcast a message to server
func SendMessage(serverID, to, method, message string) error {
buffer := []byte(message)
return SendtBytes(serverID, to, method, buffer)
}
// SendtBytes broadcast a message to server
func SendtBytes(serverID, to, method string, message []byte) error {
// look https://github.com/kataras/iris/tree/master/adaptors/websocket/message.go , client.go and client.js
// to understand the buffer line:
buffer := []byte(fmt.Sprintf("iris-websocket-message:%v;0;%v;%v;", method, serverID, to))
buffer = append(buffer, message...)
_, err := WS.Write(buffer)
if err != nil {
fmt.Println(err)
return err
}
return nil
}
// ClientLoop connects to websocket server, the keep send and recv dataS
func ClientLoop() {
for {
time.Sleep(time.Second)
err := ConnectWebSocket()
if err != nil {
fmt.Println("failed to connect websocket", err.Error())
continue
}
// time.Sleep(time.Second)
err = SendMessage("2", "all", "join", "dummy2")
go sendUntilErr(1)
recvUntilErr()
err = CloseWebSocket()
if err != nil {
fmt.Println("failed to close websocket", err.Error())
}
}
}
/////////////////////////////////////////////////////////////////////////
// server side
// OnConnect handles incoming websocket connection
func OnConnect(c websocket.Connection) {
fmt.Println("socket.OnConnect()")
c.On("join", func(message string) { OnJoin(message, c) })
c.On("objectupdate", func(message string) { OnObjectUpdated(message, c) })
// ok works too c.EmitMessage([]byte("dsadsa"))
c.OnDisconnect(func() { OnDisconnect(c) })
}
// ServerLoop listen and serve websocket requests
func ServerLoop() {
app := iris.New()
app.Adapt(iris.DevLogger()) // enable all (error) logs
app.Adapt(httprouter.New()) // select the httprouter as the servemux
ws := websocket.New(websocket.Config{Endpoint: "/socket"})
app.Adapt(ws)
ws.OnConnection(OnConnect)
app.Listen("0.0.0.0:9090")
}
// OnJoin handles Join broadcast group request
func OnJoin(message string, c websocket.Connection) {
t := time.Now()
c.Join("server2")
fmt.Println("OnJoin() time taken:", time.Since(t))
}
// OnObjectUpdated broadcasts to all client an incoming message
func OnObjectUpdated(message string, c websocket.Connection) {
t := time.Now()
s := strings.Split(message, ";")
if len(s) != 3 {
fmt.Println("OnObjectUpdated() invalid message format:" + message)
return
}
serverID, _, objectID := s[0], s[1], s[2]
err := c.To("server"+serverID).Emit("objectupdate", objectID)
if err != nil {
fmt.Println(err, "failed to broacast object")
return
}
fmt.Println(fmt.Sprintf("OnObjectUpdated() message:%v, time taken: %v", message, time.Since(t)))
}
// OnDisconnect clean up things when a client is disconnected
func OnDisconnect(c websocket.Connection) {
c.Leave("server2")
fmt.Println("OnDisconnect(): client disconnected!")
}

View File

@ -0,0 +1,62 @@
package main
import (
"fmt"
"gopkg.in/kataras/iris.v6"
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
"gopkg.in/kataras/iris.v6/adaptors/view"
"gopkg.in/kataras/iris.v6/adaptors/websocket"
)
/* Native messages no need to import the iris-ws.js to the ./templates.client.html
Use of: OnMessage and EmitMessage.
NOTICE: IF YOU HAVE RAN THE PREVIOUS EXAMPLES YOU HAVE TO CLEAR YOUR BROWSER's CACHE
BECAUSE chat.js is different than the CACHED. OTHERWISE YOU WILL GET Ws is undefined from the browser's console, becuase it will use the cached.
*/
type clientPage struct {
Title string
Host string
}
func main() {
app := iris.New()
app.Adapt(iris.DevLogger()) // enable all (error) logs
app.Adapt(httprouter.New()) // select the httprouter as the servemux
app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates
ws := websocket.New(websocket.Config{
// the path which the websocket client should listen/registed to,
Endpoint: "/my_endpoint",
// to enable binary messages (useful for protobuf):
// BinaryMessages: true,
})
app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint
app.StaticWeb("/js", "./static/js") // serve our custom javascript code
app.Get("/", func(ctx *iris.Context) {
ctx.Render("client.html", clientPage{"Client Page", ctx.Host()})
})
ws.OnConnection(func(c websocket.Connection) {
c.OnMessage(func(data []byte) {
message := string(data)
c.To(websocket.Broadcast).EmitMessage([]byte("Message from: " + c.ID() + "-> " + message)) // broadcast to all clients except this
c.EmitMessage([]byte("Me: " + message)) // writes to itself
})
c.OnDisconnect(func() {
fmt.Printf("\nConnection with ID: %s has been disconnected!", c.ID())
})
})
app.Listen(":8080")
}

View File

@ -0,0 +1,38 @@
var messageTxt;
var messages;
$(function () {
messageTxt = $("#messageTxt");
messages = $("#messages");
w = new WebSocket("ws://" + HOST + "/my_endpoint");
w.onopen = function () {
console.log("Websocket connection enstablished");
};
w.onclose = function () {
appendMessage($("<div><center><h3>Disconnected</h3></center></div>"));
};
w.onmessage = function(message){
appendMessage($("<div>" + message.data + "</div>"));
};
$("#sendBtn").click(function () {
w.send(messageTxt.val().toString());
messageTxt.val("");
});
})
function appendMessage(messageDiv) {
var theDiv = messages[0];
var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight;
messageDiv.appendTo(messages);
if (doScroll) {
theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight;
}
}

View File

@ -0,0 +1,21 @@
<html>
<head>
<title>{{ .Title}}</title>
</head>
<body>
<div id="messages"
style="border-width: 1px; border-style: solid; height: 400px; width: 375px;">
</div>
<input type="text" id="messageTxt" />
<button type="button" id="sendBtn">Send</button>
<script type="text/javascript">
var HOST = {{.Host}}
</script>
<script src="js/vendor/jquery-2.2.3.min.js" type="text/javascript"></script>
<script src="js/chat.js" type="text/javascript"></script>
</body>
</html>

View File

@ -0,0 +1,219 @@
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");
}
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;
}());
`)

View File

@ -0,0 +1,261 @@
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------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");
}
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};

View File

@ -0,0 +1,136 @@
package websocket
import (
"net/http"
"time"
"gopkg.in/kataras/iris.v6"
)
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(*iris.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 *iris.Context) 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
}

View File

@ -0,0 +1,382 @@
package websocket
import (
"bytes"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/gorilla/websocket"
"gopkg.in/kataras/iris.v6"
)
// 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()
// 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) *iris.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() *iris.Context
// OnDisconnect registers a callback which fires when this connection is closed by an error or manual
OnDisconnect(DisconnectFunc)
// OnError registers a callback which fires when this connection occurs an error
OnError(ErrorFunc)
// EmitError can be used to send a custom error message to the connection
//
// It does nothing more than firing the OnError listeners. It doesn't sends anything to the client.
EmitError(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
Leave(string)
// Disconnect disconnects the client, close the underline websocket conn and removes it from the conn list
// returns the error, if any, from the underline connection
Disconnect() error
}
connection struct {
underline UnderlineConnection
id string
messageType int
pinger *time.Ticker
disconnected bool
onDisconnectListeners []DisconnectFunc
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 *iris.Context
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(s *server, ctx *iris.Context, underlineConn UnderlineConnection, id string) *connection {
c := &connection{
underline: underlineConn,
id: id,
messageType: websocket.TextMessage,
onDisconnectListeners: make([]DisconnectFunc, 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.EmitError(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() *iris.Context {
return c.ctx
}
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) OnError(cb ErrorFunc) {
c.onErrorListeners = append(c.onErrorListeners, cb)
}
func (c *connection) EmitError(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) {
c.server.Leave(roomName, c.id)
}
func (c *connection) Disconnect() error {
return c.server.Disconnect(c.ID())
}

View File

@ -0,0 +1,49 @@
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
}

View File

@ -0,0 +1,188 @@
package websocket
import (
"encoding/json"
"github.com/kataras/go-errors"
"github.com/valyala/bytebufferpool"
"math/rand"
"strconv"
"strings"
"time"
)
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------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))
}

View File

@ -0,0 +1,388 @@
package websocket
import (
"sync"
"github.com/gorilla/websocket"
"gopkg.in/kataras/iris.v6"
)
// Server is the websocket server,
// listens on the config's port, the critical part is the event OnConnection
type Server interface {
// Adapt implements the iris' adaptor, it adapts the websocket server to an Iris station.
// see websocket.go
Adapt(frame *iris.Policies)
// Handler returns the iris.HandlerFunc
// which is setted to the 'Websocket Endpoint path',
// the client should target to this handler's developer's custom path
// ex: iris.Default.Any("/myendpoint", mywebsocket.Handler())
Handler() iris.HandlerFunc
// 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.
Leave(roomName string, connID string)
// 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() iris.HandlerFunc {
// build the upgrader once
c := s.config
upgrader := websocket.Upgrader{ReadBufferSize: c.ReadBufferSize, WriteBufferSize: c.WriteBufferSize, Error: c.Error, CheckOrigin: c.CheckOrigin}
return func(ctx *iris.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.Log(iris.DevMode, "websocket error: "+err.Error())
ctx.EmitError(iris.StatusServiceUnavailable)
return
}
s.handleConnection(ctx, conn)
}
}
// handleConnection creates & starts to listening to a new connection
func (s *server) handleConnection(ctx *iris.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(s, ctx, 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 {
// 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.
func (s *server) Leave(roomName string, connID string) {
s.mu.Lock()
s.leave(roomName, connID)
s.mu.Unlock()
}
// leave used internally, no locks used.
func (s *server) leave(roomName string, connID string) {
///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]
break
}
}
if len(s.rooms[roomName]) == 0 { // if room is empty then delete it
delete(s.rooms, roomName)
}
}
}
// 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) {
// remove the connection from the list
if c, ok := s.connections.remove(connID); ok {
if !c.disconnected {
c.disconnected = true
// stop the ping timer
c.pinger.Stop()
// leave from all joined rooms
s.LeaveAll(connID)
// fire the disconnect callbacks, if any
c.fireDisconnect()
// close the underline connection and return its error, if any.
err = c.underline.Close()
}
}
return
}

View File

@ -0,0 +1,62 @@
// Package websocket provides an easy way to setup server and client side rich websocket experience for Iris
package websocket
import (
"strings"
"gopkg.in/kataras/iris.v6"
)
// 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
}
// Adapt implements the iris' adaptor, it adapts the websocket server to an Iris station.
func (s *server) Adapt(frame *iris.Policies) {
// bind the server's Handler to Iris at Boot state
evt := iris.EventPolicy{
Boot: func(f *iris.Framework) {
wsPath := fixPath(s.config.Endpoint)
if wsPath == "" {
f.Log(iris.DevMode, "websocket's configuration field 'Endpoint' cannot be empty, websocket server stops")
return
}
wsClientSidePath := fixPath(s.config.ClientSourcePath)
if wsClientSidePath == "" {
f.Log(iris.DevMode, "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()
f.Get(wsPath, wsHandler)
// check if client side doesn't already exists
if f.Routes().Lookup(clientSideLookupName) == nil {
// serve the client side on domain:port/iris-ws.js
f.StaticContent(wsClientSidePath, "application/javascript", ClientSource).ChangeName(clientSideLookupName)
}
},
}
evt.Adapt(frame)
}

View File

@ -4,7 +4,6 @@ import (
"crypto/tls" "crypto/tls"
"net" "net"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"time" "time"
@ -17,11 +16,12 @@ type (
// OptionSetter sets a configuration field to the main configuration // OptionSetter sets a configuration field to the main configuration
// used to help developers to write less and configure only what they really want and nothing else // used to help developers to write less and configure only what they really want and nothing else
// example: // example:
// iris.New(iris.Configuration{Sessions:iris.SessionConfiguration{Cookie:"mysessionid"}, Websocket: iris.WebsocketConfiguration{Endpoint:"/my_endpoint"}}) // iris.New(iris.Configuration{Charset: "UTF-8", Gzip:true})
// now can be done also by using iris.Option$FIELD: // now can be done also by using iris.Option$FIELD:
// iris.New(irisOptionSessionsCookie("mycookieid"),iris.OptionWebsocketEndpoint("my_endpoint")) // iris.New(iris.OptionCharset("UTF-8"), iris.OptionGzip(true))
// benefits: // benefits:
// 1. user/dev have no worries what option to pass, he/she can just press iris.Option and all options should be shown to her/his editor's autocomplete-popup window // 1. dev has no worries what option to pass,
// he/she can just press iris.Option and all options should be shown to her/his editor's autocomplete-popup window
// 2. can be passed with any order // 2. can be passed with any order
// 3. Can override previous configuration // 3. Can override previous configuration
OptionSetter interface { OptionSetter interface {
@ -177,9 +177,6 @@ type Configuration struct {
// Sessions contains the configs for sessions // Sessions contains the configs for sessions
Sessions SessionsConfiguration Sessions SessionsConfiguration
// Websocket contains the configs for Websocket's server integration
Websocket WebsocketConfiguration
// Other are the custom, dynamic options, can be empty // Other are the custom, dynamic options, can be empty
// this fill used only by you to set any app's options you want // this fill used only by you to set any app's options you want
// for each of an Iris instance // for each of an Iris instance
@ -443,7 +440,6 @@ func DefaultConfiguration() Configuration {
Charset: DefaultCharset, Charset: DefaultCharset,
Gzip: false, Gzip: false,
Sessions: DefaultSessionsConfiguration(), Sessions: DefaultSessionsConfiguration(),
Websocket: DefaultWebsocketConfiguration(),
Other: options.Options{}, Other: options.Options{},
} }
} }
@ -529,184 +525,6 @@ func DefaultSessionsConfiguration() SessionsConfiguration {
} }
} }
// WebsocketConfiguration the config contains options for the Websocket main config field
type WebsocketConfiguration struct {
// WriteTimeout time allowed to write a message to the connection.
// Default value is 15 * time.Second
WriteTimeout 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 (PongTimeout * 9) / 10
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
// see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more
// Defaults to false
BinaryMessages bool
// 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
// ReadBufferSize is the buffer size for the underline reader
ReadBufferSize int
// WriteBufferSize is the buffer size for the underline writer
WriteBufferSize int
// Error specifies the function for generating HTTP error responses.
//
// The default behavior is to store the reason in the context (ctx.Set(reason)) and fire any custom error (ctx.EmitError(status))
Error func(ctx *Context, status int, reason error)
// CheckOrigin returns true if the request Origin header is acceptable. If
// CheckOrigin is nil, the host in the Origin header must not be set or
// must match the host of the request.
//
// The default behavior is to allow all origins
// you can change this behavior by setting the iris.Default.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin
CheckOrigin func(r *http.Request) bool
// IDGenerator used to create (and later on, set)
// an ID for each incoming websocket connections (clients).
// If empty then the ID is generated by the result of 64
// random combined characters
IDGenerator func(r *http.Request) string
}
var (
// OptionWebsocketWriteTimeout time allowed to write a message to the connection.
// Default value is 15 * time.Second
OptionWebsocketWriteTimeout = func(val time.Duration) OptionSet {
return func(c *Configuration) {
c.Websocket.WriteTimeout = val
}
}
// OptionWebsocketPongTimeout allowed to read the next pong message from the connection
// Default value is 60 * time.Second
OptionWebsocketPongTimeout = func(val time.Duration) OptionSet {
return func(c *Configuration) {
c.Websocket.PongTimeout = val
}
}
// OptionWebsocketPingPeriod send ping messages to the connection with this period. Must be less than PongTimeout
// Default value is (PongTimeout * 9) / 10
OptionWebsocketPingPeriod = func(val time.Duration) OptionSet {
return func(c *Configuration) {
c.Websocket.PingPeriod = val
}
}
// OptionWebsocketMaxMessageSize max message size allowed from connection
// Default value is 1024
OptionWebsocketMaxMessageSize = func(val int64) OptionSet {
return func(c *Configuration) {
c.Websocket.MaxMessageSize = val
}
}
// OptionWebsocketBinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text
// see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more
// Defaults to false
OptionWebsocketBinaryMessages = func(val bool) OptionSet {
return func(c *Configuration) {
c.Websocket.BinaryMessages = val
}
}
// OptionWebsocketEndpoint 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.
OptionWebsocketEndpoint = func(val string) OptionSet {
return func(c *Configuration) {
c.Websocket.Endpoint = val
}
}
// OptionWebsocketReadBufferSize is the buffer size for the underline reader
OptionWebsocketReadBufferSize = func(val int) OptionSet {
return func(c *Configuration) {
c.Websocket.ReadBufferSize = val
}
}
// OptionWebsocketWriteBufferSize is the buffer size for the underline writer
OptionWebsocketWriteBufferSize = func(val int) OptionSet {
return func(c *Configuration) {
c.Websocket.WriteBufferSize = val
}
}
// OptionWebsocketError specifies the function for generating HTTP error responses.
OptionWebsocketError = func(val func(*Context, int, error)) OptionSet {
return func(c *Configuration) {
c.Websocket.Error = val
}
}
// OptionWebsocketCheckOrigin returns true if the request Origin header is acceptable. If
// CheckOrigin is nil, the host in the Origin header must not be set or
// must match the host of the request.
OptionWebsocketCheckOrigin = func(val func(*http.Request) bool) OptionSet {
return func(c *Configuration) {
c.Websocket.CheckOrigin = val
}
}
// OptionWebsocketIDGenerator used to create (and later on, set)
// an ID for each incoming websocket connections (clients).
// If empty then the ID is generated by the result of 64
// random combined characters
OptionWebsocketIDGenerator = func(val func(*http.Request) string) OptionSet {
return func(c *Configuration) {
c.Websocket.IDGenerator = val
}
}
)
const (
// DefaultWebsocketWriteTimeout 15 * time.Second
DefaultWebsocketWriteTimeout = 15 * time.Second
// DefaultWebsocketPongTimeout 60 * time.Second
DefaultWebsocketPongTimeout = 60 * time.Second
// DefaultWebsocketPingPeriod (DefaultPongTimeout * 9) / 10
DefaultWebsocketPingPeriod = (DefaultWebsocketPongTimeout * 9) / 10
// DefaultWebsocketMaxMessageSize 1024
DefaultWebsocketMaxMessageSize = 1024
)
var (
// DefaultWebsocketError is the default method to manage the handshake websocket errors
DefaultWebsocketError = func(ctx *Context, status int, reason error) {
ctx.Set("WsError", reason)
ctx.EmitError(status)
}
// DefaultWebsocketCheckOrigin is the default method to allow websocket clients to connect to this server
// you can change this behavior by setting the iris.Default.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin
DefaultWebsocketCheckOrigin = func(r *http.Request) bool {
return true
}
// WebsocketCheckSameOrigin returns true if the origin is not set or is equal to the request host
WebsocketCheckSameOrigin = func(r *http.Request) bool {
origin := r.Header.Get("origin")
if len(origin) == 0 {
return true
}
u, err := url.Parse(origin)
if err != nil {
return false
}
return u.Host == r.Host
}
)
// DefaultWebsocketConfiguration returns the default config for iris-ws websocket package
func DefaultWebsocketConfiguration() WebsocketConfiguration {
return WebsocketConfiguration{
WriteTimeout: DefaultWebsocketWriteTimeout,
PongTimeout: DefaultWebsocketPongTimeout,
PingPeriod: DefaultWebsocketPingPeriod,
MaxMessageSize: DefaultWebsocketMaxMessageSize,
BinaryMessages: false,
ReadBufferSize: 4096,
WriteBufferSize: 4096,
Endpoint: "",
// use the kataras/go-websocket default
IDGenerator: nil,
}
}
// Default values for base Server conf // Default values for base Server conf
const ( const (
// DefaultServerHostname returns the default hostname which is 0.0.0.0 // DefaultServerHostname returns the default hostname which is 0.0.0.0

10
doc.go
View File

@ -513,11 +513,11 @@ Example code:
You should have a basic idea of the framework by now, we just scratched the surface. You should have a basic idea of the framework by now, we just scratched the surface.
If you enjoy what you just saw and want to learn more, please follow the below links: If you enjoy what you just saw and want to learn more, please follow the below links:
- examples: https://github.com/iris-contrib/examples - examples: https://github.com/iris-contrib/examples
- book: https://docs.iris-go.com - book: https://docs.iris-go.com
- adaptors: https://github.com/kataras/iris/tree/v6/adaptors - adaptors: https://github.com/kataras/iris/tree/v6/adaptors
- middleware: https://github.com/kataras/iris/tree/v6/middleware & https://github.com/iris-contrib/middleware - middleware: https://github.com/kataras/iris/tree/v6/middleware & https://github.com/iris-contrib/middleware
- godocs: https://godoc.org/github.com/kataras/iris - godocs: https://godoc.org/github.com/kataras/iris
*/ */

20
iris.go
View File

@ -70,10 +70,9 @@ type Framework struct {
ln net.Listener ln net.Listener
closedManually bool closedManually bool
once sync.Once once sync.Once
Config *Configuration Config *Configuration
sessions sessions.Sessions sessions sessions.Sessions
Websocket *WebsocketServer
} }
var defaultGlobalLoggerOuput = log.New(os.Stdout, "[iris] ", log.LstdFlags) var defaultGlobalLoggerOuput = log.New(os.Stdout, "[iris] ", log.LstdFlags)
@ -253,19 +252,6 @@ func New(setters ...OptionSetter) *Framework {
}}) }})
} }
{
// +------------------------------------------------------------+
// | Module Name: Websocket |
// | On Init: Attach a new websocket server. |
// | It starts on first callback registration |
// +------------------------------------------------------------+
// in order to be able to call $instance.Websocket.OnConnection.
// The whole server's configuration will be
// initialized on the first OnConnection registration (no runtime)
s.Websocket = NewWebsocketServer(s)
}
{ {
// +------------------------------------------------------------+ // +------------------------------------------------------------+
// | Module Name: Router | // | Module Name: Router |

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2017 Gerasimos Maropoulos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,3 +1,25 @@
The MIT License (MIT)
Copyright (c) 2016-2017 Gerasimos Maropoulos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/

21
middleware/logger/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2017 Gerasimos Maropoulos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2017 Gerasimos Maropoulos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,104 +0,0 @@
package iris
import (
"net/http"
"sync"
"github.com/kataras/go-websocket"
)
// conversionals
const (
// All is the string which the Emitter use to send a message to all
All = websocket.All
// NotMe is the string which the Emitter use to send a message to all except this websocket.Connection
NotMe = websocket.NotMe
// Broadcast is the string which the Emitter use to send a message to all except this websocket.Connection, same as 'NotMe'
Broadcast = websocket.Broadcast
)
// Note I keep this code only to no change the front-end API, we could only use the go-websocket and set our custom upgrader
type (
// WebsocketServer is the iris websocket server, expose the websocket.Server
// the below code is a wrapper and bridge between iris-contrib/websocket and kataras/go-websocket
WebsocketServer struct {
websocket.Server
station *Framework
once sync.Once
// Config:
// if endpoint is not empty then this configuration is used instead of the station's
// useful when the user/dev wants more than one websocket server inside one iris instance.
Config WebsocketConfiguration
}
)
// NewWebsocketServer returns a new empty unitialized websocket server
// it runs on first OnConnection
func NewWebsocketServer(station *Framework) *WebsocketServer {
return &WebsocketServer{station: station, Server: websocket.New(), Config: station.Config.Websocket}
}
// NewWebsocketServer creates the client side source route and the route path Endpoint with the correct Handler
// receives the websocket configuration and the iris station
// and returns the websocket server which can be attached to more than one iris station (if needed)
func (ws *WebsocketServer) init() {
if ws.Config.Endpoint == "" {
ws.Config = ws.station.Config.Websocket
}
c := ws.Config
if c.Endpoint == "" {
return
}
if c.CheckOrigin == nil {
c.CheckOrigin = DefaultWebsocketCheckOrigin
}
if c.Error == nil {
c.Error = DefaultWebsocketError
}
// set the underline websocket server's configuration
ws.Server.Set(websocket.Config{
WriteTimeout: c.WriteTimeout,
PongTimeout: c.PongTimeout,
PingPeriod: c.PingPeriod,
MaxMessageSize: c.MaxMessageSize,
BinaryMessages: c.BinaryMessages,
ReadBufferSize: c.ReadBufferSize,
WriteBufferSize: c.WriteBufferSize,
Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
ws.station.Context.Run(w, r, func(ctx *Context) {
c.Error(ctx, status, reason)
})
},
CheckOrigin: c.CheckOrigin,
IDGenerator: c.IDGenerator,
})
// set the routing for client-side source (javascript) (optional)
clientSideLookupName := "iris-websocket-client-side"
ws.station.Get(c.Endpoint, ToHandler(ws.Server.Handler()))
// check if client side already exists
if ws.station.Routes().Lookup(clientSideLookupName) == nil {
// serve the client side on domain:port/iris-ws.js
ws.station.StaticContent("/iris-ws.js", contentJavascript, websocket.ClientSource).ChangeName(clientSideLookupName)
}
}
// WebsocketConnection is the front-end API that you will use to communicate with the client side
type WebsocketConnection interface {
websocket.Connection
}
// OnConnection this is the main event you, as developer, will work with each of the websocket connections
func (ws *WebsocketServer) OnConnection(connectionListener func(WebsocketConnection)) {
ws.once.Do(ws.init)
ws.Server.OnConnection(func(c websocket.Connection) {
connectionListener(c)
})
}