simmi
simmi

Reputation: 29

websocket clients: is node.js really faster than go (gorilla and gobwas)?

I am moving my websocket code from node.js to golang where I do a lot of processing on the data. The critial issue for me is to read and process the data as quickly as possible. On just reading the data and comparing node.js to the two golang solutions, I cannot get golang to be as fast as node.js. In the benchmarks below, the golang-gobwas solution is 2.2 ms slower on average over 200k messages (faster in 22% of the cases), while gorilla is 1.8 ms slower (and faster in 23% of the cases).

The benchmark node.js code is:

"use strict"
const WebSocket   = require('ws')

var ws = new WebSocket("wss://api.hitbtc.com/api/2/ws")

ws.onopen = function(evt) { 
    hitbtc_marketnames().forEach( function (marketname) {
        var marketid = marketname.replace('/', '')
        send_args({ method: "subscribeOrderbook", params: {symbol: marketid}, id: 123}, ws)
        send_args({ method: "subscribeTrades", params: {symbol: marketid}, id: 124}, ws)
    })
}
ws.onerror = function(evt) { 
    throw('error')
}
ws.onclose = function(evt) {
    throw('connection closed')
}
ws.onmessage = function(evt) { 

    var data = JSON.parse(evt.data)
    var ts = Date.now() / 1000

    if (data != undefined && data.params != undefined && data.params.timestamp != undefined) {
        var delay = Date.now() - new Date(data.params.timestamp).getTime()
        console.log(data.params.symbol, ";", data.params.sequence, ";", data.params.timestamp, ";", delay, '; js ;', evt.data.length)
    }

}

function send_args (args, ws ) {
    var msg = JSON.stringify(args)
    console.log(Date.now(), ' send: '+msg)
    try {
        ws.send(msg)
    } catch(ex) {
        console.log(ex)
    }
}

function hitbtc_marketnames() {
    // return ['ETH/PAX']
    return ['ADA/BCH','ADA/BTC','ADA/ETH','ADA/USD','BCH/EURS','BNB/BTC','BNB/ETH','BNB/USD','BSV/BTC','BSV/USD','BTC/EURS','BTC/PAX','BTC/USD','BTC/USDC','BTG/BTC','BTG/ETH','BTG/USD','DASH/BCH','DASH/BTC','DASH/EOS','DASH/ETH','DASH/EURS','DASH/USD','DOGE/BTC','DOGE/ETH','DOGE/USD','EOS/BCH','EOS/BTC','EOS/ETH','EOS/EURS','EOS/PAX','EOS/USD','ETC/BCH','ETC/BTC','ETC/ETH','ETC/USD','ETH/BTC','ETH/EURS','ETH/PAX','ETH/USD','ETH/USDC','EURS/USD','HT/BTC','HT/USD','IOTA/BTC','IOTA/ETH','IOTA/USD','LEO/USD','LINK/BCH','LINK/BTC','LINK/ETH','LINK/USD','LTC/BCH','LTC/BTC','LTC/EOS','LTC/ETH','LTC/EURS','LTC/USD','NEO/BTC','NEO/EOS','NEO/ETH','NEO/EURS','NEO/USD','OMG/BCH','OMG/BTC','OMG/ETH','OMG/USD','QTUM/BTC','QTUM/ETH','QTUM/USD','TRX/BCH','TRX/BTC','TRX/EOS','TRX/ETH','TRX/USD','USD/PAX','USDT/USD','USD/USDC','XEM/BTC','XEM/ETH','XLM/BCH','XLM/BTC','XLM/ETH','XLM/USD','XMR/BCH','XMR/BTC','XMR/EOS','XMR/ETH','XMR/EURS','XMR/USD','XRP/BCH','XRP/BTC','XRP/EOS','XRP/ETH','XRP/EURS','XRP/USDT','XTZ/BTC','XTZ/ETH','XTZ/USD','ZEC/BCH','ZEC/BTC','ZEC/EOS','ZEC/ETH','ZEC/EURS','ZEC/USD']
}

The golang-gobwas solution is

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "github.com/gobwas/ws"
    "github.com/gobwas/ws/wsutil"
    "log"
    "net/url"
    "os"
    "os/signal"
    "strings"
    "time"
)

