├── kaca.go ├── LICENSE ├── client.go ├── dispatcher.go ├── README.md └── conn.go /kaca.go: -------------------------------------------------------------------------------- 1 | package kaca 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func ServeWs(addr string, checkOrigin bool) { 9 | go disp.run() 10 | if checkOrigin { 11 | http.HandleFunc("/ws", serveWsCheckOrigin) 12 | } else { 13 | http.HandleFunc("/ws", serveWs) 14 | } 15 | err := http.ListenAndServe(addr, nil) 16 | if err != nil { 17 | log.Fatal("ListenAndServe: ", err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 sk,http://cocosk.com/ ,skkmvp@hotmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package kaca 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "log" 6 | "math/rand" 7 | "net/url" 8 | ) 9 | 10 | type client struct { 11 | id uint64 12 | addr string 13 | path string 14 | conn *websocket.Conn 15 | } 16 | 17 | func NewClient(addr, path string) *client { 18 | u := url.URL{Scheme: "ws", Host: addr, Path: path} 19 | log.Printf("connecting to %s", u.String()) 20 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 21 | if err != nil { 22 | log.Fatal("dial:", err) 23 | } 24 | return &client{ 25 | id: uint64(rand.Int63()), 26 | addr: addr, 27 | path: path, 28 | conn: c, 29 | } 30 | } 31 | 32 | func (c *client) Broadcast(message string) { 33 | err := c.conn.WriteMessage(websocket.TextMessage, []byte(message)) 34 | if err != nil { 35 | log.Println("write:", err) 36 | } 37 | } 38 | 39 | func (c *client) Pub(topic, message string) { 40 | sendMsg := PUB_PREFIX + topic + SPLIT_LINE + message 41 | err := c.conn.WriteMessage(websocket.TextMessage, []byte(sendMsg)) 42 | if err != nil { 43 | log.Println("write:", err) 44 | } 45 | } 46 | 47 | func (c *client) Sub(topic string) { 48 | sendMsg := SUB_PREFIX + topic 49 | err := c.conn.WriteMessage(websocket.TextMessage, []byte(sendMsg)) 50 | if err != nil { 51 | log.Println("write:", err) 52 | } 53 | log.Println("sub topic :" + topic + "success") 54 | } 55 | 56 | func (c *client) ConsumeMessage(f func(m string)) { 57 | go func() { 58 | for { 59 | _, message, err := c.conn.ReadMessage() 60 | if err != nil { 61 | log.Println("read:", err) 62 | break 63 | } 64 | log.Printf("recv: %s", message) 65 | f(string(message)) 66 | } 67 | }() 68 | } 69 | 70 | func (c *client) Shutdown() { 71 | c.conn.Close() 72 | } 73 | -------------------------------------------------------------------------------- /dispatcher.go: -------------------------------------------------------------------------------- 1 | package kaca 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type dispatcher struct { 10 | // Registered connections. 11 | connections map[*connection]bool 12 | broadcast chan []byte 13 | sub chan string 14 | pub chan string 15 | register chan *connection 16 | unregister chan *connection 17 | } 18 | 19 | func NewDispatcher() *dispatcher { 20 | return &dispatcher{ 21 | broadcast: make(chan []byte), 22 | sub: make(chan string), 23 | pub: make(chan string), 24 | register: make(chan *connection), 25 | unregister: make(chan *connection), 26 | connections: make(map[*connection]bool), 27 | } 28 | } 29 | 30 | func (d *dispatcher) run() { 31 | for { 32 | select { 33 | case c := <-d.register: 34 | d.connections[c] = true 35 | case c := <-d.unregister: 36 | if _, ok := d.connections[c]; ok { 37 | delete(d.connections, c) 38 | close(c.send) 39 | } 40 | case m := <-d.broadcast: 41 | for c := range d.connections { 42 | select { 43 | case c.send <- m: 44 | default: 45 | close(c.send) 46 | delete(d.connections, c) 47 | } 48 | } 49 | case m := <-d.sub: 50 | msp := strings.Split(m, SPLIT_LINE) 51 | //subscribe message 52 | log.Println("sub->" + m) 53 | for c := range d.connections { 54 | if msp[0] == strconv.Itoa(int(c.cid)) { 55 | c.topics = append(c.topics, msp[1]) 56 | } 57 | } 58 | 59 | case m := <-d.pub: 60 | //publish message 61 | msp := strings.Split(m, SPLIT_LINE) 62 | log.Println("pub->" + m) 63 | for c := range d.connections { 64 | for _, t := range c.topics { 65 | if t == msp[0] { 66 | select { 67 | case c.send <- []byte(msp[1]): 68 | default: 69 | close(c.send) 70 | delete(d.connections, c) 71 | } 72 | break 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # about kaca 2 | a pub/sub messaging system based on websocket 3 | 4 | ## Getting started 5 | ```bash 6 | go get github.com/scottkiss/kaca 7 | ``` 8 | 9 | ## server 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "github.com/scottkiss/kaca" 16 | ) 17 | 18 | func main() { 19 | //use true to set check origin 20 | kaca.ServeWs(":8080",true) 21 | } 22 | ``` 23 | 24 | ## pub/sub client 25 | 26 | ```go 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | "github.com/scottkiss/kaca" 32 | "time" 33 | ) 34 | 35 | func main() { 36 | producer := kaca.NewClient(":8080", "ws") 37 | consumer := kaca.NewClient(":8080", "ws") 38 | consumer.Sub("say") 39 | consumer.Sub("you") 40 | consumer.ConsumeMessage(func(message string) { 41 | fmt.Println("consume =>" + message) 42 | }) 43 | time.Sleep(time.Second * time.Duration(2)) 44 | producer.Pub("you", "world") 45 | producer.Pub("say", "hello") 46 | time.Sleep(time.Second * time.Duration(2)) 47 | } 48 | 49 | ``` 50 | 51 | ## broadcast client 52 | ```go 53 | 54 | package main 55 | 56 | import ( 57 | "fmt" 58 | "github.com/scottkiss/kaca" 59 | "time" 60 | ) 61 | 62 | func main() { 63 | producer := kaca.NewClient(":8080", "ws") 64 | consumer := kaca.NewClient(":8080", "ws") 65 | c2 := kaca.NewClient(":8080", "ws") 66 | c2.ConsumeMessage(func(message string) { 67 | fmt.Println("c2 consume =>" + message) 68 | }) 69 | consumer.Sub("say") 70 | consumer.Sub("you") 71 | consumer.ConsumeMessage(func(message string) { 72 | fmt.Println("consume =>" + message) 73 | }) 74 | time.Sleep(time.Second * time.Duration(2)) 75 | producer.Broadcast("broadcast...") 76 | time.Sleep(time.Second * time.Duration(2)) 77 | } 78 | 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package kaca 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "log" 6 | "math/rand" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var upgrader = websocket.Upgrader{ 14 | ReadBufferSize: 1024, 15 | WriteBufferSize: 1024, 16 | } 17 | 18 | const ( 19 | // Time allowed to write a message to the peer. 20 | writeWait = 10 * time.Second 21 | 22 | // Time allowed to read the next pong message from the peer. 23 | pongWait = 60 * time.Second 24 | 25 | // Send pings to peer with this period. Must be less than pongWait. 26 | pingPeriod = (pongWait * 9) / 10 27 | 28 | // Maximum message size allowed from peer. 29 | maxMessageSize = 512 30 | SUB_PREFIX = "__sub:" 31 | PUB_PREFIX = "__pub:" 32 | maxTopics = 100 33 | SPLIT_LINE = "_:_" 34 | ) 35 | 36 | var disp = NewDispatcher() 37 | 38 | type connection struct { 39 | // websocket connection. 40 | ws *websocket.Conn 41 | send chan []byte 42 | topics []string 43 | cid uint64 44 | } 45 | 46 | func (c *connection) deliver() { 47 | ticker := time.NewTicker(pingPeriod) 48 | defer func() { 49 | ticker.Stop() 50 | c.ws.Close() 51 | }() 52 | for { 53 | select { 54 | case message, ok := <-c.send: 55 | if !ok { 56 | c.sendMsg(websocket.CloseMessage, []byte{}) 57 | return 58 | } 59 | if err := c.sendMsg(websocket.TextMessage, message); err != nil { 60 | return 61 | } 62 | case <-ticker.C: 63 | if err := c.sendMsg(websocket.PingMessage, []byte{}); err != nil { 64 | return 65 | } 66 | } 67 | } 68 | } 69 | 70 | func (c *connection) dispatch() { 71 | defer func() { 72 | disp.unregister <- c 73 | c.ws.Close() 74 | }() 75 | c.ws.SetReadDeadline(time.Now().Add(pongWait)) 76 | c.ws.SetReadLimit(maxMessageSize) 77 | c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 78 | for { 79 | _, message, err := c.ws.ReadMessage() 80 | if err != nil { 81 | break 82 | } 83 | msg := string(message) 84 | if strings.Contains(msg, SUB_PREFIX) { 85 | topic := strings.Split(msg, SUB_PREFIX)[1] 86 | disp.sub <- strconv.Itoa(int(c.cid)) + SPLIT_LINE + topic 87 | } else if strings.Contains(msg, PUB_PREFIX) { 88 | topic_msg := strings.Split(msg, PUB_PREFIX)[1] 89 | disp.pub <- topic_msg 90 | } else { 91 | disp.broadcast <- message 92 | } 93 | } 94 | } 95 | 96 | func (c *connection) sendMsg(mt int, payload []byte) error { 97 | c.ws.SetWriteDeadline(time.Now().Add(writeWait)) 98 | return c.ws.WriteMessage(mt, payload) 99 | } 100 | 101 | func serveWs(w http.ResponseWriter, r *http.Request) { 102 | if r.Method != "GET" { 103 | http.Error(w, "Method not allowed", 405) 104 | return 105 | } 106 | 107 | upgrader.CheckOrigin = func(r *http.Request) bool { 108 | return true 109 | } 110 | ws, err := upgrader.Upgrade(w, r, nil) 111 | if err != nil { 112 | log.Println(err) 113 | return 114 | } 115 | c := &connection{cid: uint64(rand.Int63()), send: make(chan []byte, 256), ws: ws, topics: make([]string, maxTopics)} 116 | disp.register <- c 117 | go c.dispatch() 118 | c.deliver() 119 | } 120 | 121 | func serveWsCheckOrigin(w http.ResponseWriter, r *http.Request) { 122 | if r.Method != "GET" { 123 | http.Error(w, "Method not allowed", 405) 124 | return 125 | } 126 | ws, err := upgrader.Upgrade(w, r, nil) 127 | if err != nil { 128 | log.Println(err) 129 | return 130 | } 131 | c := &connection{cid: uint64(rand.Int63()), send: make(chan []byte, 256), ws: ws, topics: make([]string, maxTopics)} 132 | disp.register <- c 133 | go c.dispatch() 134 | c.deliver() 135 | } 136 | --------------------------------------------------------------------------------