├── .env ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── script └── bootstrap.sh ├── src ├── chat │ ├── chat.go │ ├── main.go │ ├── names.go │ └── types.go ├── gopool │ └── pool.go └── proxy │ └── proxy.go └── web ├── css └── main.css ├── index.html └── js ├── main.js └── ws.js /.env: -------------------------------------------------------------------------------- 1 | CHATPORT=3333 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/src/golang.org/x/sys"] 2 | path = vendor/src/golang.org/x/sys 3 | url = https://go.googlesource.com/sys 4 | [submodule "vendor/src/github.com/gobwas/pool"] 5 | path = vendor/src/github.com/gobwas/pool 6 | url = https://github.com/gobwas/pool.git 7 | [submodule "vendor/src/github.com/gobwas/ws"] 8 | path = vendor/src/github.com/gobwas/ws 9 | url = https://github.com/gobwas/ws.git 10 | [submodule "vendor/src/github.com/gobwas/httphead"] 11 | path = vendor/src/github.com/gobwas/httphead 12 | url = https://github.com/gobwas/httphead.git 13 | [submodule "vendor/src/github.com/mailru/easygo"] 14 | path = vendor/src/github.com/mailru/easygo 15 | url = https://github.com/mailru/easygo.git 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2019 Sergey Kamardin 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_PATH=$(PWD):$(PWD)/vendor:$(GOPATH) 2 | 3 | .PHONY: vendor 4 | 5 | all: vendor chat proxy 6 | 7 | chat: 8 | GOPATH="$(GO_PATH)" go build -o ./bin/chat ./src/chat 9 | 10 | proxy: 11 | GOPATH="$(GO_PATH)" go build -o ./bin/proxy ./src/proxy 12 | 13 | vendor: 14 | git submodule init; \ 15 | git submodule update; \ 16 | 17 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: script/bootstrap.sh 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [ws](https://github.com/gobwas/ws) examples 2 | 3 | [![website][website-image]][website-url] 4 | 5 | > Example applications written in Go with `github.com/gobwas/ws` inside. 6 | 7 | # Applications 8 | 9 | - [x] [Chat](https://github.com/gobwas/ws-examples/tree/master/src/chat) 10 | - [ ] Chat CLI 11 | - [ ] Twitter hashtag watcher 12 | 13 | # Notes 14 | 15 | ## Commands 16 | 17 | Currently these commands are developed: 18 | - `bin/chat` the chat application, which is listening raw tcp socket and 19 | handles [jsonrpc]-like messages. 20 | - `bin/proxy` proxy that used for two purposes. First of all, to serve static 21 | files for chat ui. Second and technical one is to proxy `/ws` requests to 22 | running chat app. This is done only for running on heroku, where only one port 23 | is able to be exported. 24 | 25 | ## Building 26 | 27 | All commands can be built by `make *` or by just `make`. 28 | 29 | The directory structure is convinient for [gb](https://getgb.io/docs/usage/) 30 | vendoring tool. But instead of using `gb` git submodules are used to vendor 31 | dependencies. Thus, `make vendor` will update existing submodules. 32 | 33 | > Also, `gb` directory structure is here to signal the heroku buildpack to use 34 | > appropriate build logic. 35 | 36 | Chat application deployed [here][website-url]. 37 | 38 | [website-image]: https://img.shields.io/website-up-down-green-red/http/vast-beyond-95791.herokuapp.com.svg?label=running-example 39 | [website-url]: https://vast-beyond-95791.herokuapp.com/#!/chat 40 | 41 | -------------------------------------------------------------------------------- /script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | chat_port=3333 4 | 5 | ./bin/chat -listen=":${chat_port}" & 6 | chat_pid=$! 7 | for t in $(yes "1" | head -n 5); do 8 | sock=$(ls -la /proc/${chat_pid}/fd/ | fgrep 'socket' | awk '{print $4}') 9 | if [ ! -z "$sock" ]; then 10 | break 11 | fi 12 | sleep $t 13 | done 14 | 15 | ./bin/proxy -listen=":${PORT}" -chat_addr=":${chat_port}" 16 | -------------------------------------------------------------------------------- /src/chat/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "gopool" 7 | "io" 8 | "math/rand" 9 | "net" 10 | "sort" 11 | "strconv" 12 | "sync" 13 | "time" 14 | 15 | "github.com/gobwas/ws" 16 | "github.com/gobwas/ws/wsutil" 17 | ) 18 | 19 | // User represents user connection. 20 | // It contains logic of receiving and sending messages. 21 | // That is, there are no active reader or writer. Some other layer of the 22 | // application should call Receive() to read user's incoming message. 23 | type User struct { 24 | io sync.Mutex 25 | conn io.ReadWriteCloser 26 | 27 | id uint 28 | name string 29 | chat *Chat 30 | } 31 | 32 | // Receive reads next message from user's underlying connection. 33 | // It blocks until full message received. 34 | func (u *User) Receive() error { 35 | req, err := u.readRequest() 36 | if err != nil { 37 | u.conn.Close() 38 | return err 39 | } 40 | if req == nil { 41 | // Handled some control message. 42 | return nil 43 | } 44 | switch req.Method { 45 | case "rename": 46 | name, ok := req.Params["name"].(string) 47 | if !ok { 48 | return u.writeErrorTo(req, Object{ 49 | "error": "bad params", 50 | }) 51 | } 52 | prev, ok := u.chat.Rename(u, name) 53 | if !ok { 54 | return u.writeErrorTo(req, Object{ 55 | "error": "already exists", 56 | }) 57 | } 58 | u.chat.Broadcast("rename", Object{ 59 | "prev": prev, 60 | "name": name, 61 | "time": timestamp(), 62 | }) 63 | return u.writeResultTo(req, nil) 64 | case "publish": 65 | req.Params["author"] = u.name 66 | req.Params["time"] = timestamp() 67 | u.chat.Broadcast("publish", req.Params) 68 | default: 69 | return u.writeErrorTo(req, Object{ 70 | "error": "not implemented", 71 | }) 72 | } 73 | return nil 74 | } 75 | 76 | // readRequests reads json-rpc request from connection. 77 | // It takes io mutex. 78 | func (u *User) readRequest() (*Request, error) { 79 | u.io.Lock() 80 | defer u.io.Unlock() 81 | 82 | h, r, err := wsutil.NextReader(u.conn, ws.StateServerSide) 83 | if err != nil { 84 | return nil, err 85 | } 86 | if h.OpCode.IsControl() { 87 | return nil, wsutil.ControlFrameHandler(u.conn, ws.StateServerSide)(h, r) 88 | } 89 | 90 | req := &Request{} 91 | decoder := json.NewDecoder(r) 92 | if err := decoder.Decode(req); err != nil { 93 | return nil, err 94 | } 95 | 96 | return req, nil 97 | } 98 | 99 | func (u *User) writeErrorTo(req *Request, err Object) error { 100 | return u.write(Error{ 101 | ID: req.ID, 102 | Error: err, 103 | }) 104 | } 105 | 106 | func (u *User) writeResultTo(req *Request, result Object) error { 107 | return u.write(Response{ 108 | ID: req.ID, 109 | Result: result, 110 | }) 111 | } 112 | 113 | func (u *User) writeNotice(method string, params Object) error { 114 | return u.write(Request{ 115 | Method: method, 116 | Params: params, 117 | }) 118 | } 119 | 120 | func (u *User) write(x interface{}) error { 121 | w := wsutil.NewWriter(u.conn, ws.StateServerSide, ws.OpText) 122 | encoder := json.NewEncoder(w) 123 | 124 | u.io.Lock() 125 | defer u.io.Unlock() 126 | 127 | if err := encoder.Encode(x); err != nil { 128 | return err 129 | } 130 | 131 | return w.Flush() 132 | } 133 | 134 | func (u *User) writeRaw(p []byte) error { 135 | u.io.Lock() 136 | defer u.io.Unlock() 137 | 138 | _, err := u.conn.Write(p) 139 | 140 | return err 141 | } 142 | 143 | // Chat contains logic of user interaction. 144 | type Chat struct { 145 | mu sync.RWMutex 146 | seq uint 147 | us []*User 148 | ns map[string]*User 149 | 150 | pool *gopool.Pool 151 | out chan []byte 152 | } 153 | 154 | func NewChat(pool *gopool.Pool) *Chat { 155 | chat := &Chat{ 156 | pool: pool, 157 | ns: make(map[string]*User), 158 | out: make(chan []byte, 1), 159 | } 160 | 161 | go chat.writer() 162 | 163 | return chat 164 | } 165 | 166 | // Register registers new connection as a User. 167 | func (c *Chat) Register(conn net.Conn) *User { 168 | user := &User{ 169 | chat: c, 170 | conn: conn, 171 | } 172 | 173 | c.mu.Lock() 174 | { 175 | user.id = c.seq 176 | user.name = c.randName() 177 | 178 | c.us = append(c.us, user) 179 | c.ns[user.name] = user 180 | 181 | c.seq++ 182 | } 183 | c.mu.Unlock() 184 | 185 | user.writeNotice("hello", Object{ 186 | "name": user.name, 187 | }) 188 | c.Broadcast("greet", Object{ 189 | "name": user.name, 190 | "time": timestamp(), 191 | }) 192 | 193 | return user 194 | } 195 | 196 | // Remove removes user from chat. 197 | func (c *Chat) Remove(user *User) { 198 | c.mu.Lock() 199 | removed := c.remove(user) 200 | c.mu.Unlock() 201 | 202 | if !removed { 203 | return 204 | } 205 | 206 | c.Broadcast("goodbye", Object{ 207 | "name": user.name, 208 | "time": timestamp(), 209 | }) 210 | } 211 | 212 | // Rename renames user. 213 | func (c *Chat) Rename(user *User, name string) (prev string, ok bool) { 214 | c.mu.Lock() 215 | { 216 | if _, has := c.ns[name]; !has { 217 | ok = true 218 | prev, user.name = user.name, name 219 | delete(c.ns, prev) 220 | c.ns[name] = user 221 | } 222 | } 223 | c.mu.Unlock() 224 | 225 | return prev, ok 226 | } 227 | 228 | // Broadcast sends message to all alive users. 229 | func (c *Chat) Broadcast(method string, params Object) error { 230 | var buf bytes.Buffer 231 | 232 | w := wsutil.NewWriter(&buf, ws.StateServerSide, ws.OpText) 233 | encoder := json.NewEncoder(w) 234 | 235 | r := Request{Method: method, Params: params} 236 | if err := encoder.Encode(r); err != nil { 237 | return err 238 | } 239 | if err := w.Flush(); err != nil { 240 | return err 241 | } 242 | 243 | c.out <- buf.Bytes() 244 | 245 | return nil 246 | } 247 | 248 | // writer writes broadcast messages from chat.out channel. 249 | func (c *Chat) writer() { 250 | for bts := range c.out { 251 | c.mu.RLock() 252 | us := c.us 253 | c.mu.RUnlock() 254 | 255 | for _, u := range us { 256 | u := u // For closure. 257 | c.pool.Schedule(func() { 258 | u.writeRaw(bts) 259 | }) 260 | } 261 | } 262 | } 263 | 264 | // mutex must be held. 265 | func (c *Chat) remove(user *User) bool { 266 | if _, has := c.ns[user.name]; !has { 267 | return false 268 | } 269 | 270 | delete(c.ns, user.name) 271 | 272 | i := sort.Search(len(c.us), func(i int) bool { 273 | return c.us[i].id >= user.id 274 | }) 275 | if i >= len(c.us) { 276 | panic("chat: inconsistent state") 277 | } 278 | 279 | without := make([]*User, len(c.us)-1) 280 | copy(without[:i], c.us[:i]) 281 | copy(without[i:], c.us[i+1:]) 282 | c.us = without 283 | 284 | return true 285 | } 286 | 287 | func (c *Chat) randName() string { 288 | var suffix string 289 | for { 290 | name := animals[rand.Intn(len(animals))] + suffix 291 | if _, has := c.ns[name]; !has { 292 | return name 293 | } 294 | suffix += strconv.Itoa(rand.Intn(10)) 295 | } 296 | return "" 297 | } 298 | 299 | func timestamp() int64 { 300 | return time.Now().UnixNano() / int64(time.Millisecond) 301 | } 302 | -------------------------------------------------------------------------------- /src/chat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "gopool" 6 | "log" 7 | "net" 8 | "time" 9 | 10 | "github.com/gobwas/ws" 11 | "github.com/mailru/easygo/netpoll" 12 | 13 | "net/http" 14 | _ "net/http/pprof" 15 | ) 16 | 17 | var ( 18 | addr = flag.String("listen", ":3333", "address to bind to") 19 | debug = flag.String("pprof", "", "address for pprof http") 20 | workers = flag.Int("workers", 128, "max workers count") 21 | queue = flag.Int("queue", 1, "workers task queue size") 22 | ioTimeout = flag.Duration("io_timeout", time.Millisecond*100, "i/o operations timeout") 23 | ) 24 | 25 | func main() { 26 | flag.Parse() 27 | 28 | if x := *debug; x != "" { 29 | log.Printf("starting pprof server on %s", x) 30 | go func() { 31 | log.Printf("pprof server error: %v", http.ListenAndServe(x, nil)) 32 | }() 33 | } 34 | 35 | // Initialize netpoll instance. We will use it to be noticed about incoming 36 | // events from listener of user connections. 37 | poller, err := netpoll.New(nil) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | var ( 43 | // Make pool of X size, Y sized work queue and one pre-spawned 44 | // goroutine. 45 | pool = gopool.NewPool(*workers, *queue, 1) 46 | chat = NewChat(pool) 47 | exit = make(chan struct{}) 48 | ) 49 | // handle is a new incoming connection handler. 50 | // It upgrades TCP connection to WebSocket, registers netpoll listener on 51 | // it and stores it as a chat user in Chat instance. 52 | // 53 | // We will call it below within accept() loop. 54 | handle := func(conn net.Conn) { 55 | // NOTE: we wrap conn here to show that ws could work with any kind of 56 | // io.ReadWriter. 57 | safeConn := deadliner{conn, *ioTimeout} 58 | 59 | // Zero-copy upgrade to WebSocket connection. 60 | hs, err := ws.Upgrade(safeConn) 61 | if err != nil { 62 | log.Printf("%s: upgrade error: %v", nameConn(conn), err) 63 | conn.Close() 64 | return 65 | } 66 | 67 | log.Printf("%s: established websocket connection: %+v", nameConn(conn), hs) 68 | 69 | // Register incoming user in chat. 70 | user := chat.Register(safeConn) 71 | 72 | // Create netpoll event descriptor for conn. 73 | // We want to handle only read events of it. 74 | desc := netpoll.Must(netpoll.HandleRead(conn)) 75 | 76 | // Subscribe to events about conn. 77 | poller.Start(desc, func(ev netpoll.Event) { 78 | if ev&(netpoll.EventReadHup|netpoll.EventHup) != 0 { 79 | // When ReadHup or Hup received, this mean that client has 80 | // closed at least write end of the connection or connections 81 | // itself. So we want to stop receive events about such conn 82 | // and remove it from the chat registry. 83 | poller.Stop(desc) 84 | chat.Remove(user) 85 | return 86 | } 87 | // Here we can read some new message from connection. 88 | // We can not read it right here in callback, because then we will 89 | // block the poller's inner loop. 90 | // We do not want to spawn a new goroutine to read single message. 91 | // But we want to reuse previously spawned goroutine. 92 | pool.Schedule(func() { 93 | if err := user.Receive(); err != nil { 94 | // When receive failed, we can only disconnect broken 95 | // connection and stop to receive events about it. 96 | poller.Stop(desc) 97 | chat.Remove(user) 98 | } 99 | }) 100 | }) 101 | } 102 | 103 | // Create incoming connections listener. 104 | ln, err := net.Listen("tcp", *addr) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | 109 | log.Printf("websocket is listening on %s", ln.Addr().String()) 110 | 111 | // Create netpoll descriptor for the listener. 112 | // We use OneShot here to manually resume events stream when we want to. 113 | acceptDesc := netpoll.Must(netpoll.HandleListener( 114 | ln, netpoll.EventRead|netpoll.EventOneShot, 115 | )) 116 | 117 | // accept is a channel to signal about next incoming connection Accept() 118 | // results. 119 | accept := make(chan error, 1) 120 | 121 | // Subscribe to events about listener. 122 | poller.Start(acceptDesc, func(e netpoll.Event) { 123 | // We do not want to accept incoming connection when goroutine pool is 124 | // busy. So if there are no free goroutines during 1ms we want to 125 | // cooldown the server and do not receive connection for some short 126 | // time. 127 | err := pool.ScheduleTimeout(time.Millisecond, func() { 128 | conn, err := ln.Accept() 129 | if err != nil { 130 | accept <- err 131 | return 132 | } 133 | 134 | accept <- nil 135 | handle(conn) 136 | }) 137 | if err == nil { 138 | err = <-accept 139 | } 140 | if err != nil { 141 | if err != gopool.ErrScheduleTimeout { 142 | goto cooldown 143 | } 144 | if ne, ok := err.(net.Error); ok && ne.Temporary() { 145 | goto cooldown 146 | } 147 | 148 | log.Fatalf("accept error: %v", err) 149 | 150 | cooldown: 151 | delay := 5 * time.Millisecond 152 | log.Printf("accept error: %v; retrying in %s", err, delay) 153 | time.Sleep(delay) 154 | } 155 | 156 | poller.Resume(acceptDesc) 157 | }) 158 | 159 | <-exit 160 | } 161 | 162 | func nameConn(conn net.Conn) string { 163 | return conn.LocalAddr().String() + " > " + conn.RemoteAddr().String() 164 | } 165 | 166 | // deadliner is a wrapper around net.Conn that sets read/write deadlines before 167 | // every Read() or Write() call. 168 | type deadliner struct { 169 | net.Conn 170 | t time.Duration 171 | } 172 | 173 | func (d deadliner) Write(p []byte) (int, error) { 174 | if err := d.Conn.SetWriteDeadline(time.Now().Add(d.t)); err != nil { 175 | return 0, err 176 | } 177 | return d.Conn.Write(p) 178 | } 179 | 180 | func (d deadliner) Read(p []byte) (int, error) { 181 | if err := d.Conn.SetReadDeadline(time.Now().Add(d.t)); err != nil { 182 | return 0, err 183 | } 184 | return d.Conn.Read(p) 185 | } 186 | -------------------------------------------------------------------------------- /src/chat/names.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var animals = [...]string{ 4 | "aardvark", 5 | "albatross", 6 | "alligator", 7 | "alpaca", 8 | "ant", 9 | "anteater", 10 | "antelope", 11 | "ape", 12 | "armadillo", 13 | "baboon", 14 | "badger", 15 | "barracuda", 16 | "bat", 17 | "bear", 18 | "beaver", 19 | "bee", 20 | "bird", 21 | "aves", 22 | "bison", 23 | "boar", 24 | "buffalo", 25 | "camel", 26 | "caribou", 27 | "cassowary", 28 | "cat", 29 | "caterpillar", 30 | "cattle", 31 | "chamois", 32 | "cheetah", 33 | "chicken", 34 | "chimpanzee", 35 | "chinchilla", 36 | "chough", 37 | "coati", 38 | "cobra", 39 | "cockroach", 40 | "cod", 41 | "cormorant", 42 | "coyote", 43 | "crab", 44 | "crane", 45 | "crocodile", 46 | "crow", 47 | "curlew", 48 | "deer", 49 | "dinosaur", 50 | "dog", 51 | "dogfish", 52 | "dolphin", 53 | "donkey", 54 | "dotterel", 55 | "dove", 56 | "dragonfly", 57 | "duck", 58 | "dugong", 59 | "dunlin", 60 | "eagle", 61 | "echidna", 62 | "eel", 63 | "eland", 64 | "elephant", 65 | "elephant seal", 66 | "elk", 67 | "emu", 68 | "falcon", 69 | "ferret", 70 | "finch", 71 | "fish", 72 | "flamingo", 73 | "fly", 74 | "fox", 75 | "frog", 76 | "gaur", 77 | "gazelle", 78 | "gerbil", 79 | "giant panda", 80 | "giraffe", 81 | "gnat", 82 | "gnu", 83 | "goat", 84 | "goldfinch", 85 | "goosander", 86 | "goose", 87 | "gorilla", 88 | "goshawk", 89 | "grasshopper", 90 | "grouse", 91 | "guanaco", 92 | "guinea fowl", 93 | "guinea pig", 94 | "gull", 95 | "hamster", 96 | "hare", 97 | "hawk", 98 | "hedgehog", 99 | "heron", 100 | "herring", 101 | "hippo", 102 | "hornet", 103 | "horse", 104 | "hummingbird", 105 | "hyena", 106 | "ibex", 107 | "ibis", 108 | "jackal", 109 | "jaguar", 110 | "jay", 111 | "jellyfish", 112 | "kangaroo", 113 | "kinkajou", 114 | "koala", 115 | "komodo dragon", 116 | "kouprey", 117 | "kudu", 118 | "lapwing", 119 | "lark", 120 | "lemur", 121 | "leopard", 122 | "lion", 123 | "llama", 124 | "lobster", 125 | "locust", 126 | "loris", 127 | "louse", 128 | "lyrebird", 129 | "magpie", 130 | "mallard", 131 | "mammoth", 132 | "manatee", 133 | "mandrill", 134 | "mink", 135 | "mole", 136 | "mongoose", 137 | "monkey", 138 | "moose", 139 | "mouse", 140 | "mosquito", 141 | "narwhal", 142 | "newt", 143 | "nightingale", 144 | "octopus", 145 | "okapi", 146 | "opossum", 147 | "ostrich", 148 | "otter", 149 | "owl", 150 | "oyster", 151 | "panther", 152 | "parrot", 153 | "panda", 154 | "partridge", 155 | "peafowl", 156 | "pelican", 157 | "penguin", 158 | "pheasant", 159 | "pig", 160 | "pigeon", 161 | "polar bear", 162 | "pony", 163 | "porcupine", 164 | "porpoise", 165 | "prairie dog", 166 | "quail", 167 | "quelea", 168 | "quetzal", 169 | "rabbit", 170 | "raccoon", 171 | "ram", 172 | "rat", 173 | "raven", 174 | "red deer", 175 | "red panda", 176 | "reindeer", 177 | "rhinoceros", 178 | "rook", 179 | "salamander", 180 | "salmon", 181 | "sand dollar", 182 | "sandpiper", 183 | "sardine", 184 | "sea lion", 185 | "sea urchin", 186 | "seahorse", 187 | "seal", 188 | "shark", 189 | "sheep", 190 | "shrew", 191 | "skunk", 192 | "sloth", 193 | "snail", 194 | "snake", 195 | "spider", 196 | "squirrel", 197 | "starling", 198 | "stegosaurus", 199 | "swan", 200 | "tapir", 201 | "tarsier", 202 | "termite", 203 | "tiger", 204 | "toad", 205 | "turkey", 206 | "turtle", 207 | "vicuña", 208 | "wallaby", 209 | "walrus", 210 | "wasp", 211 | "water buffalo", 212 | "weasel", 213 | "whale", 214 | "wolf", 215 | "wolverine", 216 | "wombat", 217 | "wren", 218 | "yak", 219 | "zebra", 220 | } 221 | -------------------------------------------------------------------------------- /src/chat/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Object represents generic message parameters. 4 | // In real-world application it is better to avoid such types for better 5 | // performance. 6 | type Object map[string]interface{} 7 | 8 | type Request struct { 9 | ID int `json:"id"` 10 | Method string `json:"method"` 11 | Params Object `json:"params"` 12 | } 13 | 14 | type Response struct { 15 | ID int `json:"id"` 16 | Result Object `json:"result"` 17 | } 18 | 19 | type Error struct { 20 | ID int `json:"id"` 21 | Error Object `json:"error"` 22 | } 23 | -------------------------------------------------------------------------------- /src/gopool/pool.go: -------------------------------------------------------------------------------- 1 | // Package gopool contains tools for goroutine reuse. 2 | // It is implemented only for examples of github.com/gobwas/ws usage. 3 | package gopool 4 | 5 | import ( 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | // ErrScheduleTimeout returned by Pool to indicate that there no free 11 | // goroutines during some period of time. 12 | var ErrScheduleTimeout = fmt.Errorf("schedule error: timed out") 13 | 14 | // Pool contains logic of goroutine reuse. 15 | type Pool struct { 16 | sem chan struct{} 17 | work chan func() 18 | } 19 | 20 | // NewPool creates new goroutine pool with given size. It also creates a work 21 | // queue of given size. Finally, it spawns given amount of goroutines 22 | // immediately. 23 | func NewPool(size, queue, spawn int) *Pool { 24 | if spawn <= 0 && queue > 0 { 25 | panic("dead queue configuration detected") 26 | } 27 | if spawn > size { 28 | panic("spawn > workers") 29 | } 30 | p := &Pool{ 31 | sem: make(chan struct{}, size), 32 | work: make(chan func(), queue), 33 | } 34 | for i := 0; i < spawn; i++ { 35 | p.sem <- struct{}{} 36 | go p.worker(func() {}) 37 | } 38 | 39 | return p 40 | } 41 | 42 | // Schedule schedules task to be executed over pool's workers. 43 | func (p *Pool) Schedule(task func()) { 44 | p.schedule(task, nil) 45 | } 46 | 47 | // ScheduleTimeout schedules task to be executed over pool's workers. 48 | // It returns ErrScheduleTimeout when no free workers met during given timeout. 49 | func (p *Pool) ScheduleTimeout(timeout time.Duration, task func()) error { 50 | return p.schedule(task, time.After(timeout)) 51 | } 52 | 53 | func (p *Pool) schedule(task func(), timeout <-chan time.Time) error { 54 | select { 55 | case <-timeout: 56 | return ErrScheduleTimeout 57 | case p.work <- task: 58 | return nil 59 | case p.sem <- struct{}{}: 60 | go p.worker(task) 61 | return nil 62 | } 63 | } 64 | 65 | func (p *Pool) worker(task func()) { 66 | defer func() { <-p.sem }() 67 | 68 | task() 69 | 70 | for task := range p.work { 71 | task() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | var ( 13 | addr = flag.String("listen", ":8888", "port to listen") 14 | chatAddr = flag.String("chat_addr", "localhost:3333", "chat tcp addr to proxy pass") 15 | ) 16 | 17 | func main() { 18 | flag.Parse() 19 | wd, err := os.Getwd() 20 | if err != nil { 21 | log.Fatalf("can not get os working directory: %v", err) 22 | } 23 | 24 | web := http.FileServer(http.Dir(wd + "/web")) 25 | 26 | http.Handle("/", web) 27 | http.Handle("/web/", http.StripPrefix("/web/", web)) 28 | http.Handle("/ws", upstream("chat", "tcp", *chatAddr)) 29 | 30 | log.Printf("proxy is listening on %q", *addr) 31 | log.Fatal(http.ListenAndServe(*addr, nil)) 32 | } 33 | 34 | func upstream(name, network, addr string) http.Handler { 35 | if conn, err := net.Dial(network, addr); err != nil { 36 | log.Printf("warning: test upstream %q error: %v", name, err) 37 | } else { 38 | log.Printf("upstream %q ok", name) 39 | conn.Close() 40 | } 41 | 42 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 | peer, err := net.Dial(network, addr) 44 | if err != nil { 45 | log.Printf("dial upstream error: %v", err) 46 | w.WriteHeader(502) 47 | return 48 | } 49 | if err := r.Write(peer); err != nil { 50 | log.Printf("write request to upstream error: %v", err) 51 | w.WriteHeader(502) 52 | return 53 | } 54 | hj, ok := w.(http.Hijacker) 55 | if !ok { 56 | w.WriteHeader(500) 57 | return 58 | } 59 | conn, _, err := hj.Hijack() 60 | if err != nil { 61 | w.WriteHeader(500) 62 | return 63 | } 64 | 65 | log.Printf( 66 | "serving %s < %s <~> %s > %s", 67 | peer.RemoteAddr(), peer.LocalAddr(), conn.RemoteAddr(), conn.LocalAddr(), 68 | ) 69 | 70 | go func() { 71 | defer peer.Close() 72 | defer conn.Close() 73 | io.Copy(peer, conn) 74 | }() 75 | go func() { 76 | defer peer.Close() 77 | defer conn.Close() 78 | io.Copy(conn, peer) 79 | }() 80 | }) 81 | } 82 | 83 | func indexHandler(wd string) (http.Handler, error) { 84 | index, err := os.Open(wd + "/web/index.html") 85 | if err != nil { 86 | return nil, err 87 | } 88 | stat, err := index.Stat() 89 | if err != nil { 90 | return nil, err 91 | } 92 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 93 | http.ServeContent(w, r, "", stat.ModTime(), index) 94 | }), nil 95 | } 96 | -------------------------------------------------------------------------------- /web/css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | body { 6 | /* Margin bottom by footer height */ 7 | background-color: #073642; 8 | } 9 | .header { 10 | position: fixed; 11 | width: 100%; 12 | height: 60px; 13 | top: 0; 14 | padding: 10px 0; 15 | background-color: white; 16 | } 17 | .footer { 18 | position: fixed; 19 | bottom: 0; 20 | width: 100%; 21 | height: 90px; 22 | padding: 25px 0; 23 | background-color: #ffffff; 24 | } 25 | .content { 26 | padding: 60px 0 90px 0; 27 | } 28 | .nav > li > a { 29 | color: #6c71c4; 30 | } 31 | .nav > li > a:hover { 32 | color: #d33682; 33 | background-color: #fff; 34 | } 35 | .nav > .nav-header { 36 | font-size: 19px; 37 | font-weight: 500; 38 | margin: 5px 10px 0px 0px; 39 | } 40 | .nav-header > a#chat { 41 | color: #073642; 42 | padding: 1px 0; 43 | transition: color 100ms; 44 | } 45 | .nav-header > a#chat:focus, .nav-header > a#chat:hover { 46 | background-color: #fff; 47 | } 48 | .nav-header > a#chat:hover { 49 | color: #6c71c4; 50 | /*color: #d33682;*/ 51 | } 52 | header .user { 53 | color: #839496; 54 | margin: 10px 0; 55 | } 56 | .user { 57 | transition: color 1000ms; 58 | } 59 | .user:hover { 60 | color: #6c71c4; 61 | } 62 | .user > .glyphicon { 63 | margin-right: 7px; 64 | } 65 | .user > input.user-name { 66 | display: inline-block; 67 | border: none; 68 | font-weight: 500; 69 | } 70 | .user > input:hover { 71 | color: #073642; 72 | cursor: pointer; 73 | } 74 | .user > input:focus { 75 | outline: none; 76 | box-shadow: none; 77 | color: #073642; 78 | } 79 | .messages { 80 | padding: 10px; 81 | overflow: scroll; 82 | } 83 | .message { 84 | font-family: Menlo,Monaco,Consolas,"Courier New",monospace; 85 | color: #657b83; 86 | margin: 0; 87 | } 88 | .message > span + span { 89 | margin-left: 8px; 90 | } 91 | .message > .message-time { 92 | color: #586e75; 93 | display: inline-block; 94 | min-width: 67px; 95 | text-align: right; 96 | } 97 | .message > .message-author { 98 | color: #268bd2; 99 | } 100 | .message > .message-text { 101 | color: #859900; 102 | word-break: break-all; 103 | } 104 | .compose-input { 105 | border: 0; 106 | box-shadow: none; 107 | font-size:15px; 108 | } 109 | .compose-input:focus { 110 | outline: none; 111 | box-shadow: none; 112 | } 113 | .crash { 114 | position: absolute; 115 | top: 0; 116 | bottom: 0; 117 | left: 0; 118 | right: 0; 119 | } 120 | .crash-message { 121 | position: absolute; 122 | top: 50%; 123 | margin-top:-25px; 124 | line-height: 50px; 125 | width: 100%; 126 | text-align: center; 127 | color: #dc322f; 128 | font-size: 21px; 129 | } 130 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Gopher chat 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /web/js/main.js: -------------------------------------------------------------------------------- 1 | function getWebSocketEndpoint() { 2 | var h = window.location.href.split("/") 3 | return "ws" + h[0].replace("http", "") + "//" + h[2] + "/ws"; 4 | } 5 | 6 | var Connection = new Client(getWebSocketEndpoint()); 7 | 8 | var Messages = { 9 | list: [], 10 | init: function() { 11 | Connection.handle("publish", function(raw) { 12 | var msg = Object.assign({ 13 | kind: "publish", 14 | }, raw) 15 | Messages.list.push(msg) 16 | m.redraw() 17 | }) 18 | Connection.handle("rename", function(raw) { 19 | Messages.list.push(Object.assign({ 20 | kind:"rename", 21 | }, raw)) 22 | m.redraw() 23 | }) 24 | Connection.handle("greet", function(raw) { 25 | Messages.list.push(Object.assign({ 26 | kind:"greet", 27 | }, raw)) 28 | m.redraw() 29 | }) 30 | Connection.handle("goodbye", function(raw) { 31 | Messages.list.push(Object.assign({ 32 | kind:"goodbye", 33 | }, raw)) 34 | m.redraw() 35 | }) 36 | }, 37 | send: function(msg) { 38 | Connection.call("publish", Object.assign({}, msg, { 39 | time: "" + msg.time 40 | })) 41 | } 42 | }; 43 | 44 | var User = { 45 | name: "" 46 | } 47 | 48 | var Chat = { 49 | lastScroll: 0, 50 | onupdate: function(vnode) { 51 | var scroll = vnode.dom.scrollHeight 52 | if (Chat.lastScroll == scroll) { 53 | return 54 | } 55 | Chat.lastScroll = scroll 56 | document.body.scrollTop = scroll 57 | }, 58 | view: function() { 59 | return m("div.messages", [ 60 | Messages.list.map(function(msg) { 61 | var d = new Date(msg.time); 62 | switch (msg.kind) { 63 | case "rename": 64 | return m("p.message", [ 65 | m("span.message-time", d.toLocaleTimeString()), 66 | m("span.message-prev", msg.prev), 67 | m("span.message-invite", "~>"), 68 | m("span.message-last", msg.name) 69 | ]) 70 | case "publish": 71 | return m("p.message", [ 72 | m("span.message-time", d.toLocaleTimeString()), 73 | m("span.message-author", msg.author), 74 | m("span.message-invite", ">"), 75 | m("span.message-text", msg.text) 76 | ]) 77 | case "greet": 78 | return m("p.message", [ 79 | m("span.message-time", d.toLocaleTimeString()), 80 | m("span.message-author", msg.name), 81 | m("span.message-info", "is here!") 82 | ]) 83 | case "goodbye": 84 | return m("p.message", [ 85 | m("span.message-time", d.toLocaleTimeString()), 86 | m("span.message-author", msg.name), 87 | m("span.message-info", "gone =(") 88 | ]) 89 | } 90 | 91 | }) 92 | ]) 93 | } 94 | }; 95 | 96 | var App = { 97 | view: function(vnode) { 98 | var nav = function(route, caption) { 99 | var p = { 100 | role: "presentation", 101 | } 102 | if (m.route.get() == route) { 103 | p.className = "disabled" 104 | } 105 | return m("li", p, [ 106 | m("a", { href: route, oncreate: m.route.link }, caption) 107 | ]) 108 | } 109 | 110 | var items = [ 111 | m(".col-xs-7", [ 112 | m("ul.nav.nav-pills", [ 113 | m("li.nav-header", {role: "presentation"}, [ 114 | m("a#chat", { href: "/chat", oncreate: m.route.link }, "GoChat"), 115 | ]), 116 | nav("/about", "about"), 117 | ]), 118 | ]) 119 | ] 120 | 121 | if (Bootstrap.ready) { 122 | items.push(m(".col-xs-5.text-right", [ 123 | m("form.user", { 124 | onsubmit: function(e) { 125 | e.preventDefault() 126 | if (User.name.length != 0) { 127 | Bootstrap.rename() 128 | } 129 | } 130 | }, [ 131 | m("span.glyphicon.glyphicon-user"), 132 | m("span.user-prefix", "@"), 133 | m("input.user-name", { 134 | value: User.name, 135 | oncreate: function(vnode) { 136 | vnode.dom.style.width = textWidth(vnode.dom.value) 137 | }, 138 | onfocus: function(e) { 139 | var prev = this.value 140 | setTimeout(function() { 141 | e.target.value = prev 142 | }, 1) 143 | }, 144 | oninput: function(e) { 145 | var el = e.target 146 | User.name = el.value 147 | el.style.width = textWidth(el.value) 148 | }, 149 | onchange: function(e) { 150 | if (User.name.length != 0) { 151 | Bootstrap.rename() 152 | } 153 | } 154 | }), 155 | ]) 156 | ])) 157 | } 158 | 159 | return [ 160 | m("header.header", [ 161 | m("div.container", [ 162 | m(".row", items) 163 | ]) 164 | ]), 165 | m("div.container.content", [ 166 | m("section", vnode.children) 167 | ]) 168 | ] 169 | } 170 | }; 171 | 172 | function textWidth(text) { 173 | var ret = 0; 174 | var div = document.createElement('div'); 175 | document.body.appendChild(div); 176 | m.render(div, m("div", { 177 | oncreate: function(vnode) { 178 | ret = vnode.dom.clientWidth; 179 | }, 180 | onupdate: function(vnode) { 181 | ret = vnode.dom.clientWidth; 182 | }, 183 | style: { 184 | "font-weight": "500", 185 | "font-size": "14px", 186 | "position": "absolute", 187 | "visibility": "hidden", 188 | "height": "auto", 189 | "width": "auto", 190 | "white-space": "nowrap" 191 | }, 192 | }, text)); 193 | 194 | ret = ret + 5 + "px"; 195 | document.body.removeChild(div); 196 | 197 | return ret 198 | } 199 | 200 | var Message = { 201 | text: "", 202 | reset: function() { 203 | var text = Message.text 204 | Message.text = "" 205 | return text 206 | } 207 | } 208 | 209 | var Compose = { 210 | oncreate: function() { 211 | setTimeout(function() { 212 | document.getElementById("compose").focus() 213 | }, 10) 214 | }, 215 | view: function() { 216 | return m("footer.footer", [ 217 | m("div.container", [ 218 | m("form.form-horizontal.compose", 219 | {onsubmit: function(e) { 220 | e.preventDefault() 221 | var text = Message.reset() 222 | if (text.length == 0) { 223 | return 224 | } 225 | Messages.send({ 226 | author: User.name, 227 | text: text, 228 | time: Date.now() 229 | }) 230 | }}, 231 | [ 232 | m("div.form-group", [ 233 | m("div.col-xs-12", [ 234 | m("input.form-control.compose-input#compose", { 235 | type: "text", 236 | value: Message.text, 237 | placeholder: "Write a message...", 238 | autocomplete: "off", 239 | oninput: m.withAttr("value", function(value) { 240 | Message.text = value 241 | }) 242 | }) 243 | ]) 244 | ]), 245 | ] 246 | ) 247 | ]) 248 | ]) 249 | } 250 | } 251 | 252 | var About = { 253 | view: function() { 254 | return m("div", "hello, websocket!") 255 | } 256 | }; 257 | 258 | var Bootstrap = { 259 | ready: false, 260 | oninit: function() { 261 | Messages.init() 262 | Connection.handle("hello", function(raw) { 263 | User.name = raw.name 264 | 265 | Bootstrap.ready = true 266 | if (Bootstrap.spinner) { 267 | Bootstrap.spinner.stop() 268 | } 269 | m.redraw(); 270 | }) 271 | }, 272 | connect: function() { 273 | var self = this; 274 | this.ready = false 275 | 276 | m.route.set("/") 277 | 278 | return Connection.connect() 279 | .catch(function(err) { 280 | console.warn("connect error:", err); 281 | self.err = err; 282 | }) 283 | }, 284 | rename: function() { 285 | return Connection 286 | .call("rename", { name: User.name }) 287 | .then(function() { 288 | console.log("rename ok" ) 289 | }) 290 | .catch(function(err) { 291 | console.warn("rename error:", err); 292 | }); 293 | }, 294 | oncreate: function() { 295 | if (this.ready) { 296 | return 297 | } 298 | if (this.err != null) { 299 | return; 300 | } 301 | 302 | setTimeout(function() { 303 | if (Bootstrap.ready) { 304 | return 305 | } 306 | var opts = { 307 | lines: 17, 308 | length: 12, 309 | width: 2, 310 | radius: 12, 311 | color: '#268bd2', 312 | opacity: 0.1, 313 | speed: 1.5, 314 | } 315 | Bootstrap.spinner = new Spinner(opts).spin(document.body) 316 | }, 500) 317 | 318 | return Bootstrap.connect() 319 | }, 320 | view: function(vnode) { 321 | if (this.ready) { 322 | m.route.set("/chat") 323 | return 324 | } 325 | if (this.err != null) { 326 | return m(".crash", [ 327 | m(".crash-message", "Oh snap! Something went wrong! =(") 328 | ]) 329 | } 330 | return m("div", "loading...") 331 | } 332 | } 333 | 334 | 335 | m.route(document.body, "/", { 336 | "/": { 337 | render: function() { 338 | return m(Bootstrap) 339 | }, 340 | }, 341 | "/chat": { 342 | render: function() { 343 | if (!Bootstrap.ready) { 344 | m.route.set("/") 345 | return 346 | } 347 | return [ m(App, m(Chat)), m(Compose) ] 348 | } 349 | }, 350 | "/about": { 351 | render: function() { 352 | return m(App, m(About)) 353 | } 354 | } 355 | }); 356 | -------------------------------------------------------------------------------- /web/js/ws.js: -------------------------------------------------------------------------------- 1 | class Client { 2 | constructor(endpoint) { 3 | this.endpoint = endpoint; 4 | this.seq = 1; 5 | this.ready = false; 6 | this.ws = null; 7 | this.pending = {}; 8 | this.handler = {}; 9 | } 10 | 11 | connect() { 12 | var self = this; 13 | if (this.ws != null) { 14 | return this.ws 15 | } 16 | return this.ws = new Promise(function(resolve, reject) { 17 | var ws = new WebSocket(self.endpoint) 18 | var pending = true; 19 | ws.onerror = function(err) { 20 | if (pending) { 21 | pending = false; 22 | reject(err) 23 | return 24 | } 25 | 26 | console.warn("websocket lifetime error:" + err) 27 | Object.keys(self.pending).forEach(function(k) { 28 | self.pending[k].reject(err); 29 | delete self.pending[k]; 30 | }) 31 | }; 32 | ws.onopen = function() { 33 | if (pending) { 34 | pending = false 35 | resolve(ws) 36 | } 37 | }; 38 | ws.onmessage = function(s) { 39 | var msg, request, handler; 40 | try { 41 | msg = JSON.parse(s.data); 42 | } catch (err) { 43 | console.warn("parse incoming message error:", err); 44 | return 45 | } 46 | 47 | // Notice 48 | if (msg.id == void 0 || msg.id == 0) { 49 | if (handler = self.handler[msg.method]) { 50 | handler.forEach((h) => h(msg.params, self)); 51 | return 52 | } 53 | console.warn("no handler for method:", msg.method); 54 | return 55 | } 56 | 57 | request = self.pending[msg.id]; 58 | if (request == null) { 59 | console.warn("no pending request for:", msg.method, msg.id); 60 | return 61 | } 62 | 63 | delete self.pending[msg.id]; 64 | if (msg.error != null) { 65 | request.reject(msg.error); 66 | } else { 67 | request.resolve(msg.result); 68 | } 69 | 70 | return; 71 | }; 72 | }) 73 | } 74 | 75 | call(method, params) { 76 | var self = this; 77 | return this.connect() 78 | .then(function(conn) { 79 | var seq = self.seq++; 80 | var dfd = defer(); 81 | self.pending[seq] = dfd; 82 | conn.send(JSON.stringify({ 83 | id: seq, 84 | method: method, 85 | params: params 86 | })) 87 | return dfd.promise; 88 | }) 89 | } 90 | 91 | handle(method, callback) { 92 | var list = this.handler[method]; 93 | if (list == null) { 94 | this.handler[method] = [callback]; 95 | return 96 | } 97 | list.push(callback); 98 | } 99 | } 100 | 101 | function defer() { 102 | var d = {} 103 | d.promise = new Promise(function(resolve, reject) { 104 | d.resolve = resolve; 105 | d.reject = reject; 106 | }) 107 | return d 108 | } 109 | --------------------------------------------------------------------------------