func hitbtc_marketname() []string {
    return []string{"ADA/BCH", "ADA/BTC", "ADA/ETH", "ADA/USD", "BCH/EURS", "BNB/BTC", "BNB/ETH", "BNB/USD", "BSV/BTC", "BSV/USD", "BTC/EURS", "BTC/PAX", "BTC/USD", "BTC/USDC", "BTG/BTC", "BTG/ETH", "BTG/USD", "DASH/BCH", "DASH/BTC", "DASH/EOS", "DASH/ETH", "DASH/EURS", "DASH/USD", "DOGE/BTC", "DOGE/ETH", "DOGE/USD", "EOS/BCH", "EOS/BTC", "EOS/ETH", "EOS/EURS", "EOS/PAX", "EOS/USD", "ETC/BCH", "ETC/BTC", "ETC/ETH", "ETC/USD", "ETH/BTC", "ETH/EURS", "ETH/PAX", "ETH/USD", "ETH/USDC", "EURS/USD", "HT/BTC", "HT/USD", "IOTA/BTC", "IOTA/ETH", "IOTA/USD", "LEO/USD", "LINK/BCH", "LINK/BTC", "LINK/ETH", "LINK/USD", "LTC/BCH", "LTC/BTC", "LTC/EOS", "LTC/ETH", "LTC/EURS", "LTC/USD", "NEO/BTC", "NEO/EOS", "NEO/ETH", "NEO/EURS", "NEO/USD", "OMG/BCH", "OMG/BTC", "OMG/ETH", "OMG/USD", "QTUM/BTC", "QTUM/ETH", "QTUM/USD", "TRX/BCH", "TRX/BTC", "TRX/EOS", "TRX/ETH", "TRX/USD", "USD/PAX", "USDT/USD", "USD/USDC", "XEM/BTC", "XEM/ETH", "XLM/BCH", "XLM/BTC", "XLM/ETH", "XLM/USD", "XMR/BCH", "XMR/BTC", "XMR/EOS", "XMR/ETH", "XMR/EURS", "XMR/USD", "XRP/BCH", "XRP/BTC", "XRP/EOS", "XRP/ETH", "XRP/EURS", "XRP/USDT", "XTZ/BTC", "XTZ/ETH", "XTZ/USD", "ZEC/BCH", "ZEC/BTC", "ZEC/EOS", "ZEC/ETH", "ZEC/EURS", "ZEC/USD"}
}

type messageReceived struct {
    Jsonrpc string
    Method  string
    Params  struct {
        Bid       []interface{}
        Ask       []interface{}
        Data      []interface{}
        Sequence  int64
        Symbol    string
        Timestamp string
    }
}

func main() {

    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)

    u := url.URL{Scheme: "wss", Host: "api.hitbtc.com", Path: "api/2/ws"}
    fmt.Println("connecting to", u.String())

    conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), u.String())
    if err != nil {
        log.Fatal(err)
    }

    defer conn.Close()

    var bJson []byte

    for _, marketname := range hitbtc_marketname() {

        m := map[string]interface{}{
            "method": "subscribeOrderbook",
            "params": map[string]string{
                "symbol": strings.ReplaceAll(marketname, "/", ""),
            },
            "id": 123,
        }

        bJson, err = json.Marshal(m)
        if err != nil {
            log.Fatal(err)
        }
        err = wsutil.WriteClientMessage(conn, ws.OpText, bJson)
        if err != nil {
            log.Fatal(err)
        }

        m["method"] = "subscribeTrades"
        m["id"] = 124

        bJson, err = json.Marshal(m)
        if err != nil {
            log.Fatal(err)
        }
        err = wsutil.WriteClientMessage(conn, ws.OpText, bJson)
        if err != nil {
            log.Fatal(err)
        }
    }

    go func() {

        for {

            var t time.Time
            var data messageReceived

            msg, _, err := wsutil.ReadServerData(conn)
            if err != nil {
                log.Fatal(err)
            }

            json.Unmarshal(msg, &data)

            if len(data.Params.Timestamp) > 0 {
                t, err = time.Parse("2006-01-02T15:04:05.000Z", data.Params.Timestamp)
                if err != nil {
                    log.Fatal(err)
                }
                fmt.Println(data.Params.Symbol, ";", data.Params.Sequence, ";", data.Params.Timestamp, ";", time.Now().Sub(t).Seconds()*1000, "; gobwas ;", len(msg))
            }
        }
    }()

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-interrupt:
            log.Println("interrupt")
            select {
            case <-time.After(time.Second):
            }
            return
        }
    }

}

The golang-gorilla code is

package main

import (
    "flag"
    "fmt"
    "github.com/gorilla/websocket"
    "log"
    "os"
    "os/signal"
    "strings"
    "time"
)

