├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── acl.go ├── acl_test.go ├── client.go ├── doc.go ├── fibonacci.go ├── message.go ├── message_test.go ├── queue.go ├── samples ├── queue │ └── main.go ├── samples.md └── topic │ └── main.go ├── server.go ├── stack.go ├── stack_test.go └── topic.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | 6 | install: 7 | - go get ./... 8 | - go get -t 9 | 10 | script: 11 | - go test -v ./... 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2016 François SAMIN 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-wsqueue 2 | 3 | ![Current Status](https://img.shields.io/badge/current%20status-WIP-orange.svg) [![Build Status](https://travis-ci.org/fsamin/go-wsqueue.svg?branch=master)](https://travis-ci.org/fsamin/go-wsqueue) [![Godoc Status](https://img.shields.io/badge/godoc-available-brightgreen.svg)](https://godoc.org/github.com/fsamin/go-wsqueue) 4 | 5 | ## Introduction 6 | 7 | - Golang API over gorilla/mux and gorilla/websocket 8 | - Kind of AMQP but over websockets 9 | 10 | ### Publish & Subscribe Pattern 11 | 12 | Publish and subscribe semantics are implemented by Topics. 13 | 14 | When you publish a message it goes to all the subscribers who are interested - so zero to many subscribers will receive a copy of the message. Only subscribers who had an active subscription at the time the broker receives the message will get a copy of the message. 15 | 16 | ### Work queues Pattern 17 | 18 | The main idea behind Work Queues (aka: Task Queues) is to avoid doing a resource-intensive task immediately and having to wait for it to complete. Instead we schedule the task to be done later. We encapsulate a task as a message and send it to the queue. A worker process running in the background will pop the tasks and eventually execute the job. When you run many workers the tasks will be shared between them. 19 | 20 | Queues implement load balancer semantics. A single message will be received by exactly one consumer. If there are no consumers available at the time the message is sent it will be kept until a consumer is available that can process the message. If a consumer receives a message and does not acknowledge it before closing then the message will be redelivered to another consumer. A queue can have many consumers with messages load balanced across the available consumers. 21 | 22 | ## Getting started 23 | 24 | `$ go get github.com/fsamin/go-wsqueue` 25 | 26 | ### Publish & Subscribe 27 | 28 | Run this with 3 terminals : 29 | 30 | `go run main.go -server` 31 | 32 | `go run main.go -client1` 33 | 34 | `go run main.go -client2` 35 | 36 | 37 | main.go : 38 | 39 | ``` 40 | package main 41 | 42 | import ( 43 | "flag" 44 | "log" 45 | "net/http" 46 | "time" 47 | 48 | "github.com/fsamin/go-wsqueue" 49 | "github.com/gorilla/mux" 50 | 51 | "github.com/jmcvetta/randutil" 52 | ) 53 | 54 | var fServer = flag.Bool("server", false, "Run server") 55 | var fClient1 = flag.Bool("client1", false, "Run client #1") 56 | var fClient2 = flag.Bool("client2", false, "Run client #2") 57 | 58 | func main() { 59 | flag.Parse() 60 | forever := make(chan bool) 61 | 62 | if *fServer { 63 | server() 64 | } 65 | if *fClient1 { 66 | client("1") 67 | } 68 | if *fClient2 { 69 | client("2") 70 | } 71 | 72 | <-forever 73 | } 74 | 75 | func server() { 76 | r := mux.NewRouter() 77 | s := wsqueue.NewServer(r, "") 78 | q := s.CreateTopic("topic1") 79 | 80 | q.OpenedConnectionHandler = func(c *wsqueue.Conn) { 81 | log.Println("Welcome " + c.ID) 82 | q.Publish("Welcome " + c.ID) 83 | } 84 | 85 | q.ClosedConnectionHandler = func(c *wsqueue.Conn) { 86 | log.Println("Bye bye " + c.ID) 87 | } 88 | http.Handle("/", r) 89 | go http.ListenAndServe("0.0.0.0:9000", r) 90 | 91 | //Start send message to queue 92 | go func() { 93 | for { 94 | time.Sleep(5 * time.Second) 95 | s, _ := randutil.AlphaString(10) 96 | q.Publish("message from goroutine 1 : " + s) 97 | } 98 | }() 99 | 100 | go func() { 101 | for { 102 | time.Sleep(10 * time.Second) 103 | s, _ := randutil.AlphaString(10) 104 | q.Publish("message from goroutine 2 : " + s) 105 | } 106 | }() 107 | } 108 | 109 | func client(ID string) { 110 | //Connect a client 111 | go func() { 112 | c := &wsqueue.Client{ 113 | Protocol: "ws", 114 | Host: "localhost:9000", 115 | Route: "/", 116 | } 117 | cMessage, cError, err := c.Subscribe("topic1") 118 | if err != nil { 119 | panic(err) 120 | } 121 | for { 122 | select { 123 | case m := <-cMessage: 124 | log.Println("\n\n********* Client " + ID + " *********" + m.String() + "\n******************") 125 | case e := <-cError: 126 | log.Println("\n\n********* Client " + ID + " *********" + e.Error() + "\n******************") 127 | } 128 | } 129 | }() 130 | } 131 | 132 | ``` 133 | 134 | ### Load balanced Work Queues 135 | 136 | Run this with 3 terminals : 137 | 138 | `go run main.go -server` 139 | 140 | `go run main.go -client1` 141 | 142 | `go run main.go -client2` 143 | 144 | 145 | main.go : 146 | 147 | ``` 148 | package main 149 | 150 | import ( 151 | "flag" 152 | "log" 153 | "net/http" 154 | "time" 155 | 156 | "github.com/fsamin/go-wsqueue" 157 | "github.com/gorilla/mux" 158 | 159 | "github.com/jmcvetta/randutil" 160 | ) 161 | 162 | var fServer = flag.Bool("server", false, "Run server") 163 | var fClient1 = flag.Bool("client1", false, "Run client #1") 164 | var fClient2 = flag.Bool("client2", false, "Run client #2") 165 | 166 | func main() { 167 | flag.Parse() 168 | forever := make(chan bool) 169 | 170 | if *fServer { 171 | server() 172 | } 173 | if *fClient1 { 174 | client("1") 175 | } 176 | if *fClient2 { 177 | client("2") 178 | } 179 | 180 | <-forever 181 | } 182 | 183 | func server() { 184 | r := mux.NewRouter() 185 | s := wsqueue.NewServer(r, "") 186 | q := s.CreateQueue("queue1", 10) 187 | 188 | http.Handle("/", r) 189 | go http.ListenAndServe("0.0.0.0:9000", r) 190 | 191 | //Start send message to queue 192 | go func() { 193 | for { 194 | time.Sleep(5 * time.Second) 195 | s, _ := randutil.AlphaString(10) 196 | q.Send("message from goroutine 1 : " + s) 197 | } 198 | }() 199 | 200 | go func() { 201 | for { 202 | time.Sleep(6 * time.Second) 203 | s, _ := randutil.AlphaString(10) 204 | q.Send("message from goroutine 2 : " + s) 205 | } 206 | }() 207 | } 208 | 209 | func client(ID string) { 210 | //Connect a client 211 | go func() { 212 | c := &wsqueue.Client{ 213 | Protocol: "ws", 214 | Host: "localhost:9000", 215 | Route: "/", 216 | } 217 | cMessage, cError, err := c.Listen("queue1") 218 | if err != nil { 219 | panic(err) 220 | } 221 | for { 222 | select { 223 | case m := <-cMessage: 224 | log.Println("\n\n********* Client " + ID + " *********" + m.String() + "\n******************") 225 | case e := <-cError: 226 | log.Println("\n\n********* Client " + ID + " *********" + e.Error() + "\n******************") 227 | } 228 | } 229 | }() 230 | } 231 | 232 | ``` 233 | -------------------------------------------------------------------------------- /acl.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import "net/http" 4 | 5 | //ACL stands for Access Control List. It's a slice of permission for a queue or a topic 6 | type ACL []ACE 7 | 8 | //ACE stands for Access Control Entity 9 | type ACE interface { 10 | Scheme() ACLScheme 11 | } 12 | 13 | //ACEDigest aims to authenticate a user with a username and a password 14 | type ACEDigest struct { 15 | Username string `json:"username,omitempty"` 16 | Password string `json:"password,omitempty"` 17 | } 18 | 19 | //Scheme return WORLD, DIGEST or IP 20 | func (a *ACEDigest) Scheme() ACLScheme { 21 | return ACLSSchemeDigest 22 | } 23 | 24 | //ACEIP aims to authenticate a user with a IP adress 25 | type ACEIP struct { 26 | IP string `json:"ip,omitempty"` 27 | } 28 | 29 | //Scheme is IP 30 | func (a *ACEIP) Scheme() ACLScheme { 31 | return ACLSSchemeIP 32 | } 33 | 34 | //ACEWorld -> everyone 35 | type ACEWorld struct{} 36 | 37 | //Scheme is World 38 | func (a *ACEWorld) Scheme() ACLScheme { 39 | return ACLSSchemeWorld 40 | } 41 | 42 | //ACLScheme : There are three different scheme 43 | type ACLScheme string 44 | 45 | const ( 46 | //ACLSSchemeWorld scheme is a fully open scheme 47 | ACLSSchemeWorld ACLScheme = "WORLD" 48 | //ACLSSchemeDigest scheme represents a "manually" set group of authenticated users. 49 | ACLSSchemeDigest = "DIGEST" 50 | //ACLSSchemeIP scheme represents a "manually" set group of user authenticated by their IP address 51 | ACLSSchemeIP = "IP" 52 | ) 53 | 54 | func checkACL(acl ACL, w http.ResponseWriter, r *http.Request) bool { 55 | for _, ace := range acl { 56 | switch ace.Scheme() { 57 | case ACLSSchemeWorld: 58 | Logfunc("Connection Authorized") 59 | return true 60 | case ACLSSchemeIP: 61 | ip := r.Header.Get("X-Forwarded-For") 62 | aceIP, b := ace.(*ACEIP) 63 | if !b { 64 | w.WriteHeader(http.StatusUnauthorized) 65 | return false 66 | } 67 | if ip == aceIP.IP { 68 | Logfunc("Connection Authorized for IP %s", ip) 69 | return true 70 | } 71 | Warnfunc("Connection unauthorized for IP:%s", ip) 72 | case ACLSSchemeDigest: 73 | u, p, b := r.BasicAuth() 74 | if b { 75 | aceDigest, b := ace.(*ACEDigest) 76 | if b && aceDigest.Username == u && aceDigest.Password == p { 77 | Logfunc("Connection Authorized with BasicAuth %s", u) 78 | return true 79 | } 80 | } 81 | Warnfunc("Connection unauthorized for BasicAuth %s", u) 82 | } 83 | } 84 | w.WriteHeader(http.StatusUnauthorized) 85 | return false 86 | } 87 | -------------------------------------------------------------------------------- /acl_test.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/phayes/freeport" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCheckACLShouldAuthorizeEveryoneWhenACEIsSetToWord(t *testing.T) { 13 | var port = freeport.GetPort() 14 | wait := make(chan bool, 1) 15 | 16 | //setup ACL 17 | acl := ACL{ 18 | &ACEWorld{}, 19 | } 20 | 21 | //setup server 22 | handler := func(w http.ResponseWriter, r *http.Request) { 23 | assert.True(t, checkACL(acl, w, r), "check should return true") 24 | wait <- true 25 | } 26 | http.HandleFunc(fmt.Sprintf("/%d", port), handler) 27 | //Run the server 28 | t.Logf("Starting test server on port %d", port) 29 | go http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 30 | 31 | //Run the test 32 | t.Logf("Calling the test server") 33 | client := http.DefaultClient 34 | res, err := client.Get(fmt.Sprintf("http://localhost:%d/%d", port, port)) 35 | assert.NoError(t, err) 36 | assert.Equal(t, 200, res.StatusCode, "status code should be 200") 37 | 38 | <-wait 39 | } 40 | 41 | func TestCheckACLShouldAuthorizeLocalIPWhenACEIsSetLocalIP(t *testing.T) { 42 | var port = freeport.GetPort() 43 | wait := make(chan bool, 1) 44 | 45 | //setup ACL 46 | acl := ACL{ 47 | &ACEIP{"localhost0"}, 48 | } 49 | 50 | //setup server 51 | handler := func(w http.ResponseWriter, r *http.Request) { 52 | assert.True(t, checkACL(acl, w, r), "check should return true") 53 | wait <- true 54 | } 55 | http.HandleFunc(fmt.Sprintf("/%d", port), handler) 56 | //Run the server 57 | t.Logf("Starting test server on port %d", port) 58 | go http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 59 | 60 | //Run the test 61 | t.Logf("Calling the test server") 62 | client := http.DefaultClient 63 | req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/%d", port, port), nil) 64 | req.Header.Set("X-Forwarded-For", "localhost0") 65 | res, err := client.Do(req) 66 | assert.NoError(t, err) 67 | assert.Equal(t, 200, res.StatusCode, "status code should be 200") 68 | 69 | <-wait 70 | } 71 | 72 | func TestCheckACLShouldUnauthorizeLocalIPWhenACEIPIsSetToFooBar(t *testing.T) { 73 | var port = freeport.GetPort() 74 | wait := make(chan bool, 2) 75 | 76 | //setup ACL 77 | acl := ACL{ 78 | &ACEIP{"FooBar"}, 79 | } 80 | 81 | //setup server 82 | handler := func(w http.ResponseWriter, r *http.Request) { 83 | assert.False(t, checkACL(acl, w, r), "check should return false") 84 | wait <- true 85 | } 86 | http.HandleFunc(fmt.Sprintf("/%d", port), handler) 87 | //Run the server 88 | t.Logf("Starting test server on port %d", port) 89 | go http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 90 | 91 | //Run the test 92 | t.Logf("Calling the test server") 93 | client := http.DefaultClient 94 | req, _ := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/%d", port, port), nil) 95 | req.Header.Set("X-Forwarded-For", "localhost") 96 | res, err := client.Do(req) 97 | assert.NoError(t, err) 98 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode, "status code should be "+string(http.StatusUnauthorized)) 99 | wait <- true 100 | 101 | <-wait 102 | <-wait 103 | } 104 | 105 | func TestCheckACLShouldAuthorizeFooBarWhenACEDigetIsSetToFooBar(t *testing.T) { 106 | var port = freeport.GetPort() 107 | wait := make(chan bool, 1) 108 | 109 | //setup ACL 110 | acl := ACL{ 111 | &ACEDigest{Username: "Foo", Password: "Bar"}, 112 | } 113 | 114 | //setup server 115 | handler := func(w http.ResponseWriter, r *http.Request) { 116 | assert.True(t, checkACL(acl, w, r), "check should return true") 117 | wait <- true 118 | } 119 | http.HandleFunc(fmt.Sprintf("/%d", port), handler) 120 | //Run the server 121 | t.Logf("Starting test server on port %d", port) 122 | go http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 123 | 124 | //Run the test 125 | t.Logf("Calling the test server") 126 | client := http.DefaultClient 127 | req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/%d", port, port), nil) 128 | req.SetBasicAuth("Foo", "Bar") 129 | res, err := client.Do(req) 130 | assert.NoError(t, err) 131 | assert.Equal(t, 200, res.StatusCode, "status code should be 200") 132 | 133 | <-wait 134 | } 135 | 136 | func TestCheckACLShouldUnauthorizeFooBarWhenACEDigetIsSetToXXXXX(t *testing.T) { 137 | var port = freeport.GetPort() 138 | wait := make(chan bool, 2) 139 | 140 | //setup ACL 141 | acl := ACL{ 142 | &ACEDigest{Username: "XXX", Password: "XXX"}, 143 | } 144 | 145 | //setup server 146 | handler := func(w http.ResponseWriter, r *http.Request) { 147 | assert.False(t, checkACL(acl, w, r), "check should return false") 148 | wait <- true 149 | } 150 | http.HandleFunc(fmt.Sprintf("/%d", port), handler) 151 | //Run the server 152 | t.Logf("Starting test server on port %d", port) 153 | go http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 154 | 155 | //Run the test 156 | t.Logf("Calling the test server") 157 | client := http.DefaultClient 158 | req, _ := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/%d", port, port), nil) 159 | req.SetBasicAuth("Foo", "Bar") 160 | res, err := client.Do(req) 161 | assert.NoError(t, err) 162 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode, "status code should be "+string(http.StatusUnauthorized)) 163 | wait <- true 164 | 165 | <-wait 166 | <-wait 167 | } 168 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | //Client is the wqueue entrypoint 14 | type Client struct { 15 | Protocol string 16 | Host string 17 | Route string 18 | dialer *websocket.Dialer 19 | conn *websocket.Conn 20 | } 21 | 22 | type wsqueueType string 23 | 24 | const ( 25 | topic wsqueueType = "topic" 26 | queue = "queue" 27 | ) 28 | 29 | //Subscribe aims to connect to a Topic 30 | func (c *Client) Subscribe(q string) (chan Message, chan error, error) { 31 | Logfunc("Subcribing to Topic %s", q) 32 | chanMessage := make(chan Message) 33 | chanError := make(chan error) 34 | go c.handler(q, chanMessage, chanError, topic) 35 | return chanMessage, chanError, nil 36 | } 37 | 38 | //Listen aims to connect to a Queue 39 | func (c *Client) Listen(q string) (chan Message, chan error, error) { 40 | Logfunc("Listening to Queue %s", q) 41 | chanMessage := make(chan Message) 42 | chanError := make(chan error) 43 | go c.handler(q, chanMessage, chanError, queue) 44 | return chanMessage, chanError, nil 45 | } 46 | 47 | func (c *Client) connect(q string, t wsqueueType) error { 48 | var url = fmt.Sprintf("%s://%s%swsqueue/%s/%s", c.Protocol, c.Host, c.Route, string(t), q) 49 | c.dialer = websocket.DefaultDialer 50 | c.dialer.HandshakeTimeout = 1 * time.Second 51 | 52 | Logfunc("Dialing %s", url) 53 | var err error 54 | c.conn, _, err = c.dialer.Dial(url, http.Header{}) 55 | return err 56 | } 57 | 58 | func (c *Client) reconnect(q string, t wsqueueType, nbRetry int) error { 59 | var i = 0 60 | var f = NewFibonacci() 61 | for { 62 | if i != -1 && i > nbRetry { 63 | Warnfunc("Unable to connect to %s : %s", string(t), q) 64 | return fmt.Errorf("Unable to connect to %s : %s", string(t), q) 65 | } 66 | if c.conn == nil { 67 | i++ 68 | if err := c.connect(q, t); err != nil { 69 | Warnfunc("Waiting before retry connection to %s : %s", string(t), q) 70 | f.WaitForIt(time.Second) 71 | } 72 | } else { 73 | return nil 74 | } 75 | } 76 | } 77 | 78 | func (c *Client) handler(q string, chanMessage chan Message, chanError chan error, t wsqueueType) { 79 | Logfunc("Handling message on %s : %s", string(t), q) 80 | for { 81 | if e := c.reconnect(q, t, 100); e != nil { 82 | chanError <- e 83 | close(chanError) 84 | close(chanMessage) 85 | break 86 | } 87 | _, p, e := c.conn.ReadMessage() 88 | if e != nil { 89 | c.conn = nil 90 | chanError <- e 91 | if !websocket.IsUnexpectedCloseError(e, websocket.CloseMessage) { 92 | close(chanMessage) 93 | close(chanMessage) 94 | break 95 | } 96 | } else { 97 | message := &Message{} 98 | if err := json.Unmarshal(p, message); err != nil { 99 | log.Println(err) 100 | } 101 | message.Header["received"] = time.Now().String() 102 | //FIXME: Ack 103 | chanMessage <- *message 104 | } 105 | } 106 | } 107 | 108 | func (c *Client) Ack(msg *Message) error { 109 | 110 | return nil 111 | } 112 | 113 | func (c *Client) Reply(msg *Message, response *Message) error { 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package wsqueue provides a framework over gorilla/mux and gorilla/websocket to operate 3 | kind of AMQP but over websockets. It offers a Server, a Client and two king of messaging 4 | protocols : Topics and Queues. 5 | 6 | Topics : Publish & Subscribe Pattern 7 | 8 | Publish and subscribe semantics are implemented by Topics. When you publish a message it 9 | goes to all the subscribers who are interested - so zero to many subscribers will receive 10 | a copy of the message. Only subscribers who had an active subscription at the time the broker 11 | receives the message will get a copy of the message. 12 | 13 | Start a server and handle a topic 14 | 15 | //Server side 16 | r := mux.NewRouter() 17 | s := wsqueue.NewServer(r, "") 18 | q := s.CreateTopic("myTopic") 19 | http.Handle("/", r) 20 | go http.ListenAndServe("0.0.0.0:9000", r) 21 | 22 | ... 23 | //Publish a message 24 | q.Publish("This is a message") 25 | 26 | Start a client and listen on a topic 27 | 28 | //Client slide 29 | go func() { 30 | c := &wsqueue.Client{ 31 | Protocol: "ws", 32 | Host: "localhost:9000", 33 | Route: "/", 34 | } 35 | cMessage, cError, err := c.Subscribe("myTopic") 36 | if err != nil { 37 | panic(err) 38 | } 39 | for { 40 | select { 41 | case m := <-cMessage: 42 | fmt.Println(m.String()) 43 | case e := <-cError: 44 | fmt.Println(e.Error()) 45 | } 46 | } 47 | }() 48 | 49 | 50 | Queues : Work queues Pattern 51 | 52 | The main idea behind Work Queues (aka: Task Queues) is to avoid doing a resource-intensive 53 | task immediately and having to wait for it to complete. Instead we schedule the task to be 54 | done later. We encapsulate a task as a message and send it to the queue. A worker process 55 | running in the background will pop the tasks and eventually execute the job. When you run 56 | many workers the tasks will be shared between them. Queues implement load balancer semantics. 57 | A single message will be received by exactly one consumer. If there are no consumers available 58 | at the time the message is sent it will be kept until a consumer is available that can process 59 | the message. If a consumer receives a message and does not acknowledge it before closing then 60 | the message will be redelivered to another consumer. A queue can have many consumers with messages 61 | load balanced across the available consumers. 62 | 63 | Examples 64 | 65 | see samples/queue/main.go, samples/topic/main.go 66 | 67 | 68 | */ 69 | package wsqueue 70 | -------------------------------------------------------------------------------- /fibonacci.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import "time" 4 | 5 | //Fibonacci https://en.wikipedia.org/wiki/Fibonacci_number 6 | type Fibonacci struct { 7 | i int 8 | j int 9 | } 10 | 11 | //NewFibonacci returns a Fibonacci number 12 | func NewFibonacci() Fibonacci { 13 | return Fibonacci{1, 1} 14 | } 15 | 16 | //Next returns the next value 17 | func (f *Fibonacci) Next() int { 18 | r := f.i + f.j 19 | f.i = f.j 20 | f.j = r 21 | return r 22 | } 23 | 24 | //NextDuration returns the next Fibonacci number cast in the wanted duration 25 | func (f *Fibonacci) NextDuration(timeUnit time.Duration) time.Duration { 26 | return time.Duration(int(timeUnit) * f.Next()) 27 | } 28 | 29 | //WaitForIt ... HIMYM 30 | func (f *Fibonacci) WaitForIt(timeUnit time.Duration) { 31 | time.Sleep(f.NextDuration(timeUnit)) 32 | } 33 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "reflect" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/satori/go.uuid" 11 | ) 12 | 13 | type Header map[string]string 14 | 15 | //Message message 16 | type Message struct { 17 | Header Header `json:"metadata,omitempty"` 18 | Body string `json:"data"` 19 | } 20 | 21 | func newMessage(data interface{}) (*Message, error) { 22 | m := Message{ 23 | Header: make(map[string]string), 24 | Body: "", 25 | } 26 | 27 | switch data.(type) { 28 | case string, *string: 29 | m.Header["content-type"] = "string" 30 | m.Body = data.(string) 31 | case int, *int, int32, *int32, int64, *int64: 32 | m.Header["content-type"] = "int" 33 | m.Body = strconv.Itoa(data.(int)) 34 | case bool, *bool: 35 | m.Header["content-type"] = "bool" 36 | m.Body = strconv.FormatBool(data.(bool)) 37 | default: 38 | m.Header["content-type"] = "application/json" 39 | if reflect.TypeOf(data).Kind() == reflect.Ptr { 40 | m.Header["application-type"] = reflect.ValueOf(data).Elem().Type().String() 41 | } else { 42 | m.Header["application-type"] = reflect.ValueOf(data).Type().String() 43 | } 44 | b, err := json.Marshal(data) 45 | if err != nil { 46 | return nil, err 47 | } 48 | m.Body = string(b) 49 | } 50 | m.Header["id"] = uuid.NewV1().String() 51 | m.Header["date"] = time.Now().String() 52 | m.Header["host"], _ = os.Hostname() 53 | return &m, nil 54 | } 55 | 56 | func (m *Message) String() string { 57 | var s string 58 | s = "\n---HEADER---" 59 | for k, v := range m.Header { 60 | s = s + "\n" + k + ":" + v 61 | } 62 | s = s + "\n---BODY---" 63 | s = s + "\n" + m.Body 64 | return s 65 | } 66 | 67 | //ID returns message if 68 | func (m *Message) ID() string { 69 | return m.Header["id"] 70 | } 71 | 72 | //ContentType returns content-type 73 | func (m *Message) ContentType() string { 74 | return m.Header["content-type"] 75 | } 76 | 77 | //ApplicationType returns application-type. Empty if content-type is not application/json 78 | func (m *Message) ApplicationType() string { 79 | return m.Header["application-type"] 80 | } 81 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewMessageShouldBeString(t *testing.T) { 10 | data := "test message" 11 | msg, err := newMessage(data) 12 | assert.NoError(t, err) 13 | assert.NotNil(t, msg) 14 | assert.Equal(t, msg.ContentType(), "string") 15 | } 16 | 17 | func TestNewMessageShouldBeJSON(t *testing.T) { 18 | data := map[string]interface{}{ 19 | "data": "test message", 20 | } 21 | msg, err := newMessage(data) 22 | assert.NoError(t, err) 23 | assert.NotNil(t, msg) 24 | assert.Equal(t, msg.ContentType(), "application/json") 25 | assert.Equal(t, msg.ApplicationType(), "map[string]interface {}") 26 | } 27 | 28 | func TestNewMessageShouldBeJSONEvenIfPointer(t *testing.T) { 29 | data := map[string]interface{}{ 30 | "data": "test message", 31 | } 32 | msg, err := newMessage(&data) 33 | assert.NoError(t, err) 34 | assert.NotNil(t, msg) 35 | assert.Equal(t, msg.ContentType(), "application/json") 36 | assert.Equal(t, msg.ApplicationType(), "map[string]interface {}") 37 | } 38 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | const ( 11 | //MaxUint is the maximum uint on your platform 12 | maxUint = ^uint(0) 13 | //MinUint is the min uint on your platform 14 | minUint = 0 15 | //MaxInt is the max int on your platform 16 | maxInt = int(maxUint >> 1) 17 | //MinInt is the min int on your platform 18 | minInt = -maxInt - 1 19 | ) 20 | 21 | //Queue implements load balancer semantics. A single message will be received by 22 | // exactly one consumer. If there are no consumers available at the time the 23 | // message is sent it will be kept until a consumer is available that can process 24 | // the message. If a consumer receives a message and does not acknowledge it 25 | // before closing then the message will be redelivered to another consumer. 26 | // A queue can have many consumers with messages load balanced across the 27 | // available consumers. 28 | type Queue struct { 29 | Options *Options `json:"options,omitempty"` 30 | Queue string `json:"topic,omitempty"` 31 | newConsumerHandler func(*Conn) 32 | consumerExitedHandler func(*Conn) 33 | ackHandler func(*Conn, *Message) error 34 | mutex *sync.RWMutex 35 | wsConnections map[ConnID]*Conn 36 | acks map[*Message]bool 37 | lb *loadBalancer 38 | store StorageDriver 39 | stopQueue chan bool 40 | } 41 | 42 | //CreateQueue create queue 43 | func (s *Server) CreateQueue(name string, bufferSize int) *Queue { 44 | q, _ := s.newQueue(name, bufferSize) 45 | s.RegisterQueue(q) 46 | return q 47 | } 48 | 49 | func (s *Server) newQueue(name string, bufferSize int) (*Queue, error) { 50 | q := &Queue{ 51 | Queue: name, 52 | mutex: &sync.RWMutex{}, 53 | wsConnections: make(map[ConnID]*Conn), 54 | acks: make(map[*Message]bool), 55 | } 56 | q.lb = &loadBalancer{queue: q, counter: make(map[ConnID]int)} 57 | q.newConsumerHandler = newConsumerHandler(q) 58 | q.consumerExitedHandler = consumerExitedHandler(q) 59 | q.ackHandler = ackHandler(q) 60 | q.store = NewStack() 61 | q.stopQueue = make(chan bool, 1) 62 | q.Options = &Options{Storage: StorageOptions{"capacity": bufferSize}} 63 | return q, nil 64 | } 65 | 66 | //RegisterQueue register 67 | func (s *Server) RegisterQueue(q *Queue) { 68 | Logfunc("Register queue %s on route %s", q.Queue, s.RoutePrefix+"/wsqueue/queue/"+q.Queue) 69 | handler := s.createHandler( 70 | q.mutex, 71 | &q.wsConnections, 72 | &q.newConsumerHandler, 73 | &q.consumerExitedHandler, 74 | &q.ackHandler, 75 | q.Options, 76 | ) 77 | q.store.Open(q.Options) 78 | q.handle(100) 79 | s.Router.HandleFunc(s.RoutePrefix+"/wsqueue/queue/"+q.Queue, handler) 80 | s.QueuesCounter.Add(1) 81 | } 82 | 83 | type loadBalancer struct { 84 | queue *Queue 85 | counter map[ConnID]int 86 | } 87 | 88 | func (lb *loadBalancer) next() (*ConnID, error) { 89 | lb.queue.mutex.Lock() 90 | defer lb.queue.mutex.Unlock() 91 | 92 | if len(lb.queue.wsConnections) == 0 { 93 | return nil, errors.New("No connection available") 94 | } 95 | 96 | var minCounter = maxInt 97 | for id := range lb.queue.wsConnections { 98 | counter := lb.counter[id] 99 | if counter < minCounter { 100 | minCounter = counter 101 | } 102 | } 103 | 104 | for id := range lb.queue.wsConnections { 105 | c := lb.counter[id] 106 | if c == minCounter { 107 | c++ 108 | lb.counter[id] = c 109 | return &id, nil 110 | } 111 | } 112 | return nil, errors.New("No connection available") 113 | } 114 | 115 | //Send send a message 116 | func (q *Queue) Send(data interface{}) error { 117 | m, e := newMessage(data) 118 | if e != nil { 119 | return e 120 | } 121 | if len(q.wsConnections) == 0 { 122 | Logfunc("No consumer, pushing message to stack") 123 | q.store.Push(m) 124 | return nil 125 | } 126 | q.send(m) 127 | return nil 128 | } 129 | 130 | func (q *Queue) send(m *Message) { 131 | connID, err := q.lb.next() 132 | if err != nil { 133 | Warnfunc("Error while sending to %s : %s", *connID, err.Error()) 134 | q.store.Push(m) 135 | } else { 136 | conn := q.wsConnections[*connID] 137 | q.acks[m] = false 138 | b, _ := json.Marshal(m) 139 | q.mutex.Lock() 140 | err := conn.WSConn.WriteMessage(1, b) 141 | q.mutex.Unlock() 142 | if err != nil { 143 | Logfunc("Error while sending to %s : %s", *connID, err.Error()) 144 | q.store.Push(m) 145 | } 146 | } 147 | } 148 | 149 | func (q *Queue) handle(interval int64) { 150 | var cont = true 151 | go func(c *bool) { 152 | for *c { 153 | time.Sleep(time.Duration(interval) * time.Millisecond) 154 | if len(q.wsConnections) > 0 { 155 | data := q.store.Pop() 156 | if data != nil { 157 | m, b := data.(*Message) 158 | if !b { 159 | Warnfunc("Cannot cast %s to message", data) 160 | } 161 | q.send(m) 162 | } 163 | } 164 | } 165 | }(&cont) 166 | go func(c *bool) { 167 | var b bool 168 | //FIXME: test 169 | b = <-q.stopQueue 170 | cont = *c && b 171 | }(&cont) 172 | } 173 | 174 | func newConsumerHandler(q *Queue) func(*Conn) { 175 | return func(c *Conn) { 176 | q.mutex.Lock() 177 | q.lb.counter[c.ID] = 0 178 | //Reinitiatlisation du load balancer 179 | for id := range q.lb.counter { 180 | q.lb.counter[id] = 0 181 | } 182 | q.mutex.Unlock() 183 | } 184 | } 185 | 186 | func consumerExitedHandler(q *Queue) func(*Conn) { 187 | return func(c *Conn) { 188 | q.mutex.Lock() 189 | delete(q.lb.counter, c.ID) 190 | q.mutex.Unlock() 191 | } 192 | } 193 | 194 | func ackHandler(q *Queue) func(*Conn, *Message) error { 195 | return func(c *Conn, m *Message) error { 196 | q.mutex.Lock() 197 | q.acks[m] = true 198 | q.mutex.Unlock() 199 | return nil 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /samples/queue/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/fsamin/go-wsqueue" 10 | "github.com/gorilla/mux" 11 | 12 | "github.com/jmcvetta/randutil" 13 | ) 14 | 15 | var fServer = flag.Bool("server", false, "Run server") 16 | var fClient1 = flag.Bool("client1", false, "Run client #1") 17 | var fClient2 = flag.Bool("client2", false, "Run client #2") 18 | 19 | func main() { 20 | flag.Parse() 21 | forever := make(chan bool) 22 | 23 | if *fServer { 24 | server() 25 | } 26 | if *fClient1 { 27 | client("1") 28 | } 29 | if *fClient2 { 30 | client("2") 31 | } 32 | 33 | <-forever 34 | } 35 | 36 | func server() { 37 | r := mux.NewRouter() 38 | s := wsqueue.NewServer(r, "") 39 | q := s.CreateQueue("queue1", 2) 40 | 41 | http.Handle("/", r) 42 | go http.ListenAndServe("0.0.0.0:9000", r) 43 | 44 | //Start send message to queue 45 | go func() { 46 | for { 47 | time.Sleep(5 * time.Second) 48 | s, _ := randutil.AlphaString(10) 49 | fmt.Println("send") 50 | q.Send("> message from goroutine 1 : " + s) 51 | } 52 | }() 53 | 54 | go func() { 55 | for { 56 | time.Sleep(6 * time.Second) 57 | s, _ := randutil.AlphaString(10) 58 | fmt.Println("send") 59 | q.Send("> message from goroutine 2 : " + s) 60 | } 61 | }() 62 | } 63 | 64 | func client(ID string) { 65 | //Connect a client 66 | go func() { 67 | c := &wsqueue.Client{ 68 | Protocol: "ws", 69 | Host: "localhost:9000", 70 | Route: "/", 71 | } 72 | cMessage, cError, err := c.Listen("queue1") 73 | if err != nil { 74 | panic(err) 75 | } 76 | for { 77 | select { 78 | case m := <-cMessage: 79 | fmt.Println("\n\n********* Client " + ID + " *********" + m.String() + "\n******************") 80 | case e := <-cError: 81 | fmt.Println("\n\n********* Client " + ID + " *********" + e.Error() + "\n******************") 82 | } 83 | } 84 | }() 85 | } 86 | -------------------------------------------------------------------------------- /samples/samples.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsamin/go-wsqueue/2b0d443b92dee3493d6126d3c7802adba598d7cc/samples/samples.md -------------------------------------------------------------------------------- /samples/topic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/fsamin/go-wsqueue" 10 | "github.com/gorilla/mux" 11 | 12 | "github.com/jmcvetta/randutil" 13 | ) 14 | 15 | var fServer = flag.Bool("server", false, "Run server") 16 | var fClient1 = flag.Bool("client1", false, "Run client #1") 17 | var fClient2 = flag.Bool("client2", false, "Run client #2") 18 | 19 | func main() { 20 | flag.Parse() 21 | forever := make(chan bool) 22 | 23 | if *fServer { 24 | server() 25 | } 26 | if *fClient1 { 27 | client("1") 28 | } 29 | if *fClient2 { 30 | client("2") 31 | } 32 | 33 | <-forever 34 | } 35 | 36 | func server() { 37 | r := mux.NewRouter() 38 | s := wsqueue.NewServer(r, "") 39 | q := s.CreateTopic("topic1") 40 | 41 | q.OpenedConnectionHandler = func(c *wsqueue.Conn) { 42 | fmt.Println("Welcome " + c.ID) 43 | q.Publish("Welcome " + c.ID) 44 | } 45 | 46 | q.ClosedConnectionHandler = func(c *wsqueue.Conn) { 47 | fmt.Println("Bye bye " + c.ID) 48 | } 49 | http.Handle("/", r) 50 | go http.ListenAndServe("0.0.0.0:9000", r) 51 | 52 | //Start send message to queue 53 | go func() { 54 | for { 55 | time.Sleep(5 * time.Second) 56 | s, _ := randutil.AlphaString(10) 57 | q.Publish("message from goroutine 1 : " + s) 58 | } 59 | }() 60 | 61 | go func() { 62 | for { 63 | time.Sleep(10 * time.Second) 64 | s, _ := randutil.AlphaString(10) 65 | q.Publish("message from goroutine 2 : " + s) 66 | } 67 | }() 68 | } 69 | 70 | func client(ID string) { 71 | //Connect a client 72 | go func() { 73 | c := &wsqueue.Client{ 74 | Protocol: "ws", 75 | Host: "localhost:9000", 76 | Route: "/", 77 | } 78 | cMessage, cError, err := c.Subscribe("topic1") 79 | if err != nil { 80 | panic(err) 81 | } 82 | for { 83 | select { 84 | case m := <-cMessage: 85 | fmt.Println("\n\n********* Client " + ID + " *********" + m.String() + "\n******************") 86 | case e := <-cError: 87 | fmt.Println("\n\n********* Client " + ID + " *********" + e.Error() + "\n******************") 88 | } 89 | } 90 | }() 91 | } 92 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import ( 4 | "encoding/json" 5 | "expvar" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/gorilla/websocket" 13 | 14 | "github.com/satori/go.uuid" 15 | ) 16 | 17 | //Logfunc is a function that logs the provided message with optional 18 | //fmt.Sprintf-style arguments. By default, logs to the default log.Logger. 19 | //setting it to nil can be used to disable logging for this package. 20 | //This doesn’t enforce a coupling with any specific external package 21 | //and is already widely supported by existing loggers. 22 | var Logfunc = log.Printf 23 | 24 | //Warnfunc is a function that logs the provided message with optional 25 | //fmt.Sprintf-style arguments. By default, logs to the default log.Logger. 26 | //setting it to nil can be used to disable logging for this package. 27 | //This doesn’t enforce a coupling with any specific external package 28 | //and is already widely supported by existing loggers. 29 | var Warnfunc = log.Printf 30 | 31 | //Server is a server 32 | type Server struct { 33 | Router *mux.Router 34 | RoutePrefix string 35 | QueuesCounter *expvar.Int 36 | TopicsCounter *expvar.Int 37 | ClientsCounter *expvar.Int 38 | MessagesCounter *expvar.Int 39 | } 40 | 41 | //StorageDriver is in-memory Stack or Redis server 42 | type StorageDriver interface { 43 | Open(options *Options) 44 | Push(data interface{}) 45 | Pop() interface{} 46 | } 47 | 48 | //Options is options on topic or queues 49 | type Options struct { 50 | ACL ACL `json:"acl,omitempty"` 51 | Storage StorageOptions `json:"storage,omitempty"` 52 | } 53 | 54 | //StorageOptions is a collection of options, see storage documentation 55 | type StorageOptions map[string]interface{} 56 | 57 | //ConnID a a connection ID 58 | type ConnID string 59 | 60 | //Conn is a conn 61 | type Conn struct { 62 | ID ConnID 63 | WSConn *websocket.Conn 64 | } 65 | 66 | var upgrader = websocket.Upgrader{ 67 | CheckOrigin: func(r *http.Request) bool { return true }, 68 | } 69 | 70 | //NewServer init a new WSQueue server 71 | func NewServer(router *mux.Router, routePrefix string) *Server { 72 | s := &Server{ 73 | Router: router, 74 | RoutePrefix: routePrefix, 75 | } 76 | router.HandleFunc(routePrefix+"/vars", varsHandler) 77 | if routePrefix != "" { 78 | routePrefix = "." + routePrefix 79 | } 80 | s.QueuesCounter = expvar.NewInt("wsqueue" + routePrefix + ".stats.queues.counter") 81 | s.TopicsCounter = expvar.NewInt("wsqueue" + routePrefix + ".stats.topics.counter") 82 | s.ClientsCounter = expvar.NewInt("wsqueue" + routePrefix + ".stats.clients.counter") 83 | s.MessagesCounter = expvar.NewInt("wsqueue" + routePrefix + ".stats.messages.counter") 84 | 85 | return s 86 | } 87 | func (s *Server) createHandler( 88 | mutex *sync.RWMutex, 89 | wsConnections *map[ConnID]*Conn, 90 | openedConnectionCallback *func(*Conn), 91 | closedConnectionCallback *func(*Conn), 92 | onMessageCallback *func(*Conn, *Message) error, 93 | options *Options, 94 | ) func( 95 | w http.ResponseWriter, 96 | r *http.Request, 97 | ) { 98 | return func(w http.ResponseWriter, r *http.Request) { 99 | 100 | if options != nil && len(options.ACL) > 0 { 101 | if !checkACL(options.ACL, w, r) { 102 | Warnfunc("Not Authorized by ACL") 103 | w.Write([]byte("Not Authorized by ACL")) 104 | return 105 | } 106 | } 107 | 108 | c, err := upgrader.Upgrade(w, r, nil) 109 | if err != nil { 110 | Warnfunc("Cannot upgrade connection %s", err.Error()) 111 | w.Write([]byte(fmt.Sprintf("Cannot upgrade connection %s", err.Error()))) 112 | w.WriteHeader(426) 113 | return 114 | } 115 | 116 | mutex.Lock() 117 | conn := &Conn{ 118 | ID: ConnID(uuid.NewV4().String()), 119 | WSConn: c, 120 | } 121 | if (*wsConnections)[conn.ID] != nil { 122 | (*wsConnections)[conn.ID].WSConn.Close() 123 | (*closedConnectionCallback)((*wsConnections)[conn.ID]) 124 | } 125 | (*wsConnections)[conn.ID] = conn 126 | mutex.Unlock() 127 | 128 | if (*openedConnectionCallback) != nil { 129 | go (*openedConnectionCallback)(conn) 130 | } 131 | 132 | s.ClientsCounter.Add(1) 133 | 134 | defer c.Close() 135 | for { 136 | _, message, err := c.ReadMessage() 137 | if err != nil { 138 | mutex.Lock() 139 | delete(*wsConnections, conn.ID) 140 | mutex.Unlock() 141 | if (*closedConnectionCallback) != nil { 142 | (*closedConnectionCallback)(conn) 143 | } 144 | s.ClientsCounter.Add(-1) 145 | break 146 | } 147 | 148 | if (*onMessageCallback) != nil { 149 | var parsedMessage Message 150 | if e := json.Unmarshal(message, &parsedMessage); err != nil { 151 | Warnfunc("Cannot Unmarshall message", e.Error()) 152 | } 153 | (*onMessageCallback)(conn, &parsedMessage) 154 | } 155 | } 156 | 157 | } 158 | } 159 | 160 | func varsHandler(w http.ResponseWriter, r *http.Request) { 161 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 162 | fmt.Fprintf(w, "{\n") 163 | first := true 164 | expvar.Do(func(kv expvar.KeyValue) { 165 | if !first { 166 | fmt.Fprintf(w, ",\n") 167 | } 168 | first = false 169 | fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) 170 | }) 171 | fmt.Fprintf(w, "\n}\n") 172 | } 173 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | //Stack is a thread-safe "First In First Out" stack 10 | type Stack struct { 11 | top *stackItem 12 | count int 13 | mutex *sync.Mutex 14 | max int 15 | } 16 | 17 | type stackItem struct { 18 | data interface{} 19 | next *stackItem 20 | } 21 | 22 | //NewStack intialize a brand new Stack 23 | func NewStack() *Stack { 24 | s := &Stack{} 25 | s.mutex = &sync.Mutex{} 26 | return s 27 | } 28 | 29 | //Open the connection to the storage driver 30 | func (s *Stack) Open(o *Options) { 31 | if o != nil { 32 | m := o.Storage 33 | i, b := m["capacity"].(int) 34 | if !b { 35 | Logfunc("Error with stack capacity option : %s", i) 36 | return 37 | } 38 | s.max = i 39 | } 40 | } 41 | 42 | // Get peeks at the n-th item in the stack. Unlike other operations, this one costs O(n). 43 | func (s *Stack) Get(index int) (interface{}, error) { 44 | if index < 0 || index >= s.count { 45 | return nil, fmt.Errorf("Requested index %d outside stack, length %d", index, s.count) 46 | } 47 | 48 | s.mutex.Lock() 49 | defer s.mutex.Unlock() 50 | 51 | n := s.top 52 | for i := 1; i < s.count-index; i++ { 53 | n = n.next 54 | } 55 | 56 | return n.data, nil 57 | } 58 | 59 | // Dump prints of the stack. 60 | func (s *Stack) Dump() { 61 | n := s.top 62 | fmt.Print("[ ") 63 | for i := 0; i < s.count; i++ { 64 | fmt.Printf("%+v ", n.data) 65 | n = n.next 66 | } 67 | fmt.Print("]") 68 | } 69 | 70 | //Len returns current length of the stack 71 | func (s *Stack) Len() int { 72 | s.mutex.Lock() 73 | defer s.mutex.Unlock() 74 | return s.count 75 | } 76 | 77 | //Push add an item a the top of the stack 78 | func (s *Stack) Push(item interface{}) { 79 | if s.max > 0 { 80 | f := NewFibonacci() 81 | for s.Len() >= s.max { 82 | Warnfunc("Stack overflow. Waiting...") 83 | f.WaitForIt(time.Second) 84 | } 85 | } 86 | 87 | n := &stackItem{data: item} 88 | 89 | s.mutex.Lock() 90 | defer s.mutex.Unlock() 91 | if s.top == nil { 92 | s.top = n 93 | } else { 94 | n.next = s.top 95 | s.top = n 96 | } 97 | 98 | s.count++ 99 | } 100 | 101 | //Pop returns and removes the botteom of the stack 102 | func (s *Stack) Pop() interface{} { 103 | s.mutex.Lock() 104 | defer s.mutex.Unlock() 105 | 106 | var n *stackItem 107 | if s.top != nil { 108 | n = s.top 109 | s.top = n.next 110 | s.count-- 111 | } 112 | 113 | if n == nil { 114 | return nil 115 | } 116 | 117 | return n.data 118 | 119 | } 120 | 121 | //Peek returns but doesn't remove the top of the stack 122 | func (s *Stack) Peek() interface{} { 123 | s.mutex.Lock() 124 | defer s.mutex.Unlock() 125 | 126 | n := s.top 127 | if n == nil || n.data == nil { 128 | return nil 129 | } 130 | 131 | return n.data 132 | } 133 | -------------------------------------------------------------------------------- /stack_test.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | -------------------------------------------------------------------------------- /topic.go: -------------------------------------------------------------------------------- 1 | package wsqueue 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "sync" 7 | ) 8 | 9 | //Topic implements publish and subscribe semantics. When you publish a message 10 | //it goes to all the subscribers who are interested - so zero to many 11 | //subscribers will receive a copy of the message. Only subscribers who had an 12 | //active subscription at the time the broker receives the message will get a 13 | //copy of the message. 14 | type Topic struct { 15 | Options *Options `json:"options,omitempty"` 16 | Topic string `json:"topic,omitempty"` 17 | OpenedConnectionHandler func(*Conn) `json:"-"` 18 | ClosedConnectionHandler func(*Conn) `json:"-"` 19 | OnMessageHandler func(*Conn, *Message) error `json:"-"` 20 | mutex *sync.RWMutex 21 | wsConnections map[ConnID]*Conn 22 | } 23 | 24 | //CreateTopic create topic 25 | func (s *Server) CreateTopic(topic string) *Topic { 26 | t, _ := s.newTopic(topic) 27 | s.RegisterTopic(t) 28 | return t 29 | } 30 | 31 | func (s *Server) newTopic(topic string) (*Topic, error) { 32 | t := &Topic{ 33 | Topic: topic, 34 | mutex: &sync.RWMutex{}, 35 | wsConnections: make(map[ConnID]*Conn), 36 | } 37 | return t, nil 38 | } 39 | 40 | //RegisterTopic register 41 | func (s *Server) RegisterTopic(t *Topic) { 42 | log.Printf("Register queue %s on route %s", t.Topic, s.RoutePrefix+"/wsqueue/topic/"+t.Topic) 43 | handler := s.createHandler( 44 | t.mutex, 45 | &t.wsConnections, 46 | &t.OpenedConnectionHandler, 47 | &t.ClosedConnectionHandler, 48 | &t.OnMessageHandler, 49 | t.Options, 50 | ) 51 | s.Router.HandleFunc(s.RoutePrefix+"/wsqueue/topic/"+t.Topic, handler) 52 | s.TopicsCounter.Add(1) 53 | 54 | } 55 | 56 | func (t *Topic) publish(m Message) error { 57 | t.mutex.Lock() 58 | b, _ := json.Marshal(m) 59 | for _, conn := range t.wsConnections { 60 | conn.WSConn.WriteMessage(1, b) 61 | } 62 | t.mutex.Unlock() 63 | return nil 64 | } 65 | 66 | //Publish send message to everyone 67 | func (t *Topic) Publish(data interface{}) error { 68 | m, e := newMessage(data) 69 | if e != nil { 70 | return e 71 | } 72 | return t.publish(*m) 73 | } 74 | --------------------------------------------------------------------------------