目录

🥃 JavaScript 长轮询 WebSocket

长轮询是 与服务器保持持久连接 的最简单的方式,它不使用任何特定的协议。(例如 WebSocket 或者 Server Sent Event)比较容易实现,适用较多场景。

# 常规轮询

定期轮询是从服务器获取新信息的最简单的方式(定期向服务器发送请求)。作为响应,服务器首先通知自己,客户端处于在线状态,然后发送目前为止的消息包。

使用定期轮询的缺点:

  • 消息传递存在延迟(两个请求之间);
  • 即使没有消息,服务器也会每隔 10 秒被请求轰炸一次,即使用户切换到其他地方或者处于休眠状态,也是如此。就性能而言,这是一个很大的负担。

对于小型服务或许可行。

# 长轮询

是轮询服务器的一种更好的方式,容易实现,可以无延迟传递消息。长轮询流程为:

  • 请求发送到服务器;
  • 服务器在有消息时之前不会关闭连接
  • 当消息出现时,服务器将对其做出响应;
  • 浏览器立即发送新的请求。

对于此方法,浏览器发出一个请求并与服务器之间建立起一个 挂起的连接 的情况是标准的。仅在有消息被传递时,才会重新建立连接。

image-20220601161138146

🌰 例子 / 实现长轮询客户端的函数:

