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  }