websocket onmessage 在 ajax 请求期间未触发

websocket onmessage not firing during ajax request

我有一个 Web 应用程序执行 Jquery .post 数据库查询请求。我还有三个不同的 Web 套接字连接,用于将状态更新从服务器推送到客户端(实时图表中的 CPU 和内存统计信息、数据库状态和查询队列)。虽然查询不是 运行,但一切顺利,但是一旦开始查询(post 请求),那么三个 Web 套接字连接似乎 hang/block 在等待查询 return。我正在阅读这篇文章,但没有找到任何相关答案……我怀疑这对我来说可能真的很愚蠢……但这让我挠头了一天的大部分时间。我想我可能会尝试将网络套接字连接移动到网络工作者......但理论上,POST 不应该开始阻塞......所以,这里是相关的代码片段......完整源代码有几千行……所以我不想用它淹没任何人……但如果它有用的话可以展示它。所以,最大的问题是我在这里做错了什么?或者,我是否误解了 AJAX 调用在阻塞方面的工作方式?

    // query execution button that grabs the query for the most recently focused query source (SPARQL editor, history, or canned)
      $("#querySubmitButton").on("click", function(e) {
        // Disable the query button
        $("#querySubmitButton").attr('disabled',true);

        // Let's make sure we are clearing out the work area and the popup contents
        $("#viz").empty();

        // Get YASQE to tell us what type of query we are running
        var queryType = editor.getQueryType();

        // refactored so that we can clean up the on-click function and also make other query types in a more modular way
        switch(queryType) {
          case 'SELECT':
            sparqlSelect();
            break;
          case 'CONSTRUCT':
            sparqlConstruct();
            break;
          case 'ASK':
            sparqlAsk();
            break;
          case 'DESCRIBE':
            sparqlDescribe();
            break;
          case 'INSERT':
            sparqlInsert();
            break;
          default:
            popup.show("Unrecognized query type.","error");
            break;
        }
      });

