跳至主要内容

Upgrade

Upgrade 使用情境

Upgrade 是 HTTP/1.1 獨有的機制,大部分的使用情境都是 WebSocket,在連線之前要先做協議溝通(其實就是一個 HTTP round trip)

Client 端會發送以下

GET ws://localhost/ HTTP/1.1
Connection: upgrade
Upgrade: websocket

Server 端收到後,若支援 WebSocket 協議,則會回傳以下

HTTP/1.1 101 Switching Protocols
Connection: upgrade
Upgrade: websocket

以上是關於 Upgrade 快速地描述,實際上在協議溝通過程,還會有很多 WebSocket 相關的 Headers,等等會講到

WebSocket

由於這段協議溝通,在瀏覽器已經有 WebSocket 包裝好,NodeJS Server Side 也只需使用 ws 即可達成,所以我們先用成熟的 Solution 觀察這段協議溝通

  • 創建一個 index.html,包含 client 端的 WebSocket
  • 創建一個 NodeJS WebSocket Server (80 port) + 一個 NodeJS HTTP Server (5000 port) 專門 Host 上面的 index.html

index.html

<html>
<head></head>
<body>
<script>
const ws = new WebSocket("ws://localhost");
ws.addEventListener("open", function onOpen(event) {
console.log("open");
console.log(event);
});
ws.addEventListener("close", function onClose(event) {
console.log("close");
console.log(event);
});
ws.addEventListener("error", function onError(event) {
console.log("error");
console.log(event);
});
ws.addEventListener("message", function onMessage(event) {
console.log("message");
console.log(event);
appendMessageToDOM(event.data);
});
function handleSendMessage() {
const messageInput = document.getElementById("messageInput");
const chatRoomDiv = document.getElementById("chatRoom");
if (!messageInput.value) return;
ws.send(messageInput.value);
appendMessageToDOM(messageInput.value);
messageInput.value = "";
}
function appendMessageToDOM(message) {
const chatRoomDiv = document.getElementById("chatRoom");
const messageDiv = document.createElement("div");
messageDiv.innerText = message;
chatRoomDiv.appendChild(messageDiv);
}
</script>
<input id="messageInput" />
<button onclick="handleSendMessage()">送出</button>
<div id="chatRoom"></div>
</body>
</html>

index.ts

import httpServer from "../httpServer";
import WebSocket from "ws";
import { faviconListener } from "../listeners/faviconListener";
import { notFoundListener } from "../listeners/notFoundlistener";
import { readFileSync } from "fs";
import { join } from "path";

// 範例1: Simple server
const wss = new WebSocket.Server({ port: 80 });
const indexHTML = readFileSync(join(__dirname, "index.html"));
wss.on("connection", function onConnection(websokcet, req) {
websokcet.on("open", function onOpen() {
console.log("open");
});
websokcet.on("close", function onClose() {
console.log("close");
});
websokcet.on("message", function onMessage(data, isBinary) {
console.log({ data: data.toString(), isBinary });
websokcet.send("data received");
});
});
httpServer.on("request", function requestListener(req, res) {
if (req.url === "/") {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(indexHTML);
return;
}
if (req.url === "/favicon.ico") return faviconListener(req, res);
return notFoundListener(req, res);
});

使用瀏覽器打開 http://localhost:5000/ ,可以看到確實是按照 upgrade-使用情境,但多了幾個 Sec-Websocket 的 Headers http-upgrade-to-websocket

隨意嘗試傳送一些資料,也確實有成功來回! websocket-send-data

Sec-WebSocket-Version

這是一個 Request & Response Header

As Request Header 的情境

  • 格式 Sec-WebSocket-Version: 13
  • 13 是目前 (2025.06.28) 最新的版本
  • 所有 WebSocket-Version 可以在 這裡 查詢

As Response Header 的情境

  • 如果 Server 支援 Request Header 的 Sec-WebSocket-Version,則不需要 include 這個 Response Header
  • 反之,需要回傳 400 Bad Request,並且在 Response Header 加上 Sec-WebSocket-Version: 12

我們來嘗試 wsWebSocket.Server 是否有支援 400 的情境,使用 Postman 來發起協議溝通的 HTTP Request: postman-invalid-sec-websocket-version-header 確實有回傳 400,不過沒有 Sec-WebSocket-Version 的 Header,並且 Response Body 只有說 Missing or invalid Sec-WebSocket-Version header,不符合規範的 Best Practice,於是我就發了一個 PR,看作者會不會 Approve 囉!

我們再來看看 RFC6455#section-4.4 的描述

The following example demonstrates version negotiation described above:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
...
Sec-WebSocket-Version: 25

The response from the server might look as follows:

HTTP/1.1 400 Bad Request
...
Sec-WebSocket-Version: 13, 8, 7

確實跟 MDN文件 的描述一樣

If the server doesn't support the version, or any header in the handshake is not understood or has an incorrect value, the server should send a response with status 400 Bad Request and immediately close the socket. It should also include Sec-WebSocket-Version in the 400 response, listing the versions that it does support.

Sec-WebSocket-Extensions

這是一個 Request & Response Header

As Request Header 的情境

  • 格式 Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
  • 這些 Extensions 的作用,會在未來的篇章 protocols/websocket 討論

As Response Header 的情境

  • 格式同 Request Header
  • Server 需從 Client 傳來的 Sec-WebSocket-Extensions 挑出其支援的,並且回傳
  • 承上,若沒有 Server 支援的,可以捨棄這個 Header
  • 若 Server 回傳 Client 不支援的 Sec-WebSocket-Extensions,則 Client 必須斷開連結

Sec-WebSocket-Key

  • 這是一個 Request Header
  • 用來告訴 Server "我是真的要 Upgrade 到 WebSocket 呦"
  • Sec- 開頭的 Request Headers,無法透過 fetch 或是
  • 格式 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  • key 的生成方式,根據 MDN 文件 的描述
This is a randomly selected 16-byte nonce that has been base64-encoded and isomorphic encoded.

我們可以使用以下 function 生成

function generateWebSocketKeyModern() {
const bytes = crypto.getRandomValues(new Uint8Array(16));
return btoa(String.fromCharCode(...bytes));
}

Sec-WebSocket-Protocol

這是一個 Request & Response Header

As Request Header 的情境

  • 格式 Sec-WebSocket-Protocol: soap, wamp,代表 Client 支援的 sub-protocols
  • 這些 sub-protocols 的作用,會在未來的篇章 protocols/websocket 討論

As Response Header 的情境

  • 格式同 Sec-WebSocket-Protocol: soap,代表 Server 最終決定使用的 sub-protocol
  • 承上,若沒有 Server 支援的,可以捨棄這個 Header

Sec-WebSocket-Accept

  • 這是一個 Response Header
  • 格式 Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • 計算方式為:
createHash("sha1")
.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
.digest("base64");
  • 代表 Server 接受且支援升級到 WebSocket

參考資料