github.com/stafiprotocol/go-substrate-rpc-client@v1.4.7/pkg/recws/recws.go (about) 1 // Package recws provides websocket client based on gorilla/websocket 2 // that will automatically reconnect if the connection is dropped. 3 package recws 4 5 import ( 6 "crypto/tls" 7 "errors" 8 "log" 9 "math/rand" 10 "net/http" 11 "net/url" 12 "sync" 13 "time" 14 15 "github.com/gorilla/websocket" 16 "github.com/jpillora/backoff" 17 ) 18 19 // ErrNotConnected is returned when the application read/writes 20 // a message and the connection is closed 21 var ErrNotConnected = errors.New("websocket: not connected") 22 23 // The RecConn type represents a Reconnecting WebSocket connection. 24 type RecConn struct { 25 // RecIntvlMin specifies the initial reconnecting interval, 26 // default to 2 seconds 27 RecIntvlMin time.Duration 28 // RecIntvlMax specifies the maximum reconnecting interval, 29 // default to 30 seconds 30 RecIntvlMax time.Duration 31 // RecIntvlFactor specifies the rate of increase of the reconnection 32 // interval, default to 1.5 33 RecIntvlFactor float64 34 // HandshakeTimeout specifies the duration for the handshake to complete, 35 // default to 2 seconds 36 HandshakeTimeout time.Duration 37 // Proxy specifies the proxy function for the dialer 38 // defaults to ProxyFromEnvironment 39 Proxy func(*http.Request) (*url.URL, error) 40 // SubscribeHandler fires after the connection successfully establish. 41 SubscribeHandler func() error 42 // KeepAliveTimeout is an interval for sending ping/pong messages 43 // disabled if 0 44 KeepAliveTimeout time.Duration 45 ReadTimeout time.Duration 46 WriteTimeout time.Duration 47 WriteBufferSize int 48 ReadBufferSize int 49 NonVerbose bool 50 51 isConnected bool 52 mu sync.RWMutex 53 url string 54 reqHeader http.Header 55 httpResp *http.Response 56 dialErr error 57 dialer *websocket.Dialer 58 59 *websocket.Conn 60 } 61 62 func (rc *RecConn) MarkUnusable() {} 63 64 // CloseAndReconnect will try to reconnect. 65 func (rc *RecConn) CloseAndReconnect() { 66 rc.Close() 67 go rc.connect() 68 } 69 70 // setIsConnected sets state for isConnected 71 func (rc *RecConn) setIsConnected(state bool) { 72 rc.mu.Lock() 73 defer rc.mu.Unlock() 74 75 rc.isConnected = state 76 } 77 78 func (rc *RecConn) getConn() *websocket.Conn { 79 rc.mu.RLock() 80 defer rc.mu.RUnlock() 81 82 return rc.Conn 83 } 84 85 // Close closes the underlying network connection without 86 // sending or waiting for a close frame. 87 func (rc *RecConn) Close() { 88 if rc.getConn() != nil { 89 rc.mu.Lock() 90 _ = rc.Conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 91 _ = rc.Conn.Close() 92 rc.mu.Unlock() 93 } 94 95 rc.setIsConnected(false) 96 } 97 98 // ReadMessage is a helper method for getting a reader 99 // using NextReader and reading from that reader to a buffer. 100 // 101 // If the connection is closed ErrNotConnected is returned 102 func (rc *RecConn) ReadMessage() (messageType int, message []byte, err error) { 103 err = ErrNotConnected 104 if rc.IsConnected() { 105 rc.mu.Lock() 106 if rc.Conn != nil { 107 if rc.ReadTimeout > 0 { 108 _ = rc.Conn.SetReadDeadline(time.Now().Add(rc.ReadTimeout)) 109 } 110 messageType, message, err = rc.Conn.ReadMessage() 111 } 112 rc.mu.Unlock() 113 } 114 115 return 116 } 117 118 // WriteMessage is a helper method for getting a writer using NextWriter, 119 // writing the message and closing the writer. 120 // 121 // If the connection is closed ErrNotConnected is returned 122 func (rc *RecConn) WriteMessage(messageType int, data []byte) error { 123 err := ErrNotConnected 124 if rc.IsConnected() { 125 rc.mu.Lock() 126 if rc.Conn != nil { 127 if rc.WriteTimeout > 0 { 128 _ = rc.Conn.SetWriteDeadline(time.Now().Add(rc.WriteTimeout)) 129 } 130 err = rc.Conn.WriteMessage(messageType, data) 131 } 132 rc.mu.Unlock() 133 } 134 135 return err 136 } 137 138 // WriteJSON writes the JSON encoding of v to the connection. 139 // 140 // See the documentation for encoding/json Marshal for details about the 141 // conversion of Go values to JSON. 142 // 143 // If the connection is closed ErrNotConnected is returned 144 func (rc *RecConn) WriteJSON(v interface{}) error { 145 err := ErrNotConnected 146 if rc.IsConnected() { 147 rc.mu.Lock() 148 if rc.Conn != nil { 149 if rc.WriteTimeout > 0 { 150 _ = rc.Conn.SetWriteDeadline(time.Now().Add(rc.WriteTimeout)) 151 } 152 err = rc.Conn.WriteJSON(v) 153 } 154 rc.mu.Unlock() 155 } 156 157 return err 158 } 159 160 // ReadJSON reads the next JSON-encoded message from the connection and stores 161 // it in the value pointed to by v. 162 // 163 // See the documentation for the encoding/json Unmarshal function for details 164 // about the conversion of JSON to a Go value. 165 // 166 // If the connection is closed ErrNotConnected is returned 167 func (rc *RecConn) ReadJSON(v interface{}) error { 168 err := ErrNotConnected 169 if rc.IsConnected() { 170 rc.mu.Lock() 171 if rc.Conn != nil { 172 if rc.ReadTimeout > 0 { 173 _ = rc.Conn.SetReadDeadline(time.Now().Add(rc.ReadTimeout)) 174 } 175 err = rc.Conn.ReadJSON(v) 176 } 177 rc.mu.Unlock() 178 } 179 180 return err 181 } 182 183 func (rc *RecConn) setURL(url string) { 184 rc.mu.Lock() 185 defer rc.mu.Unlock() 186 187 rc.url = url 188 } 189 190 func (rc *RecConn) setReqHeader(reqHeader http.Header) { 191 rc.mu.Lock() 192 defer rc.mu.Unlock() 193 194 rc.reqHeader = reqHeader 195 } 196 197 // parseURL parses current url 198 func (rc *RecConn) parseURL(urlStr string) (string, error) { 199 if urlStr == "" { 200 return "", errors.New("dial: url cannot be empty") 201 } 202 203 u, err := url.Parse(urlStr) 204 205 if err != nil { 206 return "", errors.New("url: " + err.Error()) 207 } 208 209 if u.Scheme != "ws" && u.Scheme != "wss" { 210 return "", errors.New("url: websocket uris must start with ws or wss scheme") 211 } 212 213 if u.User != nil { 214 return "", errors.New("url: user name and password are not allowed in websocket URIs") 215 } 216 217 return urlStr, nil 218 } 219 220 func (rc *RecConn) setDefaultRecIntvlMin() { 221 rc.mu.Lock() 222 defer rc.mu.Unlock() 223 224 if rc.RecIntvlMin == 0 { 225 rc.RecIntvlMin = 2 * time.Second 226 } 227 } 228 229 func (rc *RecConn) setDefaultRecIntvlMax() { 230 rc.mu.Lock() 231 defer rc.mu.Unlock() 232 233 if rc.RecIntvlMax == 0 { 234 rc.RecIntvlMax = 30 * time.Second 235 } 236 } 237 238 func (rc *RecConn) setDefaultRecIntvlFactor() { 239 rc.mu.Lock() 240 defer rc.mu.Unlock() 241 242 if rc.RecIntvlFactor == 0 { 243 rc.RecIntvlFactor = 1.5 244 } 245 } 246 247 func (rc *RecConn) setDefaultHandshakeTimeout() { 248 rc.mu.Lock() 249 defer rc.mu.Unlock() 250 251 if rc.HandshakeTimeout == 0 { 252 rc.HandshakeTimeout = 2 * time.Second 253 } 254 } 255 256 func (rc *RecConn) setDefaultProxy() { 257 rc.mu.Lock() 258 defer rc.mu.Unlock() 259 260 if rc.Proxy == nil { 261 rc.Proxy = http.ProxyFromEnvironment 262 } 263 } 264 265 func (rc *RecConn) setDefaultDialer(handshakeTimeout time.Duration) { 266 rc.mu.Lock() 267 defer rc.mu.Unlock() 268 269 rc.dialer = &websocket.Dialer{ 270 HandshakeTimeout: handshakeTimeout, 271 Proxy: rc.Proxy, 272 TLSClientConfig: &tls.Config{RootCAs: nil, InsecureSkipVerify: true}, 273 } 274 if rc.WriteBufferSize > 0 { 275 rc.dialer.WriteBufferSize = rc.WriteBufferSize 276 } 277 if rc.ReadBufferSize > 0 { 278 rc.dialer.ReadBufferSize = rc.ReadBufferSize 279 } 280 } 281 282 func (rc *RecConn) getHandshakeTimeout() time.Duration { 283 rc.mu.RLock() 284 defer rc.mu.RUnlock() 285 286 return rc.HandshakeTimeout 287 } 288 289 // Dial creates a new client connection. 290 // The URL url specifies the host and request URI. Use requestHeader to specify 291 // the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies 292 // (Cookie). Use GetHTTPResponse() method for the response.Header to get 293 // the selected subprotocol (Sec-WebSocket-Protocol) and cookies (Set-Cookie). 294 func (rc *RecConn) Dial(urlStr string, reqHeader http.Header) { 295 296 urlStr, err := rc.parseURL(urlStr) 297 298 if err != nil { 299 log.Fatalf("Dial: %v", err) 300 } 301 302 // Config 303 rc.setURL(urlStr) 304 rc.setReqHeader(reqHeader) 305 rc.setDefaultRecIntvlMin() 306 rc.setDefaultRecIntvlMax() 307 rc.setDefaultRecIntvlFactor() 308 rc.setDefaultHandshakeTimeout() 309 rc.setDefaultProxy() 310 rc.setDefaultDialer(rc.getHandshakeTimeout()) 311 312 // Connect 313 go rc.connect() 314 315 // wait on first attempt 316 time.Sleep(rc.getHandshakeTimeout()) 317 } 318 319 // GetURL returns current connection url 320 func (rc *RecConn) GetURL() string { 321 rc.mu.RLock() 322 defer rc.mu.RUnlock() 323 324 return rc.url 325 } 326 327 func (rc *RecConn) getNonVerbose() bool { 328 rc.mu.RLock() 329 defer rc.mu.RUnlock() 330 331 return rc.NonVerbose 332 } 333 334 func (rc *RecConn) getBackoff() *backoff.Backoff { 335 rc.mu.RLock() 336 defer rc.mu.RUnlock() 337 338 return &backoff.Backoff{ 339 Min: rc.RecIntvlMin, 340 Max: rc.RecIntvlMax, 341 Factor: rc.RecIntvlFactor, 342 Jitter: true, 343 } 344 } 345 346 func (rc *RecConn) hasSubscribeHandler() bool { 347 rc.mu.RLock() 348 defer rc.mu.RUnlock() 349 350 return rc.SubscribeHandler != nil 351 } 352 353 func (rc *RecConn) getKeepAliveTimeout() time.Duration { 354 rc.mu.RLock() 355 defer rc.mu.RUnlock() 356 357 return rc.KeepAliveTimeout 358 } 359 360 func (rc *RecConn) writeControlPingMessage() error { 361 rc.mu.Lock() 362 defer rc.mu.Unlock() 363 364 return rc.Conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)) 365 } 366 367 func (rc *RecConn) keepAlive() { 368 var ( 369 keepAliveResponse = new(keepAliveResponse) 370 ticker = time.NewTicker(rc.getKeepAliveTimeout()) 371 ) 372 373 rc.mu.Lock() 374 rc.Conn.SetPongHandler(func(msg string) error { 375 keepAliveResponse.setLastResponse() 376 return nil 377 }) 378 rc.mu.Unlock() 379 380 go func() { 381 defer ticker.Stop() 382 383 for { 384 if !rc.IsConnected() { 385 continue 386 } 387 388 if err := rc.writeControlPingMessage(); err != nil { 389 log.Println(err) 390 } 391 392 <-ticker.C 393 if time.Since(keepAliveResponse.getLastResponse()) > rc.getKeepAliveTimeout() { 394 rc.Close() 395 return 396 } 397 398 } 399 }() 400 } 401 402 func (rc *RecConn) connect() { 403 b := rc.getBackoff() 404 rand.Seed(time.Now().UTC().UnixNano()) 405 LOOP: 406 for { 407 nextItvl := b.Duration() 408 wsConn, httpResp, err := rc.dialer.Dial(rc.url, rc.reqHeader) 409 if err != nil { 410 break LOOP 411 } 412 rc.mu.Lock() 413 rc.Conn = wsConn 414 rc.dialErr = err 415 rc.isConnected = err == nil 416 rc.httpResp = httpResp 417 rc.mu.Unlock() 418 419 if err == nil { 420 if !rc.getNonVerbose() { 421 422 if !rc.hasSubscribeHandler() { 423 return 424 } 425 426 if err := rc.SubscribeHandler(); err != nil { 427 log.Fatalf("Dial: connect handler failed with %s", err.Error()) 428 } 429 430 if rc.getKeepAliveTimeout() != 0 { 431 rc.keepAlive() 432 } 433 } 434 435 return 436 } 437 438 if !rc.getNonVerbose() { 439 log.Println(err, "Dial: will try again in", nextItvl, "seconds.") 440 } 441 442 time.Sleep(nextItvl) 443 444 } 445 } 446 447 // GetHTTPResponse returns the http response from the handshake. 448 // Useful when WebSocket handshake fails, 449 // so that callers can handle redirects, authentication, etc. 450 func (rc *RecConn) GetHTTPResponse() *http.Response { 451 rc.mu.RLock() 452 defer rc.mu.RUnlock() 453 454 return rc.httpResp 455 } 456 457 // GetDialError returns the last dialer error. 458 // nil on successful connection. 459 func (rc *RecConn) GetDialError() error { 460 rc.mu.RLock() 461 defer rc.mu.RUnlock() 462 463 return rc.dialErr 464 } 465 466 // IsConnected returns the WebSocket connection state 467 func (rc *RecConn) IsConnected() bool { 468 rc.mu.RLock() 469 defer rc.mu.RUnlock() 470 471 return rc.isConnected 472 }