Burrow, Websocket
Burrow starts RPC info service on both http and ws.
Config
[RPC.Info]
Enabled = true
ListenHost = "0.0.0.0"
ListenPort = "26658"
Startup
func InfoLauncher(kern *Kernel, conf *rpc.ServerConfig) process.Launcher {
return process.Launcher{
Name: InfoProcessName,
Enabled: conf.Enabled,
Launch: func() (process.Process, error) {
listener, err := process.ListenerFromAddress(conf.ListenAddress())
if err != nil {
return nil, err
}
err = kern.registerListener(InfoProcessName, listener)
if err != nil {
return nil, err
}
server, err := rpcinfo.StartServer(kern.Service, "/websocket", listener, kern.Logger)
if err != nil {
return nil, err
}
return server, nil
},
}
}
func StartServer(service *rpc.Service, pattern string, listener net.Listener, logger *logging.Logger) (*http.Server, error) {
logger = logger.Clone("RpcInfo")
routes := GetRoutes(service)
mux := http.NewServeMux()
wm := server.NewWebsocketManager(routes, logger)
mux.HandleFunc(pattern, wm.WebsocketHandler)
server.RegisterRPCFuncs(mux, routes, logger)
srv, err := server.StartHTTPServer(listener, mux, logger)
if err != nil {
return nil, err
}
return srv, nil
}
It creates a MUX for handling http request. THis MUX will route ‘/websocket’ to a WebsocketHandler.
WebsocketHandler
It will try to upgrade from http to ws, and create a wsConnection for further processing if need.
// WebsocketHandler upgrades the request/response (via http.Hijack) and starts the wsConnection.
func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Request) {
wsConn, err := wm.Upgrade(w, r, nil)
if err != nil {
// TODO - return http error
wm.logger.TraceMsg("Failed to upgrade to websocket connection", structure.ErrorKey, err)
return
}
// register connection
con := NewWSConnection(wsConn, wm.funcMap, wm.logger, wm.wsConnOptions...)
wm.logger.InfoMsg("New websocket connection", "remote_address", con.remoteAddr)
err = con.Start() // Blocking
if err != nil {
wm.logger.TraceMsg("Error starting connection", structure.ErrorKey, err)
}
}
wsConnection repys onTendermint’s BaseService for connection lifecycle management. It will create a read routine and at the same time block on the write routine.
// A single websocket connection contains listener id, underlying ws
// connection, and the event switch for subscribing to events.
//
// In case of an error, the connection is stopped.
type wsConnection struct {
service.BaseService
remoteAddr string
baseConn *websocket.Conn
writeChan chan types.RPCResponse
funcMap map[string]*RPCFunc
// write channel capacity
writeChanCapacity int
// each write times out after this.
writeWait time.Duration
// Connection times out if we haven't received *anything* in this long, not even pings.
readWait time.Duration
// Send pings to server with this period. Must be less than readWait, but greater than zero.
pingPeriod time.Duration
// object that is used to subscribe / unsubscribe from events
eventSub types.EventSubscriber
}
// OnStart implements service.Service by starting the read and write routines. It
// blocks until the connection closes.
func (wsc *wsConnection) OnStart() error {
wsc.writeChan = make(chan types.RPCResponse, wsc.writeChanCapacity)
// Read subscriptions/unsubscriptions to events
go wsc.readRoutine()
// Write responses, BLOCKING.
wsc.writeRoutine()
return nil
}
Call Stack
Call Stack of ws startup:
github.com/hyperledger/burrow/rpc/lib/server.(*wsConnection).OnStart at handlers.go:463
github.com/tendermint/tendermint/libs/service.(*BaseService).Start at service.go:139
github.com/hyperledger/burrow/rpc/lib/server.(*WebsocketManager).WebsocketHandler at handlers.go:713
github.com/hyperledger/burrow/rpc/lib/server.(*WebsocketManager).WebsocketHandler-fm at handlers.go:702
net/http.HandlerFunc.ServeHTTP at server.go:2007
net/http.(*ServeMux).ServeHTTP at server.go:2387
github.com/hyperledger/burrow/rpc/lib/server.RecoverAndLogHandler.func1 at http_server.go:93
net/http.HandlerFunc.ServeHTTP at server.go:2007
net/http.serverHandler.ServeHTTP at server.go:2802
net/http.(*conn).serve at server.go:1890
runtime.goexit at asm_amd64.s:1357
- Async stack trace
net/http.(*Server).Serve at server.go:2928
ws
readRoutine
It basically reads message from the ws connection,unmarshal the message, call the RPC func according to the unmarshalled mesage, and eventually write response back to ws Conn.
_, in, err := wsc.baseConn.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
wsc.Logger.Info("Client closed the connection")
} else {
wsc.Logger.Error("Failed to read request", "err", err)
}
wsc.Stop()
return
}
var request types.RPCRequest
err = json.Unmarshal(in, &request)
if err != nil {
wsc.WriteRPCResponse(types.RPCParseError("", errors.Wrap(err, "Error unmarshaling request")))
continue
}
// A Notification is a Request object without an "id" member.
// The Server MUST NOT reply to a Notification, including those that are within a batch request.
if request.ID == "" {
wsc.Logger.Debug("WSJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)")
continue
}
// Now, fetch the RPCFunc and execute it.
rpcFunc := wsc.funcMap[request.Method]
if rpcFunc == nil {
wsc.WriteRPCResponse(types.RPCMethodNotFoundError(request.ID))
continue
}
var args []reflect.Value
if rpcFunc.ws {
wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc}
if len(request.Params) > 0 {
args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx)
}
} else {
if len(request.Params) > 0 {
args, err = jsonParamsToArgsRPC(rpcFunc, request.Params)
}
}
if err != nil {
wsc.WriteRPCResponse(types.RPCInternalError(request.ID, errors.Wrap(err, "Error converting json params to arguments")))
continue
}
returns := rpcFunc.f.Call(args)
// TODO: Need to encode args/returns to string if we want to log them
wsc.Logger.Info("WSJSONRPC", "method", request.Method)
result, err := unreflectResult(returns)
if err != nil {
wsc.WriteRPCResponse(types.RPCInternalError(request.ID, err))
continue
} else {
wsc.WriteRPCResponse(types.NewRPCSuccessResponse(request.ID, result))
continue
}
writeRoutine
It will handle ws PING/PONG ticker, and wait on writeChan for any ws writing indication from reader routine.
for {
select {
case m := <-pongs:
err := wsc.writeMessageWithDeadline(websocket.PongMessage, []byte(m))
if err != nil {
wsc.Logger.Info("Failed to write pong (client may disconnect)", "err", err)
}
case <-pingTicker.C:
err := wsc.writeMessageWithDeadline(websocket.PingMessage, []byte{})
if err != nil {
wsc.Logger.Error("Failed to write ping", "err", err)
wsc.Stop()
return
}
case msg := <-wsc.writeChan:
jsonBytes, err := json.MarshalIndent(msg, "", " ")
if err != nil {
wsc.Logger.Error("Failed to marshal RPCResponse to JSON", "err", err)
} else {
if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil {
wsc.Logger.Error("Failed to write response", "err", err)
wsc.Stop()
return
}
}
case <-wsc.Quit():
return
}
}