├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── binance.go ├── binance_test.go ├── go.mod ├── go.sum ├── models.go ├── pool └── pool.go └── ws └── ws.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | 6 | git: 7 | depth: 1 8 | 9 | notifications: 10 | email: false 11 | 12 | script: 13 | - go test -v -race ./... # Run all the tests with the race detector enabled 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexey M 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/alexey-ernest/go-binance-websocket.svg?branch=master)](https://travis-ci.com/alexey-ernest/go-binance-websocket) 2 | 3 | # go-binance-websocket 4 | Binance websocket client with optimized latency 5 | 6 | ## Optimized latency 7 | Leveraging fast json deserializer and object pool for good base performance (i5 6-core): ~1000 ns/op or ~1M op/s 8 | ``` 9 | $ go test --bench=. --benchtime 30s --benchmem 10 | 11 | BenchmarkBinanceMessageHandling-6 38260412 946 ns/op 128 B/op 8 allocs/op 12 | ``` 13 | 14 | ## Example 15 | 16 | ``` 17 | import ( 18 | . "github.com/alexey-ernest/go-binance-websocket" 19 | "log" 20 | ) 21 | 22 | func main() { 23 | ws := NewBinanceWs() 24 | messages := make(chan *Depth, 10) 25 | err, _ := ws.SubscribeDepth("btcusdt", func (d *Depth) { 26 | messages <- d 27 | }) 28 | 29 | if err != nil { 30 | log.Fatalf("failed to connect to binance @depth websocket") 31 | } 32 | 33 | for m := range messages { 34 | log.Printf("%+v\n", m.RawDepth) 35 | m.DecrementReferenceCount() // return object back to the object pool 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /binance.go: -------------------------------------------------------------------------------- 1 | package binancewebsocket 2 | 3 | import ( 4 | "github.com/json-iterator/go" 5 | "github.com/alexey-ernest/go-binance-websocket/ws" 6 | "log" 7 | "fmt" 8 | "time" 9 | ) 10 | 11 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 12 | 13 | type binanceWs struct { 14 | baseURL string 15 | Conn *ws.WsConn 16 | } 17 | 18 | func NewBinanceWs() *binanceWs { 19 | bnWs := &binanceWs{} 20 | bnWs.baseURL = "wss://stream.binance.com:9443/ws" 21 | return bnWs 22 | } 23 | 24 | func (this *binanceWs) subscribe(endpoint string, handle func(msg []byte) error) { 25 | wsBuilder := ws.NewWsBuilder(). 26 | WsUrl(endpoint). 27 | AutoReconnect(). 28 | MessageHandleFunc(handle) 29 | this.Conn = wsBuilder.Build() 30 | } 31 | 32 | func (this *binanceWs) SubscribeDepth(pair string, callback func (*Depth)) (error, chan<- struct{}) { 33 | endpoint := fmt.Sprintf("%s/%s@depth@100ms", this.baseURL, pair) 34 | close := make(chan struct{}) 35 | 36 | handle := func(msg []byte) error { 37 | rawDepth := AcquireDepth() 38 | if err := json.Unmarshal(msg, rawDepth); err != nil { 39 | log.Printf("json unmarshal error: %s", string(msg)) 40 | return err 41 | } 42 | 43 | callback(rawDepth) 44 | return nil 45 | } 46 | this.subscribe(endpoint, handle) 47 | 48 | go func() { 49 | ticker := time.NewTicker(30 * time.Second) 50 | defer ticker.Stop() 51 | 52 | for { 53 | select { 54 | case <- close: 55 | this.Conn.Close() 56 | return 57 | case t := <-ticker.C: 58 | this.Conn.SendPingMessage([]byte(t.String())) 59 | } 60 | } 61 | }() 62 | 63 | return nil, close 64 | } 65 | -------------------------------------------------------------------------------- /binance_test.go: -------------------------------------------------------------------------------- 1 | package binancewebsocket 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestBinanceDepthConnection(t *testing.T) { 9 | ws := NewBinanceWs() 10 | err, close := ws.SubscribeDepth("btcusdt", func (d *Depth) {}) 11 | if err != nil { 12 | t.Fatalf("failed to connect to binance @depth websocket") 13 | } 14 | close <- struct{}{} 15 | } 16 | 17 | func TestBinanceDepthCloseConnectionDirectly(t *testing.T) { 18 | ws := NewBinanceWs() 19 | err, _ := ws.SubscribeDepth("btcusdt", func (d *Depth) {}) 20 | if err != nil { 21 | t.Fatalf("failed to connect to binance @depth websocket") 22 | } 23 | 24 | ws.Conn.Close() 25 | time.Sleep(time.Duration(1) * time.Second) 26 | } 27 | 28 | func TestBinanceDepthMessage(t *testing.T) { 29 | ws := NewBinanceWs() 30 | messages := make(chan *Depth, 10) 31 | err, close := ws.SubscribeDepth("btcusdt", func (d *Depth) { 32 | messages <- d 33 | }) 34 | 35 | if err != nil { 36 | t.Fatalf("failed to connect to binance @depth websocket") 37 | } 38 | 39 | msg := <- messages 40 | if msg.LastUpdateID == 0 { 41 | t.Errorf("LastUpdateID should not be 0") 42 | } 43 | if len(msg.Bids) == 0 && len(msg.Asks) == 0 { 44 | t.Errorf("Depth update should contain asks or bids") 45 | } 46 | 47 | close <- struct{}{} 48 | } 49 | 50 | func BenchmarkBinanceMessageHandling(b *testing.B) { 51 | ws := NewBinanceWs() 52 | //messages := make(chan *Depth, 10) 53 | err, close := ws.SubscribeDepth("btcusdt2", func (d *Depth) { 54 | d.DecrementReferenceCount() 55 | }) 56 | if err != nil { 57 | b.Fatalf("failed to connect to binance @depth websocket") 58 | } 59 | 60 | b.ResetTimer() 61 | msg := []byte("{\"e\":\"depthUpdate\",\"E\":1577485630559,\"s\":\"BTCUSDT\",\"U\":1627259958,\"u\":1627259960,\"b\":[[\"7246.02000000\",\"0.00000000\"],[\"7246.00000000\",\"0.02930400\"],[\"7245.75000000\",\"0.00000000\"],[\"7239.18000000\",\"0.00000000\"]],\"a\":[]}") 62 | for i := 0; i < b.N; i += 1 { 63 | ws.Conn.ReceiveMessage(msg) 64 | } 65 | 66 | close <- struct{}{} 67 | } 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alexey-ernest/go-binance-websocket 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.1 7 | github.com/json-iterator/go v1.1.9 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexey-ernest/go-binance-websocket v0.0.0-20191216230621-c8cc2acb760d h1:Rq9/4GVvxIqbK3RyAm3zmOcR2lQI3VYpditWcIajedk= 2 | github.com/alexey-ernest/go-binance-websocket v0.0.0-20191216230621-c8cc2acb760d/go.mod h1:Ip1Y9A/0RFFFvPUJbgXLybmpmpdglKmJinMEwhqfoLM= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 6 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 7 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 8 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 9 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 10 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 11 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 12 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 13 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 17 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package binancewebsocket 2 | 3 | import ( 4 | "github.com/alexey-ernest/go-binance-websocket/pool" 5 | //"errors" 6 | ) 7 | 8 | type RawDepth struct { 9 | LastUpdateID int64 `json:"u"` 10 | Bids [][2]string `json:"b"` 11 | Asks [][2]string `json:"a"` 12 | } 13 | 14 | type Depth struct { 15 | pool.ReferenceCounter `json:"-"` 16 | RawDepth 17 | } 18 | 19 | func (d *Depth) Reset() { 20 | d.Bids = nil 21 | d.Asks = nil 22 | d.LastUpdateID = 0 23 | } 24 | 25 | // Used by reference countable pool 26 | func ResetDepth(i interface{}) error { 27 | return nil 28 | // obj, ok := i.(*Depth) 29 | // if !ok { 30 | // return errors.New("illegal object sent to ResetDepth") 31 | // } 32 | // obj.Reset() 33 | // return nil 34 | } 35 | 36 | // depth pool 37 | var depthPool = pool.NewReferenceCountedPool( 38 | func(counter pool.ReferenceCounter) pool.ReferenceCountable { 39 | d := new(Depth) 40 | d.ReferenceCounter = counter 41 | return d 42 | }, ResetDepth) 43 | 44 | // Method to get new Depth 45 | func AcquireDepth() *Depth { 46 | return depthPool.Get().(*Depth) 47 | } 48 | -------------------------------------------------------------------------------- /pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | // Highly inpired by this blog post https://www.akshaydeo.com/blog/2017/12/23/How-did-I-improve-latency-by-700-percent-using-syncPool/ 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | ) 9 | 10 | // Interface following reference countable interface 11 | // We have provided inbuilt embeddable implementation of the reference countable pool 12 | // This interface just provides the extensibility for the implementation 13 | type ReferenceCountable interface { 14 | // Method to set the current instance 15 | SetInstance(i interface{}) 16 | 17 | // Method to increment the reference count 18 | IncrementReferenceCount() 19 | 20 | // Method to decrement reference count 21 | DecrementReferenceCount() 22 | } 23 | 24 | // A reference counter struct 25 | // This struct is supposed to be embedded inside the object to be pooled 26 | // Along with that incrementing and decrementing the references is highly important specifically around routines 27 | type ReferenceCounter struct { 28 | count *uint32 `sql:"-" json:"-" yaml:"-"` 29 | pool *sync.Pool `sql:"-" json:"-" yaml:"-"` 30 | released *uint32 `sql:"-" json:"-" yaml:"-"` 31 | reset func(interface{}) error `sql:"-" json:"-" yaml:"-"` 32 | id uint32 `sql:"-" json:"-" yaml:"-"` 33 | instance interface{} `sql:"-" json:"-" yaml:"-"` 34 | } 35 | 36 | // Method to increment a reference counter in a thread safe way 37 | func (r ReferenceCounter) IncrementReferenceCount() { 38 | atomic.AddUint32(r.count, 1) 39 | } 40 | 41 | // Method to decrement a reference counter in a thrad safe way 42 | // If the reference count goes to zero, the object is put back inside the pool 43 | func (r ReferenceCounter) DecrementReferenceCount() { 44 | if atomic.LoadUint32(r.count) == 0 { 45 | panic("reference counter should be > 0") 46 | } 47 | if atomic.AddUint32(r.count, ^uint32(0)) == 0 { // minus 1 48 | atomic.AddUint32(r.released, 1) 49 | if err := r.reset(r.instance); err != nil { 50 | panic("error while resetting an instance => " + err.Error()) 51 | } 52 | 53 | // put back to the pool 54 | r.pool.Put(r.instance) 55 | r.instance = nil 56 | } 57 | } 58 | 59 | // Method to set the current instance 60 | func (r *ReferenceCounter) SetInstance(i interface{}) { 61 | r.instance = i 62 | } 63 | 64 | // Struct representing the pool 65 | type referenceCountedPool struct { 66 | pool *sync.Pool 67 | allocated uint32 68 | returned uint32 69 | referenced uint32 70 | } 71 | 72 | // Method to create a new pool 73 | func NewReferenceCountedPool(factory func(referenceCounter ReferenceCounter) ReferenceCountable, reset func(interface{}) error) *referenceCountedPool { 74 | p := new(referenceCountedPool) 75 | p.pool = new(sync.Pool) 76 | p.pool.New = func() interface{} { 77 | // Incrementing allocated count 78 | atomic.AddUint32(&p.allocated, 1) 79 | 80 | c := factory(ReferenceCounter{ 81 | count: new(uint32), 82 | pool: p.pool, 83 | released: &p.returned, 84 | reset: reset, 85 | id: p.allocated, 86 | }) 87 | return c 88 | } 89 | return p 90 | } 91 | 92 | // Method to get new object 93 | func (p *referenceCountedPool) Get() ReferenceCountable { 94 | c := p.pool.Get().(ReferenceCountable) 95 | c.SetInstance(c) 96 | atomic.AddUint32(&p.referenced, 1) 97 | c.IncrementReferenceCount() 98 | return c 99 | } 100 | 101 | // Method to return reference counted pool stats 102 | func (p *referenceCountedPool) Stats() map[string]interface{} { 103 | return map[string]interface{}{"allocated": p.allocated, "referenced": p.referenced, "returned": p.returned} 104 | } 105 | -------------------------------------------------------------------------------- /ws/ws.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | // A websocket connection abstraction 4 | 5 | import ( 6 | "github.com/json-iterator/go" 7 | "errors" 8 | "github.com/gorilla/websocket" 9 | "net/http" 10 | "net/http/httputil" 11 | "time" 12 | "log" 13 | ) 14 | 15 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 16 | 17 | type WsConfig struct { 18 | WsUrl string 19 | ReqHeaders map[string][]string 20 | MessageHandleFunc func([]byte) error 21 | ErrorHandleFunc func(err error) 22 | IsAutoReconnect bool 23 | IsDump bool 24 | readDeadLineTime time.Duration 25 | } 26 | 27 | type WsConn struct { 28 | c *websocket.Conn 29 | WsConfig 30 | writeBufferChan chan []byte 31 | pingMessageBufferChan chan []byte 32 | closeMessageBufferChan chan []byte 33 | subs []interface{} 34 | close chan struct{} 35 | } 36 | 37 | type WsBuilder struct { 38 | wsConfig *WsConfig 39 | } 40 | 41 | func NewWsBuilder() *WsBuilder { 42 | return &WsBuilder { 43 | &WsConfig { 44 | ReqHeaders: make(map[string][]string, 1), 45 | }, 46 | } 47 | } 48 | 49 | func (b *WsBuilder) WsUrl(wsUrl string) *WsBuilder { 50 | b.wsConfig.WsUrl = wsUrl 51 | return b 52 | } 53 | 54 | func (b *WsBuilder) ReqHeader(key, value string) *WsBuilder { 55 | b.wsConfig.ReqHeaders[key] = append(b.wsConfig.ReqHeaders[key], value) 56 | return b 57 | } 58 | 59 | func (b *WsBuilder) AutoReconnect() *WsBuilder { 60 | b.wsConfig.IsAutoReconnect = true 61 | return b 62 | } 63 | 64 | func (b *WsBuilder) Dump() *WsBuilder { 65 | b.wsConfig.IsDump = true 66 | return b 67 | } 68 | 69 | func (b *WsBuilder) MessageHandleFunc(f func([]byte) error) *WsBuilder { 70 | b.wsConfig.MessageHandleFunc = f 71 | return b 72 | } 73 | 74 | func (b *WsBuilder) ErrorHandleFunc(f func(err error)) *WsBuilder { 75 | b.wsConfig.ErrorHandleFunc = f 76 | return b 77 | } 78 | 79 | func (b *WsBuilder) Build() *WsConn { 80 | wsConn := &WsConn{ 81 | WsConfig: *b.wsConfig, 82 | } 83 | return wsConn.NewWs() 84 | } 85 | 86 | func (ws *WsConn) NewWs() *WsConn { 87 | 88 | ws.readDeadLineTime = time.Minute 89 | 90 | if err := ws.connect(); err != nil { 91 | log.Panic(err) 92 | } 93 | 94 | ws.close = make(chan struct{}, 2) 95 | ws.pingMessageBufferChan = make(chan []byte, 10) 96 | ws.closeMessageBufferChan = make(chan []byte, 1) 97 | ws.writeBufferChan = make(chan []byte, 10) 98 | 99 | go ws.writeRequest() 100 | go ws.receiveMessage() 101 | 102 | return ws 103 | } 104 | 105 | func (ws *WsConn) connect() error { 106 | dialer := websocket.DefaultDialer 107 | 108 | wsConn, resp, err := dialer.Dial(ws.WsUrl, http.Header(ws.ReqHeaders)) 109 | if err != nil { 110 | log.Printf("[ws][%s] %s", ws.WsUrl, err.Error()) 111 | if ws.IsDump && resp != nil { 112 | dumpData, _ := httputil.DumpResponse(resp, true) 113 | log.Printf("[ws][%s] %s", ws.WsUrl, string(dumpData)) 114 | } 115 | return err 116 | } 117 | 118 | ws.c = wsConn 119 | 120 | if ws.IsDump { 121 | dumpData, _ := httputil.DumpResponse(resp, true) 122 | log.Printf("[ws][%s] %s", ws.WsUrl, string(dumpData)) 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func (ws *WsConn) reconnect() { 129 | ws.c.Close() 130 | 131 | var err error 132 | 133 | sleep := 1 134 | for retry := 1; retry <= 10; retry += 1 { 135 | time.Sleep(time.Duration(sleep) * time.Second) 136 | 137 | err = ws.connect() 138 | if err != nil { 139 | log.Printf("[ws][%s] websocket reconnect fail , %s", ws.WsUrl, err.Error()) 140 | } else { 141 | break 142 | } 143 | 144 | sleep <<= 1 145 | } 146 | 147 | if err != nil { 148 | log.Printf("[ws][%s] retry reconnect fail , begin exiting. ", ws.WsUrl) 149 | ws.Close() 150 | if ws.ErrorHandleFunc != nil { 151 | ws.ErrorHandleFunc(errors.New("retry reconnect fail")) 152 | } 153 | } else { 154 | // re-subscribe 155 | var subs []interface{} 156 | copy(subs, ws.subs) 157 | ws.subs = ws.subs[:0] 158 | for _, sub := range subs { 159 | ws.Subscribe(sub) 160 | } 161 | } 162 | } 163 | 164 | func (ws *WsConn) writeRequest() { 165 | var err error 166 | 167 | for { 168 | select { 169 | case <-ws.close: 170 | // stop the goroutine if the ws is closed 171 | //log.Printf("[ws][%s] close websocket, exiting write message goroutine.", ws.WsUrl) 172 | return 173 | case d := <-ws.writeBufferChan: 174 | err = ws.c.WriteMessage(websocket.TextMessage, d) 175 | case d := <-ws.pingMessageBufferChan: 176 | err = ws.c.WriteMessage(websocket.PingMessage, d) 177 | case d := <-ws.closeMessageBufferChan: 178 | err = ws.c.WriteMessage(websocket.CloseMessage, d) 179 | } 180 | 181 | if err != nil { 182 | log.Printf("[ws][%s] %s", ws.WsUrl, err.Error()) 183 | time.Sleep(time.Second) 184 | } 185 | } 186 | } 187 | 188 | func (ws *WsConn) Subscribe(sub interface{}) error { 189 | data, err := json.Marshal(sub) 190 | if err != nil { 191 | log.Printf("[ws][%s] json encode error , %s", ws.WsUrl, err) 192 | return err 193 | } 194 | ws.writeBufferChan <- data 195 | ws.subs = append(ws.subs, sub) 196 | return nil 197 | } 198 | 199 | func (ws *WsConn) SendMessage(msg []byte) { 200 | ws.writeBufferChan <- msg 201 | } 202 | 203 | func (ws *WsConn) SendPingMessage(msg []byte) { 204 | ws.pingMessageBufferChan <- msg 205 | } 206 | 207 | func (ws *WsConn) SendCloseMessage(msg []byte) { 208 | ws.closeMessageBufferChan <- msg 209 | } 210 | 211 | func (ws *WsConn) SendJsonMessage(m interface{}) error { 212 | data, err := json.Marshal(m) 213 | if err != nil { 214 | log.Printf("[ws][%s] json encode error, %s", ws.WsUrl, err) 215 | return err 216 | } 217 | ws.writeBufferChan <- data 218 | return nil 219 | } 220 | 221 | func (ws *WsConn) ReceiveMessage(msg []byte) { 222 | ws.MessageHandleFunc(msg) 223 | } 224 | 225 | func (ws *WsConn) receiveMessage() { 226 | ws.c.SetCloseHandler(func(code int, text string) error { 227 | log.Printf("[ws][%s] websocket exiting [code=%d, text=%s]", ws.WsUrl, code, text) 228 | ws.Close() 229 | return nil 230 | }) 231 | 232 | ws.c.SetPongHandler(func(pong string) error { 233 | // log.Printf("[ws][%s] pong received", ws.WsUrl) 234 | ws.c.SetReadDeadline(time.Now().Add(ws.readDeadLineTime)) 235 | return nil 236 | }) 237 | 238 | ws.c.SetPingHandler(func(ping string) error { 239 | //log.Printf("[ws][%s] ping received", ws.WsUrl) 240 | ws.c.SetReadDeadline(time.Now().Add(ws.readDeadLineTime)) 241 | err := ws.c.WriteMessage(websocket.PongMessage, nil) 242 | if err == nil { 243 | //log.Printf("[ws][%s] pong sent", ws.WsUrl) 244 | } 245 | return err 246 | }) 247 | 248 | for { 249 | 250 | t, msg, err := ws.c.ReadMessage() 251 | 252 | if len(ws.close) > 0 { 253 | // stop goroutine if the ws is closed 254 | //log.Printf("[ws][%s] close websocket, exiting receive message goroutine.", ws.WsUrl) 255 | return 256 | } 257 | 258 | if err != nil { 259 | log.Printf("[ws] error: %s", err) 260 | if ws.IsAutoReconnect { 261 | log.Printf("[ws][%s] Unexpected Closed, Begin Retry Connect.", ws.WsUrl) 262 | ws.reconnect() 263 | continue 264 | } 265 | 266 | if ws.ErrorHandleFunc != nil { 267 | ws.ErrorHandleFunc(err) 268 | } 269 | 270 | return 271 | } 272 | 273 | ws.c.SetReadDeadline(time.Now().Add(ws.readDeadLineTime)) 274 | 275 | switch t { 276 | case websocket.TextMessage: 277 | ws.MessageHandleFunc(msg) 278 | case websocket.BinaryMessage: 279 | ws.MessageHandleFunc(msg) 280 | case websocket.CloseMessage: 281 | ws.Close() 282 | return 283 | default: 284 | log.Printf("[ws][%s] error websocket message type, content is :\n %s \n", ws.WsUrl, string(msg)) 285 | } 286 | } 287 | } 288 | 289 | func (ws *WsConn) Close() { 290 | ws.close <- struct{}{} // one for the write goroutine 291 | ws.close <- struct{}{} // another for the read goroutine 292 | 293 | err := ws.c.Close() 294 | if err != nil { 295 | log.Printf("[ws][%s] close websocket error: %s", ws.WsUrl, err) 296 | } 297 | 298 | if ws.IsDump { 299 | log.Printf("[ws][%s] connection closed", ws.WsUrl) 300 | } 301 | } 302 | --------------------------------------------------------------------------------