func hitbtc_marketname() []string {
    return []string{"ADA/BCH", "ADA/BTC", "ADA/ETH", "ADA/USD", "BCH/EURS", "BNB/BTC", "BNB/ETH", "BNB/USD", "BSV/BTC", "BSV/USD", "BTC/EURS", "BTC/PAX", "BTC/USD", "BTC/USDC", "BTG/BTC", "BTG/ETH", "BTG/USD", "DASH/BCH", "DASH/BTC", "DASH/EOS", "DASH/ETH", "DASH/EURS", "DASH/USD", "DOGE/BTC", "DOGE/ETH", "DOGE/USD", "EOS/BCH", "EOS/BTC", "EOS/ETH", "EOS/EURS", "EOS/PAX", "EOS/USD", "ETC/BCH", "ETC/BTC", "ETC/ETH", "ETC/USD", "ETH/BTC", "ETH/EURS", "ETH/PAX", "ETH/USD", "ETH/USDC", "EURS/USD", "HT/BTC", "HT/USD", "IOTA/BTC", "IOTA/ETH", "IOTA/USD", "LEO/USD", "LINK/BCH", "LINK/BTC", "LINK/ETH", "LINK/USD", "LTC/BCH", "LTC/BTC", "LTC/EOS", "LTC/ETH", "LTC/EURS", "LTC/USD", "NEO/BTC", "NEO/EOS", "NEO/ETH", "NEO/EURS", "NEO/USD", "OMG/BCH", "OMG/BTC", "OMG/ETH", "OMG/USD", "QTUM/BTC", "QTUM/ETH", "QTUM/USD", "TRX/BCH", "TRX/BTC", "TRX/EOS", "TRX/ETH", "TRX/USD", "USD/PAX", "USDT/USD", "USD/USDC", "XEM/BTC", "XEM/ETH", "XLM/BCH", "XLM/BTC", "XLM/ETH", "XLM/USD", "XMR/BCH", "XMR/BTC", "XMR/EOS", "XMR/ETH", "XMR/EURS", "XMR/USD", "XRP/BCH", "XRP/BTC", "XRP/EOS", "XRP/ETH", "XRP/EURS", "XRP/USDT", "XTZ/BTC", "XTZ/ETH", "XTZ/USD", "ZEC/BCH", "ZEC/BTC", "ZEC/EOS", "ZEC/ETH", "ZEC/EURS", "ZEC/USD"}
}

type messageReceived struct {
    Jsonrpc string
    Method  string
    Params  struct {
        Bid       []interface{}
        Ask       []interface{}
        Data      []interface{}
        Sequence  int64
        Symbol    string
        Timestamp string
    }
}

func main() {
    flag.Parse()
    log.SetFlags(0)

    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)

    c, _, err := websocket.DefaultDialer.Dial("wss://api.hitbtc.com/api/2/ws", nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    defer c.Close()

    done := make(chan struct{})

    for _, channel := range hitbtc_marketname() {

        m := map[string]interface{}{
            "method": "subscribeOrderbook",
            "params": map[string]string{
                "symbol": strings.ReplaceAll(channel, "/", ""),
            },
            "id": 123,
        }
        err = c.WriteJSON(m)
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }

        m["method"] = "subscribeTrades"
        m["id"] = 124
        err = c.WriteJSON(m)
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }

    }

    go func() {
        defer close(done)
        for {

            var t time.Time
            var data messageReceived

            err := c.ReadJSON(&data)
            if err != nil {
                log.Println("error:", err)
                os.Exit(0)
            }

            if len(data.Params.Timestamp) > 0 {
                t, err = time.Parse("2006-01-02T15:04:05.000Z", data.Params.Timestamp)
                if err != nil {
                    log.Fatal(err)
                }
                fmt.Println(data.Params.Symbol, ";", data.Params.Sequence, ";", data.Params.Timestamp, ";", time.Now().Sub(t).Seconds()*1000, "; gorilla")
            }

        }
    }()

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-done:
            return
        case <-interrupt:
            log.Println("interrupt")
            select {
            case <-done:
            case <-time.After(time.Second):
            }
            return
        }
    }
}

Methodology: I output the strings of each snippet to terminal. I am running the three solutions simultaneously and comparing the delay times message by message. Everything run on a good linux server running the latest version of debian.

Is node.js really just faster than golang, or what am I missing?

Upvotes: 2

Views: 7281

Answers (1)

Dmitry Harnitski
Dmitry Harnitski

Reputation: 6008

Your code process all messages in the same goroutine.

That basically blocks new message receiving till previous message is processed.

Try this (gorilla) to see if that helps:


    go func() {

        for {
            msg, _, err := wsutil.ReadServerData(conn)
            if err != nil {
                log.Fatal(err)
            }
            // dedicated goroutine for message processing, unblocking current one
            go func() {
                var t time.Time
                var data messageReceived

                json.Unmarshal(msg, &data)

                if len(data.Params.Timestamp) > 0 {
                    t, err = time.Parse("2006-01-02T15:04:05.000Z", data.Params.Timestamp)
                    if err != nil {
                        log.Fatal(err)
                    }
                    fmt.Println(data.Params.Symbol, ";", data.Params.Sequence, ";", data.Params.Timestamp, ";", time.Now().Sub(t).Seconds()*1000, "; gobwas ;", len(msg))
                }
            }()

        }
    }()

Upvotes: 4

Related Questions