aboutsummaryrefslogtreecommitdiff
path: root/files/zh-cn/web/api/server-sent_events/using_server-sent_events/index.html
blob: f9604053af49d66c378c2a80ad877eef450a6bd3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
---
title: 使用服务器发送事件
slug: Web/API/Server-sent_events/Using_server-sent_events
tags:
  - Advanced
  - DOM
  - Guide
  - SSE
  - Server Sent Events
  - messaging
  - 服务器发送事件
  - 通信
translation_of: Web/API/Server-sent_events/Using_server-sent_events
original_slug: Server-sent_events/Using_server-sent_events
---
<p>{{DefaultAPISidebar("Server Sent Events")}}</p>

<p class="summary">开发一个使用服务器发送的事件的Web应用程序是很容易的。你需要在服务器上的一些代码将事件流传输到Web应用程序,但Web应用程序端的事情几乎完全相同,处理任何其他类型的事件。</p>

<p>在Web应用程序中使用服务器发送事件很简单.在服务器端,只需要按照一定的格式返回事件流,在客户端中,只需要为一些事件类型绑定监听函数,和处理其他普通的事件没多大区别.</p>

<h2 id="从服务器接受事件">从服务器接受事件</h2>

<p>服务器发送事件API也就是<a href="/zh-CN/Server-sent_events/EventSource" title="zh-CN/Server-sent events/EventSource"><code>EventSource</code></a>接口,在你创建一个新的<a href="/zh-CN/Server-sent_events/EventSource" title="zh-CN/Server-sent events/EventSource"><code>EventSource</code></a>对象的同时,你可以指定一个接受事件的URI.例如:</p>

<pre class="notranslate">const evtSource = new EventSource("ssedemo.php");
</pre>

<div class="note"><strong>注:</strong>从Firefox 11开始,<code>EventSource</code>开始支持<a href="/zh-CN/HTTP_access_control" title="zh-CN/HTTP_access_control">CORS</a>.虽然该特性目前并不是标准,但很快会成为标准.</div>

<p>如果发送事件的脚本不同源,应该创建一个新的包含URL和options参数的<code>EventSource</code>对象。例如,假设客户端脚本在example.com上:</p>

<pre class="notranslate">const evtSource = new EventSource("//api.example.com/ssedemo.php", { withCredentials: true } );</pre>

<p>一旦你成功初始化了一个事件源,就可以对 {{event("message")}} 事件添加一个处理函数开始监听从服务器发出的消息了:</p>

<pre class="brush: js notranslate">evtSource.onmessage = function(event) {
  const newElement = document.createElement("li");
  const eventList = document.getElementById("list");

  newElement.innerHTML = "message: " + event.data;
  eventList.appendChild(newElement);
}</pre>

<p>上面的代码监听了那些从服务器发送来的所有没有指定事件类型的消息(没有<code>event</code>字段的消息),然后把消息内容显示在页面文档中.</p>

<p>你也可以使用<code>addEventListener()</code>方法来监听其他类型的事件:</p>

<pre class="brush: js notranslate">evtSource.addEventListener("ping", function(event) {
  const newElement = document.createElement("li");
  const time = JSON.parse(event.data).time;
  newElement.innerHTML = "ping at " + time;
  eventList.appendChild(newElement);
});</pre>

<p>这段代码也类似,只是只有在服务器发送的消息中包含一个值为"ping"的<code>event</code>字段的时候才会触发对应的处理函数,也就是将<code>data</code>字段的字段值解析为JSON数据,然后在页面上显示出所需要的内容.</p>

<div class="blockIndicator warning">
<p><strong>不通过HTTP / 2使用时</strong>,SSE(server-sent events)会受到最大连接数的限制,这在打开各种选项卡时特别麻烦,因为该限制是针对每个浏览器的,并且被设置为一个非常低的数字(6)。该问题在 <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=275955" rel="noreferrer">Chrome</a> 和 <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=906896" rel="noreferrer">Firefox</a>中被标记为“无法解决”。此限制是针对每个浏览器+域的,因此这意味着您可以跨所有选项卡打开6个SSE连接到www.example1.com,并打开6个SSE连接到www.example2.com。 (来自 <a href="https://stackoverflow.com/a/5326159/1905229">Stackoverflow</a>)。使用HTTP / 2时,HTTP同一时间内的最大连接数由服务器和客户端之间协商(默认为100)。</p>
</div>

<h2 id="服务器端如何发送事件流">服务器端如何发送事件流</h2>

<p>服务器端发送的响应内容应该使用值为<code>text/event-stream</code>的MIME类型.每个通知以文本块形式发送,并以一对换行符结尾。有关事件流的格式的详细信息,请参见<a href="#事件流格式">事件流格式</a></p>

<p>演示的{{Glossary("PHP")}}代码如下:</p>

<pre class="brush: php notranslate">date_default_timezone_set("America/New_York");
header("Cache-Control: no-cache");
header("Content-Type: text/event-stream");