// Functions to do each of the query types (SELECT, CONSTRUCT, ASK, DESCRIBE, INSERT)
  // SELECT
  function sparqlSelect() {
    $.post("sparqlSelect", { database: $("#DB_label").html(),'query': editor.getValue() }).done(function(data, textStatus, xhr) {
      // Enable the query button
      $("#querySubmitButton").removeAttr('disabled');

      // If the query worked, store it
      storeQueryHistory(query);

      // if the previous query was a CONSTRUCT, then lets hide the graph metrics button
      $("#nav-trigger-graphStatistics").fadeOut(800);

      // Need to slide the query menu back
      sliders("in",$("#nav-trigger-query").attr("id"));

      var columns = [];
      var fields = [];
      var comboboxFields = [];


      // Hide the graph search panel
      $("#graphSearch").fadeOut(1400);

      // Show the results and visualization button/tab
      $("#nav-trigger-results").fadeIn(1400);
      $("#nav-trigger-visualization").fadeIn(1400);


      $.each(data.results.head.vars, function(index, value) {
        columns.push({'field': value, 'title': value});
        var to = {};
        to[value] = {type: "string"};
        fields.push(to);

        // Let's also populate the two Comboboxes for the Visualization while we are at it
        comboboxFields.push({'text': value, 'value': value});
      });

      // Now, set the two combobox datasources for visualizations
      var categoriesDS = new kendo.data.DataSource({
          data: comboboxFields
      });
      vizCategoryAxis.setDataSource(categoriesDS);

      var valuesDS = new kendo.data.DataSource({
          data: comboboxFields
      });
      vizValueAxis.setDataSource(valuesDS);

      var dataBindings = [];
      $.each(data.results.results.bindings, function(index1, value) {
        var tempobj = {};
        $.each(value, function(k1,v1) {
          tempobj[k1] = v1.value;
        });
        tempobj.id=index1;
        dataBindings.push(tempobj);
      });

      var configuration = {
        dataSource: {
          data: dataBindings,
          pageSize: 25
        },
        height: 400,
        scrollable: true,
        sortable: true,
        filterable: true,
        reorderable: true,
        resizable: true,
        toolbar: ["excel"],
        excel: {
          allPages: true,
          filterable: true,
          proxyURL: "/saveExcel"
        },
        pageable: {
          input: true,
          numeric: false,
          pageSizes: true
        },
        'columns': columns,
        dataBound: function(e) {
          $(e.sender.element).find('td').each(function() {
            var temp = $(this).html();
            if (isUrl(temp)) {
              $(this).html('<a href="' + temp + '" target="_blank">' + temp + '</a>');
            }
          });
        }
      };

      // Create the popup window
      var gridWindow = $("#resultsPopup").kendoWindow({
        width: "70%",
        title: "Query Results",
        actions: [
            "Minimize",
            "Maximize",
            "Close"
        ]
      }).data('kendoWindow');

      // Center and show the popup window
      gridWindow.center().open();

      // Create/update/refresh the grid
      resultsGrid.setOptions(configuration);
      resultsGrid.dataSource.page(1);

      $("#nav-trigger-results").on('click',function() {
        // Center and show the popup window
        gridWindow.center().open();
      });
    }).fail(function(xhr) {
      // If we are timed-out
      if (xhr.status === 401) {
        // First, clear the host, database, and status text
        $("#host_label").html('');
        $("#DB_label").html('');
        $("#status_label").html('');

        // Next, disable the query button
        $("#querySubmitButton").attr('disabled',true);

        // Change "login" tab text color to red so we know we are no longer logged in
        var styles = { 'color': "#FFCCD2" };
        $("#nav-trigger-login").css(styles);

        popup.show("Session for " + host + " has timed out, please log back in.","error");
      }
      else {
        // Enable the query button
        $("#querySubmitButton").removeAttr('disabled');
        popup.show("Error, no results (" + xhr.status + " " + xhr.statusText + ")","error");
      }
    });
  }

  // Function to connect to the query queue websocket
  function queueWebsocketConnect() {
    var qws = new WebSocket('wss://endeavour:3000/queue');

    // Let's disconnect our Websocket connections when we leave the app
    $(window).on('unload', function() {
      console.log('Websocket connection closed');
      qws.close();
    });

    // Status websocket onopen
    qws.onopen = function () {
      console.log('Websocket connection opened');
      popup.show("Websocket connection opened","success");
    };

    qws.onclose = function (event) {
      console.log('Websocket connection closed');
      popup.show("Websocket connection closed","info");
    };

    qws.onmessage = function (msg) {
      var res = JSON.parse(msg.data);
      var tableRows = '<thead><tr><td>Query Position</td><td>Query ID</td><td>Kill/Cancel Query</td></tr></thead><tbody>';
      if (res.executing != null && res.entry.length > 0) {
        $("#queryQueue").empty();
        tableRows += '<tr><td>1</td><td>' + res.executing.id + '</td><td><input type="button" class="k-button" value="Kill"></td></tr>';

        $.each(res.entry, function(index,object) {
          tableRows += '<tr><td>' + (object.pos + 1) + '</td><td>' + object.query.id + '</td><td><input type="button" class="k-button" value="Cancel"></td></tr>';
        });
        tableRows += '</tbody>';
        $("#queryQueue").html(tableRows);
      }
      else if (res.executing != null) {
        $("#queryQueue").empty();
        tableRows += '<tr><td>1</td><td>' + res.executing.id + '</td><td><input type="button" class="k-button" value="Kill"></td></tr>';
        tableRows += '</tbody>';
        $("#queryQueue").html(tableRows);
      }
      else {
        console.log(res);
        $("#queryQueue").empty();
      }
    };
  }

  // Function to connect to the stats websocket
  function websocketConnect () {
    // Set up websocket connection for system stats
    var ws = new WebSocket('wss://endeavour:3000/stats');

    // Let's disconnect our Websocket connections when we leave the app
    $(window).on('unload', function() {
      console.log('Websocket connection closed');
      ws.close();
    });

    // Status websocket onopen
    ws.onopen = function () {
      console.log('Websocket connection opened');
      popup.show("Websocket connection opened","success");
    };

    // Status websocket onclose
    ws.onclose = function (event) {
      // Disable the query button
        $("#querySubmitButton").attr('disabled',true);

      // Change "login" tab text color to red so we know we are no longer logged in
      var styles = { 'color': "#FFCCD2" };
      $("#nav-trigger-login").css(styles);

      // Clear the host, database, and status text
      $("#host_label").html('');
      $("#DB_label").html('');
      $("#status_label").html('');
      console.log('Websocket connection closed');
      popup.show("Websocket connection closed","error");

      $("#websocketReconnectButtonYes").on('click', function() {
        websocketConnect();
        queueWebsocketConnect();
        websocketReconnect.close();
      });

      $("#websocketReconnectButtonNo").on('click', function() {
        websocketReconnect.close();
      });

      websocketReconnect.center().open();
    };

    // When updates are received, push them out to update the details
    var logoutCount = 0;
    ws.onmessage = function (msg) {
      if (msg.data === 'loggedOut') {
        // Ensure we only emit this one time instead of a stream of them
        if (logoutCount == 0) {
          // Disable the query button
          $("#querySubmitButton").attr('disabled',true);

          // Change "login" tab text color to red so we know we are no longer logged in
          var styles = { 'color': "#FFCCD2" };
          $("#nav-trigger-login").css(styles);

          // Clear the host, database, and status text
          $("#host_label").html('');
          $("#DB_label").html('');
          $("#status_label").html('');

          console.log("Session for " + $("#host_label").html() + " has timed out, please log back in.");
          popup.show("Session for " + $("#host_label").html() + " has timed out, please log back in.","error");
        }
        logoutCount = 1;
      }
      else {
        logoutCount = 0;
        var res = JSON.parse(msg.data);
        var host = $("#host_label").html();
        var pdatabase = $("#DB_label").html();
        var pstatus = $("#status_label").html();

        // Disable the query button unless the database is "CONNECTED"
        if ($("#status_label").html() !== res.current.databaseStatus) {
          if (res.current.databaseStatus !== "CONNECTED") {
            $("#querySubmitButton").attr('disabled',true);
          }
          else {
            $("#querySubmitButton").removeAttr('disabled');
          }

          if (res.current.databaseStatus == 'CONNECTED' || res.current.databaseStatus == 'STOPPED') {
            $("#startDB").removeAttr('disabled');
          }
          else {
            $("#startDB").attr('disabled',true);
          }
        }

        // Maybe a more intelligent way to do this, but need to make sure that if the cookie is still valid, then populate the database login stuff
        if ($("#dbConfigHost").val() == "" && $("#dbConfigUser").val() == "") {
          $("#dbConfigHost").val(res.host);
          $("#dbConfigUser").val(res.user);

          // Change "login" tab text color to green so we know we are logged in
          var styles = { 'color': "#C5E6CC" };
          $("#nav-trigger-login").css(styles);

          var databasesDS = new kendo.data.DataSource({
            data: res.databases.database
          });
          databasePicker.setDataSource(databasesDS);
        }

        // Update the labels when values change
        if (res.host != $("#host_label").html()) {
          $("#host_label").html(res.host);
          popup.show("Host changed to " + res.host,"info");
        }
        if (pdatabase != res.current.name) {
          $("#DB_label").html(res.current.name);
          popup.show("Database changed to " + res.current.name ,"info");
        }
        if (pstatus != res.current.databaseStatus) {
          $("#status_label").html(res.current.databaseStatus);
        }

        // Update the sparklines
        cpulog.options.series[0].data = res.system.cpu;
        cpulog.refresh();
        memlog.options.series[0].data = res.system.mem;
        memlog.refresh();
      }
    };

    // Open the websocket connection to listen for changes to the query list
    var queryWS = new WebSocket('wss://endeavour:3000/queryList');

    queryWS.onmessage = function(msg) {
      var res = JSON.parse(msg.data);
      var queriesDS = new kendo.data.DataSource({
          data: res
      });
      cannedQuery.setDataSource(queriesDS);
    };
  }

嗯,我想当一个人沿着一条路走一段时间后,就会认为它的方向是正确的。经过进一步的摸索,我发现了这个问题,它与我的后端网络框架的 blocking/non-blocking 性质有关。事实证明,在 Mojolicious(Perl 网络框架)中,http 调用可以是同步的也可以是异步的,具体取决于调用的编写方式。

my $tx = $ua->get('http://foo.bar?query=getSomeFoo');
if($tx->success) {
    $self->render($tx->res->content);
}
else {
    $self->rendered($tx->res->code);
}

这是一个 blocking/synchronous 请求。在 GET 完成之前什么都不会发生。另一方面,如果像这样写请求,它是一个异步请求:

$ua->get('http://foo.bar?query=getSomeFoo' => sub {
    my ($ua,$tx) = @_;
    if($tx->success) {
        $self->render($tx->res->content);
    }
    else {
        $self->rendered($tx->res->code);
    }
});

所以,如果其他人遇到过这个问题...这里是答案。如果我是这个星球上唯一犯下这种错误的白痴......那么我想我已经把我的耻辱放在那里让所有人都笑了。

最重要的是,这在 Mojolicious 文档中有详细记录......但我一直以一种方式做这件事太久了,以至于我完全忘记了它。

干杯!