iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Modern Web

Learn HTTP With JS系列 第 19

Upgrade

  • 分享至 

  • xImage
  •  

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

參考資料


上一篇
Strict-Transport-Security
下一篇
HTTP redirections
系列文
Learn HTTP With JS30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言