github.com/bitfinexcom/bitfinex-api-go@v0.0.0-20210608095005-9e0b26f200fb/v2/websocket/transport.go (about)

     1  package websocket
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"encoding/json"
     7  	"fmt"
     8  	"github.com/op/go-logging"
     9  	"net"
    10  	"net/http"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/gorilla/websocket"
    15  )
    16  
    17  // size of channel that the websocket writer
    18  // routine pulls from
    19  const WS_WRITE_CAPACITY = 5000
    20  // size of channel that the websocket reader
    21  // routine pushes websocket updates into
    22  const WS_READ_CAPACITY = 10
    23  // seconds to wait in between re-sending
    24  // the keep alive ping
    25  const KEEP_ALIVE_TIMEOUT = 10
    26  
    27  func newWs(baseURL string, logTransport bool, log *logging.Logger) *ws {
    28  	return &ws{
    29  		BaseURL:      baseURL,
    30  		downstream:   make(chan []byte, WS_READ_CAPACITY),
    31  		quit:         make(chan error),
    32  		kill:         make(chan interface{}),
    33  		logTransport: logTransport,
    34  		log:          log,
    35  		lock:         &sync.RWMutex{},
    36  		createTime:   time.Now(),
    37  		writeChan:    make(chan []byte, WS_WRITE_CAPACITY),
    38  	}
    39  }
    40  
    41  type ws struct {
    42  	ws            *websocket.Conn
    43  	lock          *sync.RWMutex
    44  	BaseURL       string
    45  	TLSSkipVerify bool
    46  	downstream    chan []byte
    47  	logTransport  bool
    48  	log           *logging.Logger
    49  	createTime    time.Time
    50  	writeChan     chan []byte
    51  
    52  	kill chan interface{} // signal to routines to kill
    53  	quit chan error    	  // signal to parent with error, if applicable
    54  }
    55  
    56  func (w *ws) Connect() error {
    57  	if w.ws != nil {
    58  		return nil // no op
    59  	}
    60  	var d = websocket.Dialer{
    61  		Subprotocols:    []string{"p1", "p2"},
    62  		ReadBufferSize:  1024,
    63  		WriteBufferSize: 1024,
    64  		Proxy:           http.ProxyFromEnvironment,
    65  		HandshakeTimeout: time.Second * 10,
    66  	}
    67  
    68  	d.TLSClientConfig = &tls.Config{InsecureSkipVerify: w.TLSSkipVerify}
    69  
    70  	w.log.Infof("connecting ws to %s", w.BaseURL)
    71  	ws, resp, err := d.Dial(w.BaseURL, nil)
    72  	if err != nil {
    73  		if err == websocket.ErrBadHandshake {
    74  			w.log.Errorf("bad handshake: status code %d", resp.StatusCode)
    75  		}
    76  		return err
    77  	}
    78  	w.ws = ws
    79  	go w.listenWriteChannel()
    80  	go w.listenWs()
    81  	// Gorilla/go dont natively support keep alive pinging
    82  	// so we need to keep sending a message down the channel to stop
    83  	// tcp killing the connection
    84  	go w.keepAlivePinger()
    85  	return nil
    86  }
    87  
    88  func (w *ws) keepAlivePinger() {
    89  	for {
    90  		pingTimer := time.After(time.Second * KEEP_ALIVE_TIMEOUT)
    91  		select {
    92  		case <-w.kill:
    93  			return
    94  		case <-pingTimer:
    95  			w.writeChan <- []byte("ping")
    96  		}
    97  	}
    98  }
    99  
   100  // Send marshals the given interface and then sends it to the API. This method
   101  // can block so specify a context with timeout if you don't want to wait for too
   102  // long.
   103  func (w *ws) Send(ctx context.Context, msg interface{}) error {
   104  	bs, err := json.Marshal(msg)
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	select {
   110  	case <- ctx.Done():
   111  		return ctx.Err()
   112  	case <- w.kill: // ws closed
   113  		return fmt.Errorf("websocket connection closed")
   114  	default:
   115  	}
   116  	w.log.Debug("ws->srv: %s", string(bs))
   117  	// push request into writer channel
   118  	w.writeChan <- bs
   119  	return nil
   120  }
   121  
   122  func (w *ws) Done() <-chan error {
   123  	return w.quit
   124  }
   125  
   126  // listen for write requests and perform them
   127  func (w *ws) listenWriteChannel() {
   128  	for {
   129  		select {
   130  		case <-w.kill: // ws closed
   131  			return
   132  		case message := <- w.writeChan:
   133  			wsWriter, err := w.ws.NextWriter(websocket.TextMessage)
   134  			if err != nil {
   135  				w.log.Error("Unable to provision ws connection writer: ", err)
   136  				w.stop(err)
   137  				return
   138  			}
   139  			_, err = wsWriter.Write(message)
   140  			if err != nil {
   141  				w.log.Error("Unable to write to ws: ", err)
   142  				w.stop(err)
   143  				return
   144  			}
   145  			if err := wsWriter.Close(); err != nil {
   146  				w.log.Error("Unable to close ws connection writer: ", err)
   147  				w.stop(err)
   148  				return
   149  			}
   150  		}
   151  	}
   152  }
   153  
   154  // listen on ws & fwd to listen()
   155  func (w *ws) listenWs() {
   156  	for {
   157  		if w.ws == nil {
   158  			return
   159  		}
   160  		select {
   161  		case <-w.kill: // ws connection ended
   162  			return
   163  		default:
   164  			_, msg, err := w.ws.ReadMessage()
   165  			if err != nil {
   166  				if cl, ok := err.(*websocket.CloseError); ok {
   167  					w.log.Errorf("close error code: %d", cl.Code)
   168  				}
   169  				// a read during normal shutdown results in an OpError: op on closed connection
   170  				if _, ok := err.(*net.OpError); ok {
   171  					// general read error on a closed network connection, OK
   172  					return
   173  				}
   174  
   175  				w.stop(err)
   176  				return
   177  			}
   178  			w.log.Debugf("srv->ws: %s", string(msg))
   179  			w.lock.RLock()
   180  			if w.downstream == nil {
   181  				w.lock.RUnlock()
   182  				return
   183  			}
   184  			w.downstream <- msg
   185  			w.lock.RUnlock()
   186  		}
   187  	}
   188  }
   189  
   190  func (w *ws) Listen() <-chan []byte {
   191  	return w.downstream
   192  }
   193  
   194  func (w *ws) stop(err error) {
   195  	w.lock.Lock()
   196  	defer w.lock.Unlock()
   197  	if w.ws != nil {
   198  		close(w.kill)
   199  		w.quit <- err // pass error back
   200  		close(w.quit) // signal to parent listeners
   201  		close(w.downstream)
   202  		w.downstream = nil
   203  		if err := w.ws.Close(); err != nil {
   204  			w.log.Error(fmt.Errorf("error closing websocket: %s", err))
   205  		}
   206  		w.ws = nil
   207  	}
   208  }
   209  
   210  // Close the websocket connection
   211  func (w *ws) Close() {
   212  	w.stop(fmt.Errorf("transport connection Close called"))
   213  }