服务器发起的请求

huangapple go评论82阅读模式
英文:

Server-initiated requests

问题

我知道HTTP是一种请求-响应协议。我的问题是,客户端向服务器发出请求以启动一个长时间运行的进程,并且我希望通过一个包含进度信息的简单JSON消息通知客户端。

在HTTP/1.1中,我知道可以使用WebSocket、服务器发送事件(SSE)或长轮询。

现在我知道HTTP/2还不支持WebSocket。

我的问题是,在HTTP/2上处理这种情况的最佳方式是什么?

在HTTP/2中,是否有任何我不知道的新方法来处理服务器发起的请求?

我正在使用Go语言,如果这有关系的话。

英文:

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.

答案1

得分: 9

在使用Websockets之前,我们使用的是轮询(polling)方法。这意味着客户端会定期(每几秒钟或者适合你的应用程序的时间间隔)向服务器发送请求,以了解作业的状态。

许多人使用的一种优化方法是“长轮询”(long polling)。这种方法涉及到服务器接受请求,并在服务器内部检查是否有变化,如果没有变化,则进入休眠状态,直到达到特定的超时时间或者发生所需的事件,然后将消息发送回客户端。

如果超时时间到达,连接将被关闭,客户端需要发送另一个请求。服务器端的代码可能如下所示(假设函数根据其名称和签名执行合理的操作):

import (
    "net/http"
    "time"
)

func PollingHandler(w http.ResponseWriter, r *http.Request) {
    jobID := getJobID(r)
    for finish := 60; finish > 0; finish-- { // 迭代约1分钟
        status, err := checkStatus(jobID)
        if err != nil {
            writeError(w, err)
            return
        }
        if status != nil {
            writeStatus(w, status)
            return
        }
        time.Sleep(time.Second) // 休眠1秒
    }
    writeNil(w) // 返回特定的响应,告诉客户端再次发送请求。
}

更好的处理超时的方法是使用context包,并创建一个带有超时的上下文。代码如下所示:

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) // 休眠1秒
        }
    }
}

这个第二个版本将在更可靠的时间内返回结果,特别是在checkStatus可能是一个较慢的调用的情况下。

英文:

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 (
    &quot;net/http&quot;
    &quot;time&quot;
)

func PollingHandler(w http.ResponseWriter, r *http.Request) {
    jobID := getJobID(r)
    for finish := 60; finish &gt; 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 (
    &quot;net/http&quot;
    &quot;time&quot;
    &quot;golang.org/x/net/context&quot;
)

func PollingHandler(w http.ResponseWriter, r *http.Request) {
    jobID := getJobID(r)
    ctx := context.WithTimeout(context.Background(), time.Second * 60)
    for {
        select{
        case &lt;-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.

答案2

得分: 5

你可以考虑使用HTML5的text/event-stream,也就是服务器端事件(SSE)。SSE在问题中提到过,它能和HTTP2很好地配合使用。

以下是关于SSE的一些常见文章:

(目前只有IE浏览器不支持SSE)

在下面这篇文章中,将HTTP2的推送(push)与SSE结合起来。文档被推送到客户端缓存中,而SSE用于通知客户端可以从缓存中获取哪些文档(即服务器主动发起的请求通过单个HTTP2连接):

SSE的基本原理是,在服务器端,你首先设置以下内容:

Content-Type: text/event-stream

然后,每当你想向客户端发送更新时,发送以下内容:

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

在关闭连接之前,你可以选择发送以下内容,以指示浏览器在60000毫秒后重新尝试建立新连接:

retry: 60000

在浏览器中,可以按照以下方式建立连接:

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);
英文:

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: { &quot;name&quot;: &quot;value&quot;, &quot;othername&quot;: &quot;othervalue&quot; }\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 = &quot;http://myserver/myeventstreamer&quot;
if (!!window.EventSource) {
    source = new EventSource(URL);
} else {
    // Resort to xhr polling :(
    alert (&quot;This browser does not support Server Sent Events\nPlease use another browser&quot;) 
}

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

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

source.addEventListener(&#39;error&#39;, function(e) {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
}, false);

答案3

得分: 1

如果您想将JSON消息作为文本发送,服务器推送事件(SSE)是一个很好的方式。SSE被设计用于发送文本。所有事件数据都以UTF-8字符编码。缺点是通过SSE发送二进制数据效率较低。

如果您想发送二进制数据,您可能会对HTTP/2引入的服务器推送(Server Push)机制感兴趣。服务器推送允许HTTP/2服务器主动向客户端发送任何类型的文件。尽管在客户端请求之前发送,但它被称为服务器推送的“响应”。客户端会自动将通过服务器推送响应发送的文件存储在缓存中。对该文件的后续请求将立即从缓存中满足,无需与服务器进行往返。

这是将二进制数据推送到Web浏览器的高效方式。问题在于,当服务器推送响应到达时,浏览器的文档对象模型(DOM)不会收到通知。只有当浏览器请求数据时,才会发现数据已经在缓存中。我们可以通过以下方式解决这个问题。在使用服务器推送发送二进制数据后,服务器向客户端发送SSE,以通知客户端数据已被推送到其缓存中。现在,客户端可以通过请求从缓存中检索数据。

但是,既然您正在使用SSE,为什么不一开始就通过SSE发送文件呢?因为如果您处理的是二进制数据,您可以从服务器推送允许您实现的较小文件大小中受益。对于短的JSON消息,使用服务器推送可能没有意义。在需要推送二进制数据并且需要节省带宽的情况下,考虑通过服务器推送后跟SSE通知发送数据。

与轮询不同,这种方法不需要客户端定期发送请求。服务器可以在任何时候发送服务器推送响应。

英文:

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.

huangapple
  • 本文由 发表于 2015年6月15日 08:40:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/30835999.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定