$counter = rand(1, 10);
while (true) {
  // Every second, send a "ping" event.

  echo "event: ping\n";
  $curDate = date(DATE_ISO8601);
  echo 'data: {"time": "' . $curDate . '"}';
  echo "\n\n";

  // Send a simple message at random intervals.

  $counter--;

  if (!$counter) {
    echo 'data: This is a message at time ' . $curDate . "\n\n";
    $counter = rand(1, 10);
  }

  ob_end_flush();
  flush();
  sleep(1);
}</pre>

<p>上面的代码会让服务器每隔一秒生成一个事件流并返回,其中每条消息的事件类型为"ping",数据字段都使用了JSON格式,数组字段中包含了每个事件流生成时的 ISO 8601 时间戳.而且会随机返回一些无事件类型的消息.</p>

<div class="blockIndicator note">
<p><strong></strong>: 您可以在github上找到以上代码的完整示例—参见<a href="https://github.com/mdn/dom-examples/tree/master/server-sent-events">Simple SSE demo using PHP.</a></p>
</div>

<h2 id="错误处理">错误处理</h2>

<p>当发生错误(例如请求超时或与<a href="/zh-CN/docs/Web/HTTP/Access_control_CORS" title="/en-US/docs/HTTP/Access_control_CORS">HTTP访问控制(CORS)</a>有关的问题), 会生成一个错误事件. 您可以通过在<code>EventSource</code>对象上使用<code>onerror</code>回调来对此采取措施:</p>

<pre class="notranslate">evtSource.onerror = function(err) {
  console.error("EventSource failed:", err);
};</pre>

<h2 id="关闭事件流">关闭事件流</h2>

<p>默认情况下,如果客户端和服务器之间的连接关闭,则连接将重新启动。可以使用<code>.close()</code>方法终止连接。</p>

<pre class="notranslate">evtSource.close();</pre>

<h2 id="事件流格式">事件流格式</h2>

<p>事件流仅仅是一个简单的文本数据流,文本应该使用 <a href="/zh-CN/docs/Glossary/UTF-8">UTF-8</a> 格式的编码.每条消息后面都由一个空行作为分隔符.以冒号开头的行为注释行,会被忽略.</p>

<div class="note"><strong>注:</strong>注释行可以用来防止连接超时,服务器可以定期发送一条消息注释行,以保持连接不断.</div>

<p>每条消息是由多个字段组成的,每个字段由字段名,一个冒号,以及字段值组成.</p>

<h3 id="字段">字段</h3>

<p>规范中规定了下面这些字段:</p>

<dl>
 <dt><code>event</code></dt>
 <dd>事件类型.如果指定了该字段,则在客户端接收到该条消息时,会在当前的<code>EventSource</code>对象上触发一个事件,事件类型就是该字段的字段值,你可以使用<code>addEventListener()方法在当前EventSource</code>对象上监听任意类型的命名事件,如果该条消息没有<code>event</code>字段,则会触发<code>onmessage属性上的事件处理函数</code>.</dd>
 <dt><code>data</code></dt>
 <dd>消息的数据字段.如果该条消息包含多个<code>data</code>字段,则客户端会用换行符把它们连接成一个字符串来作为字段值.</dd>
 <dt><code>id</code></dt>
 <dd>事件ID,会成为当前<code>EventSource</code>对象的内部属性"最后一个事件ID"的属性值.</dd>
 <dt><code>retry</code></dt>
 <dd><span class="short_text" id="result_box" lang="zh-CN"><span>一个</span><span>整数值</span><span>,</span><span>指定了</span><span>重新连接的时间(</span><span>单位为毫秒),</span></span>如果该字段值不是整数,则会被忽略.</dd>
</dl>

<p>除了上面规定的字段名,其他所有的字段名都会被忽略.</p>

<div class="note"><strong>注:</strong> 如果一行文本中不包含冒号,则整行文本会被解析成为字段名,其字段值为空.</div>

<h3 id="例子">例子</h3>

<h4 id="未命名事件">未命名事件</h4>

<p>下面的例子中发送了三条消息,第一条仅仅是个注释,因为它以冒号开头.第二条消息只包含了一个<code>data</code>字段,值为"some text".第三条消息包含的两个<code>data</code>字段会被解析成为一个字段,值为"another message\nwith two lines".其中每两条消息之间是以一个空行为分割符的.</p>

<pre class="notranslate">: this is a test stream

data: some text

data: another message
data: with two lines
</pre>

<h4 id="命名事件">命名事件</h4>

<p>下面的事件流中包含了一些命名事件.每个事件的类型都是由<code>event</code>字段指定的,另外每个<code>data</code>字段的值可以使用JSON格式,当然也可以不是.</p>

<pre class="notranslate">event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}

event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}
</pre>

<h4 id="混合两种事件">混合两种事件</h4>

<p>你可以在一个事件流中同时使用命名事件和未命名事件.</p>

<pre class="notranslate">event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

data: Here's a system message of some kind that will get used
data: to accomplish some task.

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}
</pre>

<h2 id="浏览器兼容性">浏览器兼容性</h2>



<p>{{Compat("api.EventSource")}}</p>