Server-initiated requests

3.5k views Asked by At

I know that HTTP is a request-response protocol. My problem, in short, is that a client makes a request to the server to start a long running-process, and I want to inform the client of the progress with a simple JSON message containing progress info.

In HTTP/1.1 I know that I could use a WebSocket or server-sent events (SSE) or long polling.

Now I know that HTTP/2 does not support WebSocket yet.

My question is, what is the optimal way to handle such things over HTTP/2?

Are there any new things that I am not aware of to handle server-initiated requests in HTTP/2?

I am using the Go language, if that matters.

3

There are 3 answers

2
Endophage On BEST ANSWER

Before websockets we had polling. This literally means having the client periodically (every few seconds, or whatever time period makes sense for your application), make a request to the server to find out the status of the job.

An optimization many people use is "long" polling. This involves having the server accept the request, and internally to the server, check for changes, and sleep while there are none, until either a specific timeout is reached or the desired event occurs, which is then messaged back to the client.

If a timeout is reached, the connection is closed and the client needs to make another request. The server code would look something like the following, assume the functions do sensible things based on their names and signatures:

import (
    "net/http"
    "time"
)

func PollingHandler(w http.ResponseWriter, r *http.Request) {
    jobID := getJobID(r)
    for finish := 60; finish > 0; finish-- { // iterate for ~1 minute
        status, err := checkStatus(jobID)
        if err != nil {
            writeError(w, err)
            return
        }
        if status != nil {
            writeStatus(w, status)
            return
        }
        time.Sleep(time.Second) // sleep 1 second
    }
    writeNil(w) // specific response telling client to request again.
}

A better way to handle the timeout would be to use the context package and create a context with a timeout. That would look something like:

import (
    "net/http"
    "time"
    "golang.org/x/net/context"
)

func PollingHandler(w http.ResponseWriter, r *http.Request) {
    jobID := getJobID(r)
    ctx := context.WithTimeout(context.Background(), time.Second * 60)
    for {
        select{
        case <-ctx.Done():
            writeNil(w)
        default: 
            status, err := checkStatus(jobID)
            if err != nil {
                writeError(w, err)
                return
            }
            if status != nil {
                writeStatus(w, status)
                return
            }
            time.Sleep(time.Second) // sleep 1 second
        }
    }

}

This second version is just going to return in a more reliable amount of time, especially in the case where checkStatus may be a slower call.

1
anneb On

You could consider using the HTML5 text/event-stream a.k.a. server side events (SSE). SSE is mentioned in the question, wouldn't that work well with http2?

General articles about SSE

(IE is currently the only browser that doesn't support SSE)

In the following article, http2 push is combined with SSE. Documents are pushed into the client cache and SSE is used to notify the client what documents can be retrieved from its cache (= server initiated requests over a single http2 connection):

Basics of SSE: on the server side, you start with:

Content-Type: text/event-stream\n\n

Then for each time you want to send an update to the client you send

data: { "name": "value", "othername": "othervalue" }\n\n

When finished, before closing the connection, you can optionally send:

retry: 60000\n\n

to instruct the browser to retry a new connection after 60000 msec

In the browser the connection is made like this:

var URL = "http://myserver/myeventstreamer"
if (!!window.EventSource) {
    source = new EventSource(URL);
} else {
    // Resort to xhr polling :(
    alert ("This browser does not support Server Sent Events\nPlease use another browser") 
}

source.addEventListener('message', function(e) {
  console.log(e.data);
}, false);

source.addEventListener('open', function(e) {
  // Connection was opened.
}, false);

source.addEventListener('error', function(e) {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
}, false);
0
Michael Laszlo On

If you want to send the JSON message as text, a server-sent event (SSE) is a good way to do it. SSE is designed to send text. All event data is encoded in UTF-8 characters. The downside is that this makes it inefficient to send binary data through SSE.

If you want to send binary data, you may be interested in the Server Push mechanism introduced by HTTP/2. Server Push allows an HTTP/2 server to send any kind of file to the client on its own initiative. It's called a Server Push "response" even though it's sent before the client asks for it. The client automatically stores a file sent via Server Push response in its cache. A subsequent request for the file is immediately fulfilled from the cache without a round trip to the server.

This is an efficient way to push binary data to a web browser. The hitch is that the browser's document object model (DOM) is not notified when a Server Push response arrives. The browser only discovers that the data is in its cache when it makes a request for it. We can work around this problem in the following manner. Immediately after sending binary data with Server Push, the server sends an SSE to the client to notify it that data has been pushed to its cache. Now the client can retrieve the data from its cache by requesting it.

But as long as you're using SSE, why not send the file through SSE in the first place? Because if you're dealing with binary data, you can benefit from the smaller file size that Server Push allows you to achieve. For a short JSON message, it may not make sense to use Server Push. In situations where you're pushing binary data and you have to conserve bandwidth, consider sending the data through Server Push followed by SSE notification.

Unlike polling, this approach does not require periodic requests from the client. The server can send a Server Push response whenever it wants to.