├── .gitignore ├── examples ├── publisher.go └── subscriber.go ├── LICENSE ├── client └── client.go ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | mqueue 2 | -------------------------------------------------------------------------------- /examples/publisher.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ridwanmsharif/mqueue/client" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | err := client.Publish("topic_of_your_choice", []byte("ridwan")) 10 | if err != nil { 11 | log.Println(err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/subscriber.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ridwanmsharif/mqueue/client" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | ch, err := client.Subscribe("topic_of_your_choice") 10 | if err != nil { 11 | log.Println(err) 12 | log.Println("wtf is happening") 13 | return 14 | } 15 | 16 | for e := range ch { 17 | log.Println(string(e)) 18 | } 19 | 20 | log.Println("Channel closed") 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ridwan Sharif 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 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/gorilla/websocket" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | var ( 12 | mqueueServer = "127.0.0.1:8081" 13 | ) 14 | 15 | func Publish(topic string, payload []byte) error { 16 | rsp, err := http.Post(fmt.Sprintf("http://%s/pub?topic=%s", 17 | mqueueServer, topic), "application/json", 18 | bytes.NewBuffer(payload)) 19 | 20 | if err != nil { 21 | return err 22 | } 23 | rsp.Body.Close() 24 | if rsp.StatusCode != 200 { 25 | return fmt.Errorf("Non 200 response %d", rsp.StatusCode) 26 | } 27 | return nil 28 | } 29 | 30 | func Subscribe(topic string) (<-chan []byte, error) { 31 | conn, _, err := websocket.DefaultDialer.Dial( 32 | fmt.Sprintf("ws://%s/sub?topic=%s", mqueueServer, topic), make(http.Header)) 33 | 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | ch := make(chan []byte, 100) 39 | 40 | go func() { 41 | for { 42 | t, p, err := conn.ReadMessage() 43 | if err != nil { 44 | log.Println("Could not read message, closing channel", err) 45 | conn.Close() 46 | return 47 | } 48 | switch t { 49 | case websocket.CloseMessage: 50 | log.Println("Close message, clossing channel") 51 | conn.Close() 52 | close(ch) 53 | return 54 | default: 55 | ch <- p 56 | } 57 | } 58 | }() 59 | return ch, nil 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mqueue (Go) 2 | 3 | In-memory message broker in Go over an HTTP API using a concurrent, thread-safe publisher/subscriber architecture with multiple topics. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | $ go get github.com/ridwanmsharif/mqueue 9 | $ cd $GOPATH/src/github.com/ridwanmsharif/mqueue/ 10 | $ go build -o mqueue 11 | $ ./mqueue 12 | ``` 13 | Listening on `:8081`, ready for client/terminal use. 14 | 15 | ## Examples/Usage 16 | 17 | **Subscriber (client library)** 18 | ```go 19 | 20 | ch, err := client.Subscribe("topic_of_your_choice") 21 | if err != nil { 22 | log.Println(err) 23 | return 24 | } 25 | 26 | for e := range ch { 27 | log.Println(string(e)) 28 | } 29 | 30 | log.Println("Channel closed") 31 | 32 | ``` 33 | 34 | **Publisher (client library)** 35 | ```go 36 | 37 | err := client.Publish("topic_of_your_choice", []byte("Arbitrary Message")) 38 | if err != nil { 39 | log.Println(err) 40 | } 41 | 42 | ``` 43 | 44 | **Subscriber (command line)** 45 | ```sh 46 | curl -k -i -N -H "Connection: Upgrade" \ 47 | -H "Upgrade: websocket" \ 48 | -H "Host: localhost:8081" \ 49 | -H "Origin:http://localhost:8081" \ 50 | -H "Sec-Websocket-Version: 13" \ 51 | -H "Sec-Websocket-Key: MQ" \ 52 | "https://localhost:8081/sub?topic= 53 | topic_of_your_choice" 54 | ``` 55 | 56 | **Publisher (command line)** 57 | ```sh 58 | curl -d "Any message you wish to post""http://localhost:8081/pub?topic=topic_of_your_choice" 59 | ``` 60 | 61 | ## Disclaimer 62 | 63 | Purely experimental project. Designed for learning purposes not production use. 64 | This weas my first project in Go, the implementation here is largely based on [asim/mq](https://www.github.com/asim/mq), I'm tracking his incremental progress and repeating the same 65 | 66 | ## Contributing 67 | 68 | Bug reports and pull requests are welcome on GitHub at [@ridwanmsharif](https://www.github.com/ridwanmsharif) 69 | 70 | ## Author 71 | 72 | Ridwan M. Sharif:[ E-mail](mailto:ridwanmsharif@hotmail.com), [@ridwanmsharif](https://www.github.com/ridwanmsharif) 73 | 74 | ## License 75 | 76 | The command line utility is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT) 77 | 78 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "sync" 9 | ) 10 | 11 | type mqueue struct { 12 | topics map[string][]chan []byte 13 | mtx sync.RWMutex 14 | } 15 | 16 | var ( 17 | defaultMqueue = &mqueue{ 18 | topics: make(map[string][]chan []byte), 19 | } 20 | ) 21 | 22 | // Method to add subscriber to specefied topic in mqueue 23 | func (m *mqueue) sub(topic string) (<-chan []byte, error) { 24 | channel := make(chan []byte, 100) 25 | m.mtx.Lock() 26 | m.topics[topic] = append(m.topics[topic], channel) 27 | m.mtx.Unlock() 28 | return channel, nil 29 | } 30 | 31 | // Method to remove subscriber from specefied topic in mqueue 32 | func (m *mqueue) unsub(topic string, sub <-chan []byte) error { 33 | m.mtx.RLock() 34 | subscribers, ok := m.topics[topic] 35 | m.mtx.RUnlock() 36 | 37 | if !ok { 38 | return nil 39 | } 40 | var subs []chan []byte 41 | for _, subscriber := range subscribers { 42 | if subscriber != sub { 43 | subs = append(subs, subscriber) 44 | } 45 | continue 46 | } 47 | 48 | m.mtx.Lock() 49 | m.topics[topic] = subs 50 | m.mtx.Unlock() 51 | 52 | return nil 53 | } 54 | 55 | // Method to publish/push payload to every subscriber 56 | func (m *mqueue) pub(topic string, payload []byte) error { 57 | m.mtx.RLock() 58 | subscribers, ok := m.topics[topic] 59 | m.mtx.RUnlock() 60 | 61 | if !ok { 62 | return nil 63 | } 64 | 65 | go func() { 66 | for _, subscriber := range subscribers { 67 | select { 68 | case subscriber <- payload: 69 | default: 70 | } 71 | } 72 | }() 73 | return nil 74 | } 75 | 76 | // Subscribe to a specefic topic in mqueue 77 | func sub(w http.ResponseWriter, r *http.Request) { 78 | connection, err := websocket.Upgrade(w, r, w.Header(), 79 | 1024, 1024) 80 | if err != nil { 81 | log.Println("Websocket connection failed:", err) 82 | http.Error(w, "Could not open websocket connection", 83 | http.StatusBadRequest) 84 | return 85 | } 86 | 87 | topic := r.URL.Query().Get("topic") 88 | channel, err := defaultMqueue.sub(topic) 89 | if err != nil { 90 | log.Println("Could not retrieve %s.", topic) 91 | http.Error(w, "Could not retrieve events", 92 | http.StatusInternalServerError) 93 | return 94 | } 95 | defer defaultMqueue.unsub(topic, channel) 96 | 97 | for { 98 | select { 99 | case e := <-channel: 100 | err = connection.WriteMessage(websocket.BinaryMessage, e) 101 | if err != nil { 102 | log.Printf("Error sending event: %v", 103 | err.Error()) 104 | return 105 | } 106 | } 107 | } 108 | } 109 | 110 | // Publishes and prints to console of every subscriber 111 | func pub(w http.ResponseWriter, r *http.Request) { 112 | topic := r.URL.Query().Get("topic") 113 | b, err := ioutil.ReadAll(r.Body) 114 | if err != nil { 115 | http.Error(w, "Pub Error", http.StatusInternalServerError) 116 | return 117 | } 118 | r.Body.Close() 119 | 120 | err = defaultMqueue.pub(topic, b) 121 | if err != nil { 122 | http.Error(w, "Pub Error", http.StatusInternalServerError) 123 | return 124 | } 125 | } 126 | 127 | // Entry Point 128 | func main() { 129 | http.HandleFunc("/pub", pub) 130 | http.HandleFunc("/sub", sub) 131 | log.Println("Mqueue listening on :8081") 132 | http.ListenAndServe(":8081", nil) 133 | } 134 | --------------------------------------------------------------------------------