async function subscribe() {
  let response = await fetch("/subsribe")
  
  if (response.status == 502) {
    // 502 超时错误
    // 超时导致连接挂起时间过长,远程服务器或者代理会关闭该连接
    // 重新连接
    await subsribe()
  } else if (response.status != 200) {
    // 发生错误时
    showMessage(response.status) 
    
    // 一秒后重新连接
    await new Promise(resolve => setTimeout(resolve, 1000));
    await subsribe();
  } else {
    // 获取消息
    let message = await response.text();
  	// 显示消息
    showMessage(message)
    // 调用 subsribe() 获取下一条消息
    await subsribe();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

subscribe 函数发起了一个 fetch ,然后等待响应,处理它,并再次调用自身。

提示

对于服务器,应该可以处理许多挂起的连接。某些服务器架构是每个连接对应一个进程,导致进程数和连接数一样多,而每个进程都会消耗相当多的内存。因此,过多的连接会消耗掉全部内存。

长轮询的使用场景:

  • 在消息很少的情况下,长轮询很有效。
  • 如果消息比较频繁,那么上面描绘的请求 - 接收消息的图表就会变成锯状状。每个消息都是一个单独的请求,并带有 header,身份验证等增加了开销。此时,应该选择另一种方法(WebSocket 或者 Server Sent Events)

# WebSocket

是一个提供浏览器和服务器之间 建立持久连接交换数据 的方法协议。数据可以作为「数据包」在两个方向上传递,而不会断开连接和其他 HTTP 请求。

对于需要连续数据交换的服务,例如网络游戏,实时交易系统等,WebSocket 尤其有用。

  • WebSocket 没有跨源限制。
  • 浏览器对 WebSocket 支持很好。
  • 可以发送 / 接收字符串和二进制数据。

创建 WebSocket 连接,需要在 URL 使用特殊的协议 ws

let socket = new WebSocket("ws://javascript.info");
1

加密的 wss:// 协议,类似于 HTTPS。

wss:// 协议不仅是被加密的,而且更可靠。因为 ws:// 中数据不是加密的,对于任何中间人来说其数据都是可见的。并且,旧的代理服务器不了解 WebSocket,可能会因为奇怪的 header 而中止连接; wss:// 是基于 TLS 的 WebSocket,类似于 HTTPS 是基于 TLS 的 HTTP),传输安全层在发送方对数据进行了加密,在接收方进行解密。因此,数据包是通过代理加密传输的。它们看不到传输的里面的内容,且会让这些数据通过。

WebSocket 被建立后,就可以监听 socket 的事件:

  • open :连接已建立;
  • message :接收到数据;
  • error :连接出错;
  • close :连接已关闭;

使用 WebSocket 发送数据使用 socket.send(data) 方法。

🌰 例子 / 使用 WebSocket 发送数据:

let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");

socket.onopen = function(e) {
  alert("[open] Connection established");
  alert("Sending to server");
  socket.send("My name is John"); // 发送消息
};

socket.onmessage = function(event) {
  alert(`[message] Data received from server: ${event.data}`);
};

socket.onclose = function(event) {
  if (event.wasClean) {
    alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
  } else {
    // 例如服务器进程被杀死或网络中断
    // 在这种情况下,event.code 通常为 1006
    alert('[close] Connection died');
  }
};

socket.onerror = function(error) {
  alert(`[error] ${error.message}`);
};
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

# 建立连接

new WebSocket(url) 创建时,建立起了连接。

连接期间,浏览器使用响应头 header 发送请求询问服务器是否支持 WebSocket, 如果支持才会以 WebSocket 协议继续进行。

🌰 例子 / new WebSocket("wss://javascript.info/chat") 请求头内容:

GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
1
2
3
4
5
6
7
  • Origin :客户端页面的源。WebSocket 对象是 原生支持跨源 的。没有特殊的 header 或其他限制。~~ 旧的服务器无法处理 WebSocket,因此不存在兼容性问题。~~ 但是 Origin 很重要,它允许服务器决定是否使用 WebSocket 与该网站通信;
  • Connection: Upgrade :表示客户端想要 更改协议
  • Upgrade: websocket :更改请求的协议为 websocket
  • Sec-WebSocket-Key :浏览器随机生成的安全密钥。
  • Sec-WebSocket-Version :WebSocket 协议版本,当前为 13。`

注意不可以模拟 WebSocket 握手,例如使用 XMLHttpRequestfetch 设置这些请求头的内容。

如果服务器同意切换为 WebSocket 协议,则响应头内容为(响应码为 101 ):

101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
1
2
3
4

同时,这里的 Sec-WebSocket-AcceptSec-WebSocket-Key浏览器使用它保证响应与请求相对应

然后就可以使用 WebSocket 协议传输数据。并且之后可以看到,它的结构不是 HTTP。

WebSocket 拓展和子协议:

点击查看

WebSocket 的其他 header: Sec-WebSocket-ExtensionsSec-WebSocket-Protocol ,它们描述了扩展和子协议。

  • Sec-WebSocket-Extensions: deflate-frame :表示浏览器 支持数据压缩。这个拓展与数据传输有关,拓展了 WebSocket 的协议的功能。

    Sec-WebSocket-Extensions header 由浏览器自动发送,其中包含其支持的所有扩展的列表。

  • Sec-WebSocket-Protocol: soap, wamp :表示不仅要传输数据,还要传输 SOAP / WAMP 协议中的数据。

    这个可选的 header 是使用 new WebSocket 的第二个参数设置的。它是子协议数组,例如,如果想使用 SOAP 或 WAMP:

    let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);
    
    1

服务器应该使用同意使用的协议和扩展的列表进行响应

例如:

  • 请求头 header:

    GET /chat
    Host: javascript.info
    Upgrade: websocket
    Connection: Upgrade
    Origin: https://javascript.info
    Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
    Sec-WebSocket-Version: 13
    Sec-WebSocket-Extensions: deflate-frame
    Sec-WebSocket-Protocol: soap, wamp
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 响应头 header:

    101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
    Sec-WebSocket-Extensions: deflate-frame
    Sec-WebSocket-Protocol: soap
    
    1
    2
    3
    4
    5
    6

# 数据传输

WebSocket 通信由 frames (即数据片段)组成,可以从任何一方发送,并且有以下几种类型:

  • text frames文本数据
  • binary data frames二进制数据
  • ping/pong frames :用于检查服务器发送的链接。浏览器会自动响应它们。
  • conection close frames

在浏览器里,仅仅使用 文本数据或者二进制数据

使用 WebSocket 的 .send 方法发送文本或者二进制数据:

  • socket.send(body) 调用允许 body 是字符串或二进制格式,包括 BlobArrayBuffer 等。不需要额外的设置:直接发送它们就可以了。

当接收到数据,文本总是以 字符串 形式呈现。对于二进制数据,在 BlobArrayBuffer 格式之间选择。(由 socket.binaryType 属性设置的,默认为 "blob" ,因此二进制数据通常以 Blob 对象呈现。)

Blob 高级二进制对象,可以直接与 <a><img> 及其他标签集成在一起。所以为默认的格式。

要访问单个数据字节,可以修改为 arrayBuffer

socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
  // event.data 可以是文本(如果是文本),也可以是 arraybuffer(如果是二进制数据)
};
1
2
3
4

# 限速

如果应用程序正在生成大量的数据,但是用户的网速很慢。

此时,可以反复调用 socket.send(data) 。但是数据将会被缓存(缓冲存储)在内存中,并且只能在网速允许的情况下尽快将数据发送出去。

socket.bufferedAmount 属性储存了 目前已缓冲的字节数,等待通过网络发送。

🌰 例子 / 使用该属性查看 socket 是否真的可以用于传输:

setInterval(()=> {
  if(socket.bufferedAmount == 0) {
    socket.send(moreData());
  }
}, 100)
1
2
3
4
5

每 100 ms 检查一次 socket 。仅当所有现有的数据都已被发送出去时(即缓存中没有数据),再发送更多数据。

# 关闭连接

通常,当一方想要关闭连接时(浏览器和服务器都具有相同的权限),它们会发送一个带有 数字码 和文本形式的原因的 connection close frame

关闭连接的方法:

socket.close([code], [reason])
1
  • [code]可选的 特殊的 WebSocket 关闭码。
  • [reason]可选的 描述关闭原因的字符串。

当另一方通过 close 事件处理器获取了 关闭码和关闭原因 时。例如:

// 关闭方:
socket.close(1000, "Work complete");

// 另一方
socket.onclose = event => {
  // event.code === 1000
  // event.reason === "Work complete"
  // event.wasClean === true (clean close)
};
1
2
3
4
5
6
7
8
9

可以通过 event 获取这些属性。

通常的 WebSocket 关闭码:

点击查看
  • 1000 :默认,正常关闭(如果没有指明 code 时使用它),
  • 1006 :没有办法手动设定这个数字码,表示连接丢失(没有 close frame)。

其他数字码,例如:

  • 1001 :一方正在离开,例如服务器正在关闭,或者浏览器离开了该页面,
  • 1009 :消息太大,无法处理,
  • 1011 : 服务器上发生意外错误,
  • ……。

RFC6455, §7.4.1 (opens new window)

注意,小于 1000 的码都是被保留的,如果我们尝试设置这样的码,将会出现错误。

# 连接状态

要获取连接状态,可以通过带有值的 socket.readyState 属性:

  • 0 —— “CONNECTING”:连接还未建立,
  • 1 —— “OPEN”:通信中,
  • 2 —— “CLOSING”:连接关闭中,
  • 3 —— “CLOSED”:连接已关闭。

# Server Sent Events

Server-Sent Event 规范描述了一个内建的类 EventSource 。能保持服务器的连接,并且允许从中接收事件。(与 WebSocket 连接类似,是持久的连接)

WebSocket 与 EventSource 的区别:

WebSocket EventSource
双向:客户端和服务端都能交换消息 单向:仅服务端能发送消息
二进制和文本数据 仅文本数据
WebSocket 协议 常规 HTTP 协议

相比 WebSocket, EventSource 更加简单。

当需要从服务器接收一个数据流,可能是 聊天消息或者其他信息。 EventSource 支持自动重新连接,而在 WebSocket 需要手动实现。并且它支持常规的 HTTP 协议。

# 获取消息

使用 new EventSource(url) 。浏览器将会连接到 url 并保持连接打开,等待事件。

服务器响应的状态码应该为 200,header 为 Content-Type: text/event-stream ,然后保持此连接并以一种特殊的格式写入消息。例如:

data: Message 1

data: Message 2

data: Message 3
data: of two lines
1
2
3
4
5
6
  • data :消息文本, : 后的空格可选;
  • 消息以两个换行符 \n\n 分隔。
  • 要发送一个换行符 \n 的消息,在要换行的位置立即再发送一个 data: (上面的第三条消息)。

实际运用中,复杂的消息一般使用 JSON 编码后发送。换行符在其中编码为 \n ,因此不需要多行 data: 消息。

🌰 例子:

data: {"user":"John","message":"First line\n Second line"}
1

对于这样类型的信息,都会生成一个 message 事件:

let eventSource = new EventSource("/events/subsribe");

eventSource.onmessage = function(event) {
  console.log("new Message", event.data) 
  // 对于上面的数据流将打印三次
}
// 或 eventSource.addEventListener('message', ...)
1
2
3
4
5
6
7

# 跨源请求

EventSource 支持跨源请求,就像 fetch 和任何其他网络方法。

所以 EventSource 可以使用任何 URL:

let source = new EventSource("https://another-site.com/events");
1

远程服务器将会获取到 Origin header,并且必须以 Access-Control-Allow-Origin 响应来处理。要传递凭证(credentials),应该设置附加选项 withCredentials ,如下:

let source = new EventSource("https://another-site.com/events", {
  withCredentials: true
});
1
2
3

# 重新连接

new EventSource 连接到服务器,如果连接断开,则重新连接。每次重新连接之间有一点小的延迟,默认为几秒钟。

服务器可以使用 retry:设置需要的延迟响应时间(以毫秒为单位)。

retry: 15000
data: Hello, I set the reconnection delay to 15 seconds
1
2
  • retry: 既可以与某些数据一起出现,也可以作为独立的消息出现。

在重新连接之前,浏览器需要等待设定的延迟响应时间。例如,如果浏览器知道(从操作系统)此时没有网络连接,它会等到连接出现,然后重试。

  • 如果服务器想要浏览器 停止重新连接,那么应该使用 HTTP 状态码 204 进行响应;

  • 如果浏览器想要 关闭连接,则应该调用 eventSource.close()

    let eventSource new EventSource(...);
    
    eventSource.close();
    
    1
    2
    3
  • 如果响应具有不正确的 Content-Type 或者 HTTP 状态码不是 301,307,200 和 204,则不会进行重新连接。在这种情况下,将会发出 error 错误事件,并且浏览器不会重新连接。

当连接被关闭时,则无法「重新打开」该连接。如果要重新连接,就要重新创建 new EventSource()

# 连接 id

当一个连接由于网络问题而中断时,客户端和服务器都无法确定哪些消息已经收到哪些没有收到。为了正确地恢复连接,每条消息都应该有一个 id 字段,如下:

data: Message 1
id: 1

data: Message 2
id: 2

data: Message 3
data: of two lines
id: 3
1
2
3
4
5
6
7
8
9

当收到具有 id 的消息时,浏览器会:

  • 将属性 eventSource.lastEventId 设置为其值
  • 重新连接后,发送带有 id 的 header Last-Event-ID ,以便服务器可以重新发送后面的消息。

注意: id 被服务器附加到 data 消息后,以确保在收到消息后 lastEventId 会被更新。

# 连接状态

EventSource 对象有 readyState 连接状态属性,该属性具有:

  • EventSource.CONNECTING = 0; :连接中或者重新连接中;
  • EventSource.OPEN = 1; :已连接;
  • EventSource.CLOSED = 2; :连接已关闭;

对象创建完成或者连接断开后,它始终是 EventSource.CONNECTING (等于 0 )。

可以通过查询该属性获得 EventSource 的状态。

# 连接事件

EventSource 的对象会默认生成三个事件:

  • message收到消息,可以用 event.data 访问。
  • open连接已打开
  • error无法建立连接,例如,服务器返回 HTTP 500 状态码。

服务器可以在事件开始时使用 event: ... 指定另一种类型事件。

🌰 例子:

event: join
data: Bob

data: Hello

event: leave
data: Bob
1
2
3
4
5
6
7

处理自定义事件 join ,必须使用 addEventListener

eventSource.addEventListener('join', event => {
  alert(`Joined ${event.data}`);
});

eventSource.addEventListener('message', event => {
  alert(`Said: ${event.data}`);
});

eventSource.addEventListener('leave', event => {
  alert(`Left ${event.data}`);
});
1
2
3
4
5
6
7
8
9
10
11
📢 上次更新: 2022/09/02, 10:18:16