├── .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 | [![donation link](https://img.shields.io/badge/buy%20me%20a%20coffee-paypal-blue)](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 | --------------------------------------------------------------------------------