├── .gitignore
├── go.mod
├── test
├── script.js
└── index.html
├── LICENSE
├── go.sum
├── websocket_test.go
├── client.min.js
├── readme.md
├── client.js
└── websocket.go
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .history
3 | .vscode
4 | .idea
5 | .git
6 | .svn
7 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/AspieSoft/go-websocket
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/AspieSoft/go-regex-re2/v2 v2.2.0
7 | github.com/AspieSoft/goutil/compress/gzip v0.0.0-20240508040632-49b3bd4cec6a
8 | github.com/AspieSoft/goutil/crypt v0.0.0-20231120162123-776c8b89988d
9 | github.com/AspieSoft/goutil/v7 v7.8.0
10 | github.com/alphadose/haxmap v1.4.0
11 | golang.org/x/net v0.27.0
12 | )
13 |
14 | require golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
15 |
--------------------------------------------------------------------------------
/test/script.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | ;(function(){
4 | let socket = new ServerIO();
5 |
6 | socket.connect(function(){
7 | console.log('connected');
8 | });
9 |
10 | socket.on('message', function(data){
11 | console.log('message received:', data);
12 |
13 | socket.send('message', 'message received');
14 | });
15 |
16 | socket.disconnect(function(code){
17 | console.log('socket disconnected', code);
18 | });
19 |
20 | setTimeout(function(){
21 | socket.disconnect(1006);
22 | }, 3000);
23 | })();
24 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | WebSocket Test
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Hello, World!
20 |
21 |
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2023 web@aspiesoft.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/AspieSoft/go-regex-re2/v2 v2.2.0 h1:CK9+SYs7BYy+lV/JrmRbyF+SuTF+e+BIyjKGjKJQzLg=
2 | github.com/AspieSoft/go-regex-re2/v2 v2.2.0/go.mod h1:w+vA1zICvB4OQZGY8KdpyMwjwbFXdnZt9iQ7jRR+ycQ=
3 | github.com/AspieSoft/goutil/compress/gzip v0.0.0-20240508040632-49b3bd4cec6a h1:YXf4o4RP3E8roiBp83kbxXfx6X2Gj7loCKaBZoVsCqs=
4 | github.com/AspieSoft/goutil/compress/gzip v0.0.0-20240508040632-49b3bd4cec6a/go.mod h1:3YENrWBz3uY2QhIiPVTSgPiqG3jKzxU+agXzgIY4c4E=
5 | github.com/AspieSoft/goutil/crypt v0.0.0-20231120162123-776c8b89988d h1:FaXvJbh42XG/Z2Q8m+3fsZywbBrBHOYABk6EH2wlHjA=
6 | github.com/AspieSoft/goutil/crypt v0.0.0-20231120162123-776c8b89988d/go.mod h1:Pn5yHoJ8/THP/GM0cqTFLh5acw2Qibr1R9qa5FM7H0w=
7 | github.com/AspieSoft/goutil/v7 v7.8.0 h1:xTtVfOwxLGKanNt3tBXb+gPruQRQKnHtS2wFjCV1k7c=
8 | github.com/AspieSoft/goutil/v7 v7.8.0/go.mod h1:JGAt912jBwFrTXiazla1FTVqSI3zDetUKR5HJb9ND5I=
9 | github.com/alphadose/haxmap v1.4.0 h1:1yn+oGzy2THJj1DMuJBzRanE3sMnDAjJVbU0L31Jp3w=
10 | github.com/alphadose/haxmap v1.4.0/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM=
11 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
12 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
13 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
14 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
15 |
--------------------------------------------------------------------------------
/websocket_test.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func Test(t *testing.T){
11 | server := NewServer("http://localhost:3000")
12 | http.Handle("/ws", server.Handler())
13 |
14 | http.HandleFunc("/client.js", func(w http.ResponseWriter, r *http.Request) {
15 | http.ServeFile(w, r, "./client.js")
16 | })
17 |
18 | static := http.FileServer(http.Dir("./test/"))
19 | http.Handle("/", static)
20 |
21 | LogErrors()
22 |
23 | server.Connect(func(client *Client){
24 | fmt.Println("connected:", client.ClientID)
25 |
26 | client.On("message", func(msg interface{}) {
27 | str := ToType[string](msg)
28 | fmt.Println("client:", str)
29 | })
30 |
31 | client.Disconnect(func(code int) {
32 | fmt.Println("client disconnected:", client.ClientID, "-", code)
33 | })
34 | })
35 |
36 | handled := 0
37 |
38 | server.Connect(func(client *Client){
39 | fmt.Println("connected")
40 | handled++
41 |
42 | client.Store["key"] = "value"
43 |
44 | client.On("message", func(msg interface{}) {
45 | str := ToType[string](msg)
46 | fmt.Println("client:", str)
47 |
48 | handled++
49 | })
50 |
51 | client.Disconnect(func(code int) {
52 | fmt.Println("client disconnected", code)
53 | handled++
54 | })
55 |
56 | server.Broadcast("message", "test")
57 | server.Broadcast("no-message", "test should not be sent")
58 | })
59 |
60 | server.On("message", func(client *Client, msg interface{}) {
61 | fmt.Println("server:", msg)
62 |
63 | fmt.Println("key:", client.Store["key"])
64 | if client.Store["key"] == "value" {
65 | handled++
66 | }
67 |
68 | handled++
69 | })
70 |
71 | server.Disconnect(func(client *Client, code int) {
72 | fmt.Println("server disconnected", code)
73 | handled++
74 | })
75 |
76 | go func(){
77 | err := http.ListenAndServe(":3000", nil)
78 | if err != nil {
79 | t.Error(err)
80 | }
81 | }()
82 |
83 | time.Sleep(10 * time.Second)
84 |
85 | if handled > 0 && handled < 6 {
86 | t.Error("test did not finish correctly")
87 | }
88 |
89 | if len(ErrLog) != 0 {
90 | for _, err := range ErrLog {
91 | t.Error(err)
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/client.min.js:
--------------------------------------------------------------------------------
1 | "use strict";!function(){const t=document.body||document.querySelector("body")[0];let e;const s=document.querySelector("script[nonce]");s&&(e=s.getAttribute("nonce")),function(s){const n=document.createElement("script");n.src=s,e&&n.setAttribute("nonce",e),t.appendChild(n)}("https://cdn.jsdelivr.net/gh/nodeca/pako@1.0.11/dist/pako.min.js")}();class ServerIO{constructor(t=null){let e;e="string"==typeof t&&t.match(/^(http|ws)s?:\/\//)?t.replace(/^http/,"ws"):window.location.origin.replace(/^http/,"ws"),this.origin=e;let s=new WebSocket(e+"/ws");this.socket=s,this._sendingData=0,this._data=void 0,this.autoReconnect=!0,this._listeners={},this._currentListeners=[],this.lastConnect=0,this.lastDisconnect=0,this.connected=!1,this._disconnectCode=void 0;const n=t=>{t.target.url&&t.target.url.startsWith(e)&&(this.lastConnect=Date.now(),this.connected=!0)},i=t=>{if(!t.target.url||!t.target.url.startsWith(e))return;this.connected=!1;let s=Date.now();if(s-this.lastDisconnect<100)return;this.lastDisconnect=s,this._oldData=this._data,this._data=void 0;let n=t.code||this._disconnectCode||1e3;if(this._disconnectCode&&1e3===n&&(n=this._disconnectCode),this._disconnectCode=void 0,this._listeners["@disconnect"])for(let t=0;t{if(t.origin!==e)return;let s=t.data;if(this._data&&1===this._data.compress)try{let t=pako.ungzip(atob(s),{to:"string"});t&&""!==t&&(s=t)}catch(t){}try{s=JSON.parse(s)}catch(t){return}if("@connection"===s.name)"connect"!==s.data||this._data||setTimeout((()=>{let t=0;s.canCompress&&void 0!==window.pako&&(t=1),this._data={clientID:s.clientID,token:s.token,serverKey:s.serverKey,encKey:s.encKey,compress:t},setTimeout((()=>{this._socketSend(JSON.stringify({name:"@connection",data:"connect",token:s.token,compress:t}))}),100)}),500);else{if(s.token!==this._data.serverKey)return;if("@error"===s.name){if(this._listeners["@error"])for(let t=0;t0;)await new Promise((t=>setTimeout(t,100)));if(this._sendingData++,await new Promise((t=>setTimeout(t,10))),this._sendingData>1)return this._sendingData--,void this._socketSend(json);this.socket.send(t),await new Promise((t=>setTimeout(t,100))),this._sendingData--}}async connect(t){return setTimeout((async()=>{if("function"==typeof t){for(;!this._data;)await new Promise((t=>setTimeout(t,100)));return this._listeners["@connect"]||(this._listeners["@connect"]=[]),this._listeners["@connect"].push(t),void t.call(this,!0)}if(this._data)return;let e=Date.now();if(e-this.lastConnect<1e4)setTimeout((()=>{this.connect(t)}),e-this.lastConnect);else{for(this.lastConnect=e,this.socket=new WebSocket(this.origin+"/ws"),this.socket.addEventListener("open",this._socketFuncs.onConnect,{passive:!0}),this.socket.addEventListener("close",this._socketFuncs.onDisconnect,{passive:!0}),this.socket.addEventListener("error",this._socketFuncs.onDisconnect,{passive:!0}),this.socket.addEventListener("message",this._socketFuncs.onMessage,{passive:!0});!this._data;)await new Promise((t=>setTimeout(t,100)));if("@retry"===t&&this._oldData&&(this._socketSend(JSON.stringify({name:"@connection",data:"migrate",token:this._data.token,oldClient:this._oldData.clientID,oldToken:this._oldData.token,oldServerKey:this._oldData.serverKey,oldEncKey:this._oldData.encKey})),this._oldData=void 0),this._listeners["@connect"])for(let t=0;tsetTimeout(t,100)));let e=1e3;return"number"==typeof t&&(e=Number(t)),e<1e3&&(e+=1e3),this._disconnectCode=e,this._socketSend(JSON.stringify({name:"@connection",data:"disconnect",token:this._data.token,code:e})),setTimeout((()=>{this.connected&&this.socket.close(e)}),1e3),this}async on(t,e){if("string"!=typeof t||"string"!=typeof t.toString())return this;if(""===(t=t.toString().replace(/[^\w_-]+/g,"")))return this;for(this._listeners[t]||(this._listeners[t]=[]),this._currentListeners.includes(t)||this._currentListeners.push(t),"function"==typeof e&&this._listeners[t].push(e);!this._data;)await new Promise((t=>setTimeout(t,100)));return this._socketSend(JSON.stringify({name:"@listener",data:t,token:this._data.token})),this}async off(t,e=!0){if("string"!=typeof t||"string"!=typeof t.toString())return this;if(""===(t=t.toString().replace(/[^\w_-]+/g,"")))return this;this._listeners[t]&&e&&delete this._listeners[t];let s=this._currentListeners.indexOf(t);for(-1!==s&&this._currentListeners.splice(s,1);!this._data;)await new Promise((t=>setTimeout(t,100)));return this._socketSend(JSON.stringify({name:"@listener",data:"!"+t,token:this._data.token})),this}async send(t,e){if("string"!=typeof t||"string"!=typeof t.toString()||"function"==typeof e)return this;if(""===(t=t.toString().replace(/[^\w_-]+/g,"")))return this;for(;!this._data;)await new Promise((t=>setTimeout(t,100)));let s=JSON.stringify({name:t,data:e,token:this._data.token});if(1===this._data.compress)try{let t=btoa(pako.gzip(s,{to:"string"}));t&&""!==t&&(s=t)}catch(t){}return this._socketSend(s),this}async error(t){return"function"==typeof t&&(this._listeners["@error"]||(this._listeners["@error"]=[]),this._listeners["@error"].push(t)),this}}
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Go WebSocket
2 |
3 | [](https://paypal.me/shaynejrtaylor?country.x=US&locale.x=en_US)
4 |
5 | An easy way to get started with websockets in golang.
6 |
7 | This module tracks user sessions, can recover temporarily lost connections, and compresses data through gzip if supported by the client.
8 |
9 | ## Installation
10 |
11 | ### Go (server)
12 |
13 | ```shell script
14 | go get github.com/AspieSoft/go-websocket
15 | ```
16 |
17 | ### JavaScript (client)
18 |
19 | ```html
20 |
21 | ```
22 |
23 | ## Usage
24 |
25 | ### Go (server)
26 |
27 | ```go
28 |
29 | import (
30 | "net/http"
31 |
32 | "github.com/AspieSoft/go-websocket"
33 | )
34 |
35 | func main(){
36 | // setup a new websocket server
37 | server := websocket.NewServer("https://www.example.com")
38 | http.Handle("/ws", server.Handler())
39 |
40 | // optional: set a timeout for when disconnected clients can no longer reconnect with the same data
41 | server := websocket.NewServer("https://www.example.com", 30 * time.Second)
42 |
43 | // by default, data is compressed with gzip before being send between the server and client
44 | // this behavior can opptonally be disabled
45 | server.Gzip = false
46 |
47 | // by default, the server only reads requests up to 1 megabyte
48 | // this value can opptionally be changed
49 | server.ReqSize = 1024 // KB
50 |
51 |
52 | static := http.FileServer(http.Dir("./public/"))
53 | http.Handle("/", static)
54 |
55 | server.Connect(func(client *websocket.Client){
56 | // client connected
57 |
58 | // localstorage that stays with the client, and verifies their session
59 | // map[string]interface{}
60 | client.Store["key"] = "value"
61 |
62 | server.Broadcast("notify", "a new user connected to the server")
63 | server.Broadcast("user", client.ClientID)
64 |
65 | client.On("message", func(msg interface{}) {
66 | // client sent a message
67 |
68 | // optional: enforce a specific message type
69 | str := websocket.ToType[string](msg)
70 | b := websocket.ToType[[]byte](msg)
71 | i := websocket.ToType[int](msg)
72 | bool := websocket.ToType[bool](msg)
73 | json := websocket.ToType[map[string]interface{}](msg)
74 | array := websocket.ToType[[]interface{}](msg)
75 | })
76 |
77 | // send data to client
78 | client.Send("message", "my message to the client")
79 |
80 | client.Send("json", map[string]interface{}{
81 | "jsondata": "my json data",
82 | "key": "value",
83 | "a": 1,
84 | })
85 |
86 | client.on("send-to-friend", func(msg interface{}){
87 | json := websocket.ToType[map[string]interface{}](msg)
88 |
89 | // send a message to a different client
90 | server.send(json["friendsClientID"], json["msg"])
91 |
92 | // do other stuff...
93 | client.send("send-from-friend", map[string]interface{
94 | "ClientID": client.ClientID,
95 | "msg": "General Kenobi!",
96 | })
97 | })
98 |
99 | client.Disconnect(func(code int) {
100 | // when client disconnects
101 | server.Broadcast("disconnected", client.ClientID)
102 | })
103 | })
104 |
105 | server.On("kick", func(client *websocket.Client, msg interface{}) {
106 | friendsClientID := websocket.ToType[string](msg)
107 |
108 | // force a client to leave the server
109 | server.Kick(friendsClientID, 1000)
110 |
111 | server.Broadcast("kicked", friendsClientID+" was kicked by "+client.ClientID)
112 | })
113 |
114 | server.On("kick-all", func(client *Client, msg interface{}) {
115 | // kick every client from the server
116 | server.KickAll()
117 | })
118 |
119 | log.Fatal(http.ListenAndServe(":3000", nil))
120 | }
121 |
122 | ```
123 |
124 | ### JavaScript (client)
125 |
126 | ```JavaScript
127 |
128 | const socket = new ServerIO(); // will default to current origin
129 | // or
130 | const socket = new ServerIO('https://www.example.com'); // optional: specify a different origin
131 |
132 | socket.connect(function(initial){
133 | // connected to server
134 |
135 | socket.send('message', "my message to the server");
136 |
137 | if(initial){
138 | // very first time connecting to the server
139 | }else{
140 | // reconnecting after a previous disconnect
141 | }
142 | });
143 |
144 | socket.on('message', function(msg){
145 | console.log('I got a new message:', msg);
146 |
147 | socket.send('json', {
148 | jsondata: 'my json data',
149 | key: 'value',
150 | a: 1,
151 | });
152 | });
153 |
154 | socket.on('json', function(msg){
155 | console.log('pre-parsed json:', msg.jsondata);
156 | });
157 |
158 | socket.on('notify', function(msg){
159 | console.log(msg);
160 | });
161 |
162 | let myNewFriend = null;
163 | socket.on('user', function(msg){
164 | myNewFriend = msg;
165 |
166 | socket.send('send-to-friend', {
167 | friendsClientID: myNewFriend,
168 | msg: 'Hello, There!',
169 | })
170 | });
171 |
172 | socket.on('send-from-friend', function(msg){
173 | socket.send('kick', myNewFriend);
174 | });
175 |
176 | socket.on('kicked', function(msg){
177 | socket.send('kick-all');
178 |
179 | // run disconnect
180 | socket.disconnect();
181 |
182 | // run reconnect
183 | socket.connect();
184 | });
185 |
186 | socket.disconnect(function(status){
187 | // on disconnect
188 | if(status === 1000){
189 | console.log('disconnected successfully');
190 | }else if(status === 1006){
191 | console.warn('error: disconnected by accident, auto reconnecting...');
192 | }else{
193 | console.error('error:', status);
194 | }
195 | });
196 |
197 |
198 | socket.on('notifications', function(){
199 | console.log('my notification');
200 | })
201 |
202 | // stop listining to a message, and remove all client listeners of that type
203 | socket.off('notifications');
204 |
205 | // stop the server from sending messages, but keep the listener on the client
206 | socket.off('notifications', false);
207 |
208 | // request the server to send messages again
209 | socket.on('notifications');
210 |
211 |
212 | socket.error(function(type){
213 | // handle errors
214 | // Note: disconnection errors will Not trigger this method
215 |
216 | if(type === 'migrate'){
217 | // the client reconnected to the server, but the server failed to migrate the clients old data to the new connection
218 | // client listeners should still automatically be restored
219 | }
220 | });
221 |
222 | ```
223 |
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | ;(function(){
4 | const body = (document.body || document.querySelector('body')[0]);
5 |
6 | let nonceKey = undefined;
7 | const nonceKeyElm = document.querySelector('script[nonce]');
8 | if(nonceKeyElm){
9 | nonceKey = nonceKeyElm.getAttribute('nonce');
10 | }
11 |
12 | function addScript(url){
13 | const script = document.createElement('script');
14 | script.src = url;
15 | if(nonceKey){
16 | script.setAttribute('nonce', nonceKey);
17 | }
18 | body.appendChild(script)
19 | }
20 |
21 | // add script dependencies
22 | addScript('https://cdn.jsdelivr.net/gh/nodeca/pako@1.0.11/dist/pako.min.js');
23 | })();
24 |
25 | class ServerIO {
26 | constructor(setOrigin = null) {
27 | let origin;
28 | if(typeof setOrigin === 'string' && setOrigin.match(/^(http|ws)s?:\/\//)){
29 | origin = setOrigin.replace(/^http/, 'ws');
30 | }else{
31 | origin = window.location.origin.replace(/^http/, 'ws');
32 | }
33 | this.origin = origin;
34 |
35 | let socket = new WebSocket(origin + '/ws');
36 | this.socket = socket;
37 | this._sendingData = 0;
38 |
39 | this._data = undefined;
40 | this.autoReconnect = true;
41 | this._listeners = {};
42 | this._currentListeners = [];
43 |
44 | this.lastConnect = 0;
45 | this.lastDisconnect = 0;
46 | this.connected = false;
47 | this._disconnectCode = undefined;
48 |
49 | const onConnect = (e) => {
50 | if(!e.target.url || !e.target.url.startsWith(origin)){
51 | return;
52 | }
53 |
54 | this.lastConnect = Date.now();
55 | this.connected = true;
56 | };
57 |
58 | const onDisconnect = (e) => {
59 | if(!e.target.url || !e.target.url.startsWith(origin)){
60 | return;
61 | }
62 |
63 | this.connected = false;
64 |
65 | let now = Date.now();
66 | if(now - this.lastDisconnect < 100){
67 | return;
68 | }
69 | this.lastDisconnect = now;
70 |
71 | this._oldData = this._data;
72 | this._data = undefined;
73 |
74 | let code = e.code || this._disconnectCode || 1000;
75 | if(this._disconnectCode && code === 1000){
76 | code = this._disconnectCode;
77 | }
78 | this._disconnectCode = undefined;
79 |
80 | if(this._listeners['@disconnect']){
81 | for(let i = 0; i < this._listeners['@disconnect'].length; i++){
82 | this._listeners['@disconnect'][i].call(this, code);
83 | }
84 | }
85 |
86 | if(this.autoReconnect && [1006, 1009, 1011, 1012, 1013, 1014, 1015].includes(code)){
87 | console.log('reconnecting')
88 | this.connect('@retry');
89 | }
90 | };
91 |
92 | const onMessage = (e) => {
93 | if(e.origin !== origin){
94 | return;
95 | }
96 |
97 | let data = e.data;
98 | if(this._data && this._data.compress === 1){
99 | try {
100 | let dec = pako.ungzip(atob(data), {to: 'string'});
101 | if(dec && dec !== ''){
102 | data = dec;
103 | }
104 | } catch(e) {}
105 | }
106 |
107 | try {
108 | data = JSON.parse(data);
109 | } catch(e) {
110 | return;
111 | }
112 |
113 | if(data.name === '@connection'){
114 | if(data.data === 'connect' && !this._data){
115 | setTimeout(() => {
116 | let compress = 0;
117 | if(data.canCompress && typeof window.pako !== 'undefined'){
118 | compress = 1;
119 | }
120 |
121 | this._data = {
122 | clientID: data.clientID,
123 | token: data.token,
124 | serverKey: data.serverKey,
125 | encKey: data.encKey,
126 | compress: compress,
127 | };
128 |
129 | setTimeout(() => {
130 | this._socketSend(JSON.stringify({
131 | name: "@connection",
132 | data: "connect",
133 | token: data.token,
134 | compress: compress,
135 | }));
136 | }, 100);
137 | }, 500);
138 | }
139 | }else{
140 | if(data.token !== this._data.serverKey){
141 | return;
142 | }
143 |
144 | if(data.name === '@error'){
145 | if(this._listeners['@error']){
146 | for(let i = 0; i < this._listeners['@error'].length; i++){
147 | this._listeners['@error'][i].call(this, data.data);
148 | }
149 | }
150 |
151 | if(data.data === 'migrate'){
152 | for(let i = 0; i < this._currentListeners.length; i++){
153 | if(!this._currentListeners[i].startsWith('@')){
154 | this.on(this._currentListeners[i]);
155 | }
156 | }
157 | }
158 | return;
159 | }
160 |
161 | if(this._listeners[data.name]){
162 | for(let i = 0; i < this._listeners[data.name].length; i++){
163 | this._listeners[data.name][i].call(this, data.data);
164 | }
165 | }
166 | }
167 | };
168 |
169 | this._socketFuncs = {
170 | onConnect,
171 | onDisconnect,
172 | onMessage,
173 | };
174 | Object.freeze(this._socketFuncs);
175 |
176 | socket.addEventListener('open', onConnect, {passive: true});
177 | socket.addEventListener('close', onDisconnect, {passive: true});
178 | socket.addEventListener('error', onDisconnect, {passive: true});
179 | socket.addEventListener('message', onMessage, {passive: true});
180 | }
181 |
182 | async _socketSend(data){
183 | // fix for sending multiple strings at the same time causing overwriting data issues
184 | if(typeof data !== 'string'){
185 | return;
186 | }
187 |
188 | while(this._sendingData > 0){
189 | await new Promise(r => setTimeout(r, 100));
190 | }
191 | this._sendingData++;
192 |
193 | await new Promise(r => setTimeout(r, 10));
194 | if(this._sendingData > 1){
195 | this._sendingData--;
196 | this._socketSend(json);
197 | return;
198 | }
199 |
200 | this.socket.send(data);
201 |
202 | await new Promise(r => setTimeout(r, 100));
203 | this._sendingData--;
204 | }
205 |
206 | async connect(cb){
207 | setTimeout(async () => {
208 | // on connect
209 | if(typeof cb === 'function'){
210 | while(!this._data){
211 | await new Promise(r => setTimeout(r, 100));
212 | }
213 |
214 | if(!this._listeners['@connect']){
215 | this._listeners['@connect'] = [];
216 | }
217 | this._listeners['@connect'].push(cb);
218 |
219 | // @args: 'initial connection' -> bool
220 | cb.call(this, true);
221 | return;
222 | }
223 |
224 | if(this._data){
225 | return;
226 | }
227 |
228 | let now = Date.now();
229 | if(now - this.lastConnect < 10000){
230 | setTimeout(() => {
231 | this.connect(cb);
232 | }, now - this.lastConnect);
233 | return;
234 | }
235 |
236 | this.lastConnect = now;
237 |
238 | // run reconnect
239 | this.socket = new WebSocket(this.origin + '/ws');
240 | this.socket.addEventListener('open', this._socketFuncs.onConnect, {passive: true});
241 | this.socket.addEventListener('close', this._socketFuncs.onDisconnect, {passive: true});
242 | this.socket.addEventListener('error', this._socketFuncs.onDisconnect, {passive: true});
243 | this.socket.addEventListener('message', this._socketFuncs.onMessage, {passive: true});
244 |
245 | while(!this._data){
246 | await new Promise(r => setTimeout(r, 100));
247 | }
248 |
249 | if(cb === '@retry' && this._oldData){
250 | this._socketSend(JSON.stringify({
251 | name: "@connection",
252 | data: 'migrate',
253 | token: this._data.token,
254 | oldClient: this._oldData.clientID,
255 | oldToken: this._oldData.token,
256 | oldServerKey: this._oldData.serverKey,
257 | oldEncKey: this._oldData.encKey,
258 | }));
259 | this._oldData = undefined;
260 | }
261 |
262 | if(this._listeners['@connect']){
263 | for(let i = 0; i < this._listeners['@connect'].length; i++){
264 | this._listeners['@connect'][i].call(this, false);
265 | }
266 | }
267 | }, 100);
268 |
269 | return this;
270 | }
271 |
272 | async disconnect(cb){
273 | // on disconnect
274 | if(typeof cb === 'function'){
275 | if(!this._listeners['@disconnect']){
276 | this._listeners['@disconnect'] = [];
277 | }
278 | this._listeners['@disconnect'].push(cb);
279 | return this;
280 | }
281 |
282 | while(!this._data){
283 | await new Promise(r => setTimeout(r, 100));
284 | }
285 |
286 | let code = 1000;
287 | if(typeof cb === 'number'){
288 | code = Number(cb);
289 | }
290 | if(code < 1000){
291 | code += 1000;
292 | }
293 |
294 | this._disconnectCode = code;
295 |
296 | // run disconnect
297 | this._socketSend(JSON.stringify({
298 | name: "@connection",
299 | data: 'disconnect',
300 | token: this._data.token,
301 | code: code,
302 | }));
303 |
304 | setTimeout(() => {
305 | if(this.connected){
306 | this.socket.close(code);
307 | }
308 | }, 1000);
309 |
310 | return this;
311 | }
312 |
313 | async on(name, cb){
314 | if(typeof name !== 'string' || typeof name.toString() !== 'string'){
315 | return this;
316 | }
317 | name = name.toString().replace(/[^\w_-]+/g, '');
318 | if(name === ''){
319 | return this;
320 | }
321 |
322 | if(!this._listeners[name]){
323 | this._listeners[name] = [];
324 | }
325 |
326 | if(!this._currentListeners.includes(name)){
327 | this._currentListeners.push(name);
328 | }
329 |
330 | if(typeof cb === 'function'){
331 | this._listeners[name].push(cb);
332 | }
333 |
334 | while(!this._data){
335 | await new Promise(r => setTimeout(r, 100));
336 | }
337 |
338 | this._socketSend(JSON.stringify({
339 | name: "@listener",
340 | data: name,
341 | token: this._data.token,
342 | }));
343 |
344 | return this;
345 | }
346 |
347 | async off(name, delCB = true){
348 | if(typeof name !== 'string' || typeof name.toString() !== 'string'){
349 | return this;
350 | }
351 | name = name.toString().replace(/[^\w_-]+/g, '');
352 | if(name === ''){
353 | return this;
354 | }
355 |
356 | if(this._listeners[name] && delCB){
357 | delete this._listeners[name];
358 | }
359 |
360 | let ind = this._currentListeners.indexOf(name);
361 | if(ind !== -1){
362 | this._currentListeners.splice(ind, 1);
363 | }
364 |
365 | while(!this._data){
366 | await new Promise(r => setTimeout(r, 100));
367 | }
368 |
369 | this._socketSend(JSON.stringify({
370 | name: "@listener",
371 | data: '!'+name,
372 | token: this._data.token,
373 | }));
374 |
375 | return this;
376 | }
377 |
378 | async send(name, msg){
379 | if(typeof name !== 'string' || typeof name.toString() !== 'string' || typeof msg === 'function' /* prevent functions */){
380 | return this;
381 | }
382 | name = name.toString().replace(/[^\w_-]+/g, '');
383 | if(name === ''){
384 | return this;
385 | }
386 |
387 | while(!this._data){
388 | await new Promise(r => setTimeout(r, 100));
389 | }
390 |
391 | let json = JSON.stringify({
392 | name: name,
393 | data: msg,
394 | token: this._data.token,
395 | });
396 |
397 | if(this._data.compress === 1){
398 | try {
399 | let enc = btoa(pako.gzip(json, {to: 'string'}));
400 | if(enc && enc !== ''){
401 | json = enc;
402 | }
403 | } catch(e) {}
404 | }
405 |
406 | this._socketSend(json);
407 |
408 | return this;
409 | }
410 |
411 | async error(cb){
412 | if(typeof cb === 'function'){
413 | if(!this._listeners['@error']){
414 | this._listeners['@error'] = [];
415 | }
416 | this._listeners['@error'].push(cb);
417 | }
418 |
419 | return this;
420 | }
421 | }
422 |
--------------------------------------------------------------------------------
/websocket.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "encoding/base64"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "reflect"
9 | "strings"
10 | "time"
11 |
12 | "github.com/AspieSoft/go-regex-re2/v2"
13 | goutil_GZIP "github.com/AspieSoft/goutil/compress/gzip"
14 | "github.com/AspieSoft/goutil/crypt"
15 | "github.com/AspieSoft/goutil/v7"
16 | "github.com/alphadose/haxmap"
17 | "golang.org/x/net/websocket"
18 | )
19 |
20 | const goRoutineLimiter int = 10
21 |
22 | type listener struct {
23 | name string
24 | cbClient *func(client *Client)
25 | cbMsg *func(msg interface{})
26 | cbClientMsg *func(client *Client, msg interface{})
27 | cbCode *func(code int)
28 | cbClientCode *func(client *Client, code int)
29 | }
30 |
31 | // Client of a websocket
32 | type Client struct {
33 | ws *websocket.Conn
34 | ip string
35 | token string
36 | serverKey string
37 | encKey string
38 | ClientID string
39 | listeners []string
40 | serverListeners []listener
41 | close bool
42 | compress uint8
43 | connLost int64
44 | Store map[string]interface{}
45 |
46 | gzip *bool
47 | }
48 |
49 | // Server for a websocket
50 | type Server struct {
51 | origin string
52 | clients *haxmap.Map[string, *Client]
53 | serverListeners []listener
54 | uuidSize int
55 |
56 | // ReqSize defines number of kilobytes (KB) that can be sent by a client at a time
57 | //
58 | // default: 1024 (1MB)
59 | ReqSize uint32
60 |
61 | // Gzip defines whether or not gzip compression is enabled for the websocket server
62 | Gzip bool
63 | }
64 |
65 | // ErrLog contains a list of client errors which you can handle any way you would like
66 | var ErrLog []error = []error{}
67 |
68 | // GzipEnabled defines whether or not gzip compression is enabled be default for new websocket servers
69 | //
70 | // Deprecated: please use `Server.Gzip` instead
71 | var GzipEnabled = true
72 |
73 | var logErr bool
74 |
75 | func newErr(name string, err ...error){
76 | resErr := name
77 |
78 | // ErrLog = append(ErrLog, errors.New(name))
79 | for _, e := range err {
80 | // ErrLog = append(ErrLog, e)
81 |
82 | resErr += e.Error()
83 | }
84 |
85 | ErrLog = append(ErrLog, errors.New(resErr))
86 | }
87 |
88 | // LogErrors can be used if you would like client errors to be logged with fmt.Println
89 | //
90 | // By default, these errors will not be logged
91 | func LogErrors(){
92 | if logErr {
93 | return
94 | }
95 | logErr = true
96 |
97 | go func(){
98 | for {
99 | for len(ErrLog) != 0 {
100 | err := ErrLog[0]
101 | ErrLog = ErrLog[1:]
102 | fmt.Println(err)
103 | }
104 | time.Sleep(10 * time.Millisecond)
105 | }
106 | }()
107 | }
108 |
109 | // NewServer creates a new server
110 | //
111 | // @origin enforces a specific http/https host to be accepted, and rejects connections from other hosts
112 | func NewServer(origin string, reconnectTimeout ...time.Duration) *Server {
113 | server := Server{
114 | origin: origin,
115 | clients: haxmap.New[string, *Client](),
116 | uuidSize: 16,
117 |
118 | ReqSize: 1024,
119 | Gzip: GzipEnabled,
120 | }
121 |
122 | timeout := int64(30 * time.Second)
123 | if len(reconnectTimeout) != 0 {
124 | timeout = int64(reconnectTimeout[0])
125 | }
126 |
127 | go func(){
128 | time.Sleep(1 * time.Second)
129 |
130 | now := time.Now().UnixNano()
131 | server.clients.ForEach(func(clientID string, client *Client) bool {
132 | if client.close && now - client.connLost > timeout {
133 | server.clients.Del(clientID)
134 | }
135 | return true
136 | })
137 | }()
138 |
139 | return &server
140 | }
141 |
142 | func (s *Server) handleWS(ws *websocket.Conn){
143 | if addr := ws.RemoteAddr(); addr.Network() != "websocket" || addr.String() != s.origin {
144 | newErr("connection unexpected origin: '"+addr.Network()+"', '"+addr.String()+"'")
145 | return
146 | }
147 |
148 | clientID := s.clientUUID()
149 | token := string(crypt.RandBytes(32))
150 | serverKey := string(crypt.RandBytes(32))
151 | encKey := string(crypt.RandBytes(64))
152 |
153 | client := Client{
154 | ws: ws,
155 | ip: ws.Request().RemoteAddr,
156 | ClientID: clientID,
157 | token: token,
158 | serverKey: serverKey,
159 | encKey: encKey,
160 | Store: map[string]interface{}{},
161 |
162 | gzip: &s.Gzip,
163 | }
164 |
165 | s.clients.Set(clientID, &client)
166 |
167 | json, err := goutil.JSON.Stringify(map[string]interface{}{
168 | "name": "@connection",
169 | "data": "connect",
170 | "clientID": clientID,
171 | "token": token,
172 | "serverKey": serverKey,
173 | "encKey": encKey,
174 | "canCompress": s.Gzip,
175 | })
176 | if err != nil {
177 | newErr("connection parse err:", err)
178 | return
179 | }
180 | ws.Write(json)
181 |
182 | s.readLoop(ws, &client)
183 | }
184 |
185 | func (s *Server) readLoop(ws *websocket.Conn, client *Client) {
186 | buf := make([]byte, (int64(s.ReqSize) * 1024) + 1024)
187 | for !client.close {
188 | b, err := ws.Read(buf)
189 | if err != nil {
190 | if err == io.EOF {
191 | if !client.close {
192 |
193 | limiter := make(chan int, goRoutineLimiter)
194 | for _, l := range s.serverListeners {
195 | limiter <- 1
196 | go func(l listener){
197 | if l.name == "@disconnect" {
198 | cb := l.cbClientCode
199 | if cb != nil {
200 | time.Sleep(100 * time.Millisecond)
201 | (*cb)(client, 1006)
202 | }
203 | }
204 | <-limiter
205 | }(l)
206 | }
207 |
208 | for _, l := range client.serverListeners {
209 | limiter <- 1
210 | go func(l listener){
211 | if l.name == "@disconnect" {
212 | cb := l.cbCode
213 | if cb != nil {
214 | time.Sleep(100 * time.Millisecond)
215 | (*cb)(1006)
216 | }
217 | }
218 | <-limiter
219 | }(l)
220 | }
221 | }
222 |
223 | client.connLost = time.Now().UnixNano()
224 | client.close = true
225 | // s.clients.Del(client.ClientID)
226 | break
227 | }
228 |
229 | if client.close {
230 | break
231 | }
232 |
233 | newErr("read err:", err)
234 | continue
235 | }
236 |
237 | msg := buf[:b]
238 |
239 | go func(){
240 | msg = goutil.Clean.Bytes(msg)
241 | if s.Gzip {
242 | gunzip(&msg)
243 | }
244 |
245 | json, err := goutil.JSON.Parse(goutil.Clean.Bytes(msg))
246 | if err != nil {
247 | newErr("read parse err:", err)
248 | return
249 | }
250 |
251 | if reflect.TypeOf(json["token"]) != goutil.VarType["string"] {
252 | newErr("read invalid token: not a valid string")
253 | return
254 | }else if json["token"].(string) != client.token {
255 | newErr("read invalid token: '"+json["token"].(string)+"' != '"+client.token+"'")
256 | return
257 | }
258 |
259 | if reflect.TypeOf(json["name"]) != goutil.VarType["string"] {
260 | newErr("read invalid name: not a valid string")
261 | return
262 | }
263 | name := json["name"].(string)
264 |
265 | if name == "@connection" {
266 | if reflect.TypeOf(json["data"]) != goutil.VarType["string"] {
267 | newErr("read listener invalid data: not a valid string")
268 | return
269 | }
270 | data := json["data"].(string)
271 |
272 | if data == "connect" {
273 | client.compress = uint8(goutil.Conv.ToUint(json["compress"]))
274 |
275 | limiter := make(chan int, goRoutineLimiter)
276 | for _, l := range s.serverListeners {
277 | limiter <- 1
278 | go func(l listener){
279 | if l.name == "@connect" {
280 | cb := l.cbClient
281 | if cb != nil {
282 | time.Sleep(100 * time.Millisecond)
283 | (*cb)(client)
284 | }
285 | }
286 | <-limiter
287 | }(l)
288 | }
289 | }else if data == "disconnect" {
290 | code := goutil.Conv.ToInt(json["code"])
291 | if code < 1000 {
292 | code += 1000
293 | }
294 |
295 | limiter := make(chan int, goRoutineLimiter)
296 | for _, l := range s.serverListeners {
297 | limiter <- 1
298 | go func(l listener){
299 | if l.name == "@disconnect" {
300 | cb := l.cbClientCode
301 | if cb != nil {
302 | time.Sleep(100 * time.Millisecond)
303 | (*cb)(client, code)
304 | }
305 | }
306 | <-limiter
307 | }(l)
308 | }
309 |
310 | for _, l := range client.serverListeners {
311 | limiter <- 1
312 | go func(l listener){
313 | if l.name == "@disconnect" {
314 | cb := l.cbCode
315 | if cb != nil {
316 | time.Sleep(100 * time.Millisecond)
317 | (*cb)(code)
318 | }
319 | }
320 | <-limiter
321 | }(l)
322 | }
323 |
324 | if code == 1000 {
325 | s.clients.Del(client.ClientID)
326 | }else{
327 | client.connLost = time.Now().UnixNano()
328 | }
329 |
330 | client.close = true
331 | ws.Close()
332 | }else if data == "migrate" {
333 | oldClientID := goutil.Conv.ToString(json["oldClient"])
334 | oldToken := goutil.Conv.ToString(json["oldToken"])
335 | oldServerKey := goutil.Conv.ToString(json["oldServerKey"])
336 | oldEncKey := goutil.Conv.ToString(json["oldEncKey"])
337 |
338 | if oldClient, ok := s.clients.Get(oldClientID); ok && oldClient.close && oldClient.token == oldToken && oldClient.serverKey == oldServerKey && oldClient.encKey == oldEncKey && oldClient.ip == client.ip {
339 | // migrate old client data to new client
340 | for _, l := range oldClient.listeners {
341 | client.listeners = append(client.listeners, l)
342 | }
343 |
344 | for _, sl := range oldClient.serverListeners {
345 | client.serverListeners = append(client.serverListeners, sl)
346 | }
347 |
348 | for k, s := range oldClient.Store {
349 | if client.Store[k] == nil {
350 | client.Store[k] = s
351 | }
352 | }
353 | }else{
354 | client.sendCore("@error", "migrate")
355 | }
356 | }
357 | }else if name == "@listener" {
358 | if reflect.TypeOf(json["data"]) != goutil.VarType["string"] {
359 | newErr("read listener invalid data: not a valid string")
360 | return
361 | }
362 | data := json["data"].(string)
363 |
364 | go func(){
365 | if strings.HasPrefix(data, "!") {
366 | data = data[1:]
367 | for i := 0; i < len(client.listeners); i++ {
368 | if client.listeners[i] == data {
369 | client.listeners = append(client.listeners[:i], client.listeners[i+1:]...)
370 | break
371 | }
372 | }
373 | }else if !goutil.Contains(client.listeners, data) {
374 | client.listeners = append(client.listeners, data)
375 | }
376 | }()
377 | }else{
378 | limiter := make(chan int, goRoutineLimiter)
379 | for _, l := range s.serverListeners {
380 | limiter <- 1
381 | go func(l listener){
382 | if l.name == name {
383 | cb := l.cbClientMsg
384 | if cb != nil {
385 | (*cb)((client), json["data"])
386 | }
387 | }
388 | <-limiter
389 | }(l)
390 | }
391 |
392 | for _, l := range client.serverListeners {
393 | limiter <- 1
394 | go func(l listener){
395 | if l.name == name {
396 | cb := l.cbMsg
397 | if cb != nil {
398 | (*cb)(json["data"])
399 | }
400 | }
401 | <-limiter
402 | }(l)
403 | }
404 | }
405 | }()
406 | }
407 |
408 | // s.clients.Del(client.ClientID)
409 | }
410 |
411 | // Handler should be passed into your http handler
412 | //
413 | // http.Handle("/ws", server.Handler())
414 | func (s *Server) Handler() websocket.Handler {
415 | return websocket.Handler(s.handleWS)
416 | }
417 |
418 | // Broadcast sends a message to every client
419 | func (s *Server) Broadcast(name string, msg interface{}) {
420 | limiter := make(chan int, goRoutineLimiter)
421 | s.clients.ForEach(func(token string, client *Client) bool {
422 | limiter <- 1
423 | go func(){
424 | client.Send(name, msg)
425 | <-limiter
426 | }()
427 | return true
428 | })
429 | }
430 |
431 | // Send sends a message to a specific client
432 | func (s *Server) Send(clientID string, name string, msg interface{}){
433 | if client, ok := s.clients.Get(clientID); ok {
434 | client.Send(name, msg)
435 | }
436 | }
437 |
438 | // Send sends a message to the client
439 | func (c *Client) Send(name string, msg interface{}){
440 | if c.close {
441 | return
442 | }
443 |
444 | name = string(regex.Comp(`[^\w_-]+`).RepStrLit([]byte(name), []byte{}))
445 |
446 | if !goutil.Contains(c.listeners, name) {
447 | return
448 | }
449 |
450 | json, err := goutil.JSON.Stringify(map[string]interface{}{
451 | "name": name,
452 | "data": msg,
453 | "token": c.serverKey,
454 | })
455 | if err != nil {
456 | newErr("write parse err:", err)
457 | return
458 | }
459 |
460 | if *c.gzip && c.compress == 1 {
461 | gzip(&json)
462 | }
463 |
464 | c.ws.Write(json)
465 | }
466 |
467 | // Send sends a message to the client
468 | //
469 | // This method allows sending @name listeners
470 | func (c *Client) sendCore(name string, msg interface{}){
471 | if c.close {
472 | return
473 | }
474 |
475 | json, err := goutil.JSON.Stringify(map[string]interface{}{
476 | "name": name,
477 | "data": msg,
478 | "token": c.serverKey,
479 | })
480 | if err != nil {
481 | newErr("write parse err:", err)
482 | }
483 |
484 | if *c.gzip && c.compress == 1 {
485 | gzip(&json)
486 | }
487 |
488 | c.ws.Write(json)
489 | }
490 |
491 | // Connect runs your callback when a new client connects to the websocket
492 | func (s *Server) Connect(cb func(client *Client)){
493 | s.serverListeners = append(s.serverListeners, listener{
494 | name: "@connect",
495 | cbClient: &cb,
496 | })
497 | }
498 |
499 | // On runs your callback when any client a message of the same name
500 | func (s *Server) On(name string, cb func(client *Client, msg interface{})){
501 | name = string(regex.Comp(`[^\w_-]+`).RepStrLit([]byte(name), []byte{}))
502 |
503 | s.serverListeners = append(s.serverListeners, listener{
504 | name: name,
505 | cbClientMsg: &cb,
506 | })
507 | }
508 |
509 | // On runs your callback when the client sends a message of the same name
510 | func (c *Client) On(name string, cb func(msg interface{})){
511 | name = string(regex.Comp(`[^\w_-]+`).RepStrLit([]byte(name), []byte{}))
512 |
513 | c.serverListeners = append(c.serverListeners, listener{
514 | name: name,
515 | cbMsg: &cb,
516 | })
517 | }
518 |
519 | // Disconnect runs your callback when any client disconnects from the websocket
520 | func (s *Server) Disconnect(cb func(client *Client, code int)){
521 | s.serverListeners = append(s.serverListeners, listener{
522 | name: "@disconnect",
523 | cbClientCode: &cb,
524 | })
525 | }
526 |
527 | // Disconnect runs your callback when the client disconnects from the websocket
528 | func (c *Client) Disconnect(cb func(code int)){
529 | c.serverListeners = append(c.serverListeners, listener{
530 | name: "@disconnect",
531 | cbCode: &cb,
532 | })
533 | }
534 |
535 | // Exit will force a specific client to disconnect from the websocket
536 | func (s *Server) Kick(clientID string, code int){
537 | if client, ok := s.clients.Get(clientID); ok {
538 | client.Kick(code)
539 | }
540 | }
541 |
542 | // ExitAll will force every client to disconnect from the websocket
543 | func (s *Server) KickAll(code int){
544 | limiter := make(chan int, goRoutineLimiter)
545 | s.clients.ForEach(func(token string, client *Client) bool {
546 | limiter <- 1
547 | go func(){
548 | client.Kick(code)
549 | <-limiter
550 | }()
551 | return true
552 | })
553 | }
554 |
555 | // ExitAll will force the client to disconnect from the websocket
556 | func (c *Client) Kick(code int){
557 | c.close = true
558 | if code < 1000 {
559 | code += 1000
560 | }
561 | c.ws.WriteClose(code)
562 | }
563 |
564 | // ToType attempts to converts any interface{} from the many possible json outputs, to a specific type of your choice
565 | //
566 | // if it fails to convert, it will return a nil/zero value for the appropriate type
567 | //
568 | // Unlike 'websocket.MsgType' This method now returns the actual type, in place of returning an interface{} with that type
569 | func ToType[T goutil.SupportedType] (msg interface{}) T {
570 | return goutil.ToType[T](msg)
571 | }
572 |
573 | // MsgType attempts to converts any interface{} from the many possible json outputs, to a specific type of your choice
574 | //
575 | // if it fails to convert, it will return a nil/zero value for the appropriate type
576 | //
577 | // recommended: add .(string|[]byte|int|etc) to the end of the function to get that type output in place of interface{}
578 | //
579 | // Deprecated: Please use 'websocket.ToType' instead
580 | func MsgType[T goutil.SupportedType] (msg interface{}) interface{} {
581 | return goutil.ToType[T](msg)
582 | }
583 |
584 | func (s *Server) clientUUID() string {
585 | uuid := crypt.RandBytes(s.uuidSize)
586 |
587 | var hasID bool
588 | _, hasID = s.clients.Get(string(uuid))
589 |
590 | loops := 1000
591 | for hasID && loops > 0 {
592 | loops--
593 | uuid = crypt.RandBytes(s.uuidSize)
594 | _, hasID = s.clients.Get(string(uuid))
595 | }
596 |
597 | if hasID {
598 | s.uuidSize++
599 | return s.clientUUID()
600 | }
601 |
602 | return string(uuid)
603 | }
604 |
605 | func gzip(b *[]byte) {
606 | if comp, err := goutil_GZIP.Zip(*b); err == nil {
607 | *b = []byte(base64.StdEncoding.EncodeToString(comp))
608 | }
609 | }
610 |
611 | func gunzip(b *[]byte) {
612 | if dec, err := base64.StdEncoding.DecodeString(string(*b)); err == nil {
613 | if dec, err = goutil_GZIP.UnZip(dec); err == nil {
614 | *b = dec
615 | }
616 | }
617 | }
618 |
--------------------------------------------------------------------------------