mirror of
https://github.com/kataras/iris.git
synced 2025-03-14 08:26:26 +01:00
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:
parent
26a5f9f3b0
commit
82afcc5aa6
209
HISTORY.md
209
HISTORY.md
|
@ -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 `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)`
|
||||
- Remove `.Config.Websocket` , replaced with the `kataras/iris/adaptors/websocket.Config` adaptor.
|
||||
|
||||
- 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
|
||||
|
||||
There are many internal improvements to the [websocket server](https://github.com/kataras/go-websocket), and it's
|
||||
operating slighty faster.
|
||||
There are many internal improvements to the websocket server, it
|
||||
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
|
||||
the fasthttp iris' version and back then no one did that before.
|
||||
Now(after v6) iris is compatible with any net/http websocket library that already created by third-parties.
|
||||
Websocket is an Adaptor too and you can edit more configuration fields than before.
|
||||
No Write and Read timeout by default, you have to set the fields if you want to enable timeout.
|
||||
|
||||
If the iris' websocket feature does not cover your app's needs, you can simple use any other
|
||||
library for websockets, like the Golang's compatible to `socket.io`, example:
|
||||
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
|
||||
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
|
||||
package main
|
||||
|
|
|
@ -68,30 +68,30 @@ type (
|
|||
|
||||
var (
|
||||
errMuxEntryConflictsWildcard = errors.New(`
|
||||
Router: '%s' in new path '%s'
|
||||
httprouter: '%s' in new path '%s'
|
||||
conflicts with existing wildcarded route with path: '%s'
|
||||
in existing prefix of'%s' `)
|
||||
|
||||
errMuxEntryMiddlewareAlreadyExists = errors.New(`
|
||||
Router: Middleware were already registered for the path: '%s'`)
|
||||
httprouter: Middleware were already registered for the path: '%s'`)
|
||||
|
||||
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(`
|
||||
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(`
|
||||
Router: Unnamed wildcard found in path: '%s'`)
|
||||
httprouter: Unnamed wildcard found in path: '%s'`)
|
||||
|
||||
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(`
|
||||
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(`
|
||||
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
|
||||
|
@ -575,7 +575,7 @@ func New() iris.Policies {
|
|||
// 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
|
||||
fatalErr = true
|
||||
logger(iris.ProdMode, "fatal error on httprouter build adaptor: "+err.Error())
|
||||
logger(iris.ProdMode, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
|
21
adaptors/view/LICENSE
Normal file
21
adaptors/view/LICENSE
Normal 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.
|
44
adaptors/websocket/LICENSE
Normal file
44
adaptors/websocket/LICENSE
Normal 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.
|
99
adaptors/websocket/_examples/websocket/main.go
Normal file
99
adaptors/websocket/_examples/websocket/main.go
Normal 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")
|
||||
}
|
38
adaptors/websocket/_examples/websocket/static/js/chat.js
Normal file
38
adaptors/websocket/_examples/websocket/static/js/chat.js
Normal 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;
|
||||
}
|
||||
}
|
24
adaptors/websocket/_examples/websocket/templates/client.html
Normal file
24
adaptors/websocket/_examples/websocket/templates/client.html
Normal 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>
|
113
adaptors/websocket/_examples/websocket_connectionlist/main.go
Normal file
113
adaptors/websocket/_examples/websocket_connectionlist/main.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
179
adaptors/websocket/_examples/websocket_custom_go_client/main.go
Normal file
179
adaptors/websocket/_examples/websocket_custom_go_client/main.go
Normal 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!")
|
||||
|
||||
}
|
|
@ -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")
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
219
adaptors/websocket/client.go
Normal file
219
adaptors/websocket/client.go
Normal 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;
|
||||
}());
|
||||
`)
|
261
adaptors/websocket/client.ts
Normal file
261
adaptors/websocket/client.ts
Normal 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};
|
136
adaptors/websocket/config.go
Normal file
136
adaptors/websocket/config.go
Normal 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
|
||||
}
|
382
adaptors/websocket/connection.go
Normal file
382
adaptors/websocket/connection.go
Normal 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())
|
||||
}
|
49
adaptors/websocket/emitter.go
Normal file
49
adaptors/websocket/emitter.go
Normal 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
|
||||
}
|
188
adaptors/websocket/message.go
Normal file
188
adaptors/websocket/message.go
Normal 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))
|
||||
}
|
388
adaptors/websocket/server.go
Normal file
388
adaptors/websocket/server.go
Normal 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
|
||||
}
|
62
adaptors/websocket/websocket.go
Normal file
62
adaptors/websocket/websocket.go
Normal 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)
|
||||
}
|
190
configuration.go
190
configuration.go
|
@ -4,7 +4,6 @@ import (
|
|||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -17,11 +16,12 @@ type (
|
|||
// 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
|
||||
// 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:
|
||||
// iris.New(irisOptionSessionsCookie("mycookieid"),iris.OptionWebsocketEndpoint("my_endpoint"))
|
||||
// iris.New(iris.OptionCharset("UTF-8"), iris.OptionGzip(true))
|
||||
// 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
|
||||
// 3. Can override previous configuration
|
||||
OptionSetter interface {
|
||||
|
@ -177,9 +177,6 @@ type Configuration struct {
|
|||
// Sessions contains the configs for sessions
|
||||
Sessions SessionsConfiguration
|
||||
|
||||
// Websocket contains the configs for Websocket's server integration
|
||||
Websocket WebsocketConfiguration
|
||||
|
||||
// Other are the custom, dynamic options, can be empty
|
||||
// this fill used only by you to set any app's options you want
|
||||
// for each of an Iris instance
|
||||
|
@ -443,7 +440,6 @@ func DefaultConfiguration() Configuration {
|
|||
Charset: DefaultCharset,
|
||||
Gzip: false,
|
||||
Sessions: DefaultSessionsConfiguration(),
|
||||
Websocket: DefaultWebsocketConfiguration(),
|
||||
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
|
||||
const (
|
||||
// DefaultServerHostname returns the default hostname which is 0.0.0.0
|
||||
|
|
14
iris.go
14
iris.go
|
@ -73,7 +73,6 @@ type Framework struct {
|
|||
once sync.Once
|
||||
Config *Configuration
|
||||
sessions sessions.Sessions
|
||||
Websocket *WebsocketServer
|
||||
}
|
||||
|
||||
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 |
|
||||
|
|
21
middleware/basicauth/LICENSE
Normal file
21
middleware/basicauth/LICENSE
Normal 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.
|
|
@ -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
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
|
21
middleware/logger/LICENSE
Normal file
21
middleware/logger/LICENSE
Normal 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.
|
21
middleware/recover/LICENSE
Normal file
21
middleware/recover/LICENSE
Normal 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.
|
104
websocket.go
104
websocket.go
|
@ -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)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user