使用 PowerShell 发送原始 HTTP — 在本地确认 HTTP 运行在 TCP 之上的感觉

ネットワーク・RFCカテゴリを表すパンダのイラスト 网络・RFC

关于本文
本文由利用生成式 AI 创建的自动化工作流生成。其中确认了 RFC 9112 的 HTTP/1.1 消息结构以及 .NET 的 TcpClient / NetworkStream API,并作为不连接外部网站的 localhost 实验进行了实现。

验证状态:📘 已确认 RFC 与 .NET 官方 API・已实现 localhost 示例・尚未在 PowerShell 真实设备上进行验证

使用 PowerShell 发送原始 HTTP — 在本地确认 HTTP 运行在 TCP 之上的感觉

HTTP 并不等同于 TCP。在 HTTP/1.1 中,我们在 TCP 连接之上收发呈 request line、header fields、空行以及按需配置的 body 形状的消息。

平常由 Invoke-WebRequest 和浏览器帮我们隐藏的部分,通过直接向 TcpClient 编写字符串,会变得更容易观察。

本次实验仅在单台 PC 上完成

不将外部网页作为实验对象,而是打开两个 PowerShell。

  1. 在一端启动 127.0.0.1:8085 的简易 HTTP 服务器

  2. 在另一端建立 TCP 连接

  3. 发送 HTTP/1.1 的 GET request

  4. 服务器显示接收到的内容

  5. 返回 200 OK

sequenceDiagram
    participant C as PowerShell Client
    participant T as TCP localhost:8085
    participant S as PowerShell Server
    C->>T: TCP connect
    T->>S: connection accepted
    C->>S: GET / HTTP/1.1 + headers + CRLF CRLF
    S-->>C: HTTP/1.1 200 OK + headers + body
    S-->>C: TCP close

发送的 HTTP 看起来只是普通的字符串

客户端侧的核心代码如下。

$request = @(
    "GET / HTTP/1.1"
    "Host: localhost:8085"
    "Connection: close"
    ""
    ""
) -join "`r`n"

$requestBytes = [System.Text.Encoding]::ASCII.GetBytes($request)
$stream.Write($requestBytes, 0, $requestBytes.Length)
$stream.Flush()

HTTP/1.1 的 request line 是 GET / HTTP/1.1。其后紧跟 header fields,并通过空行来结束 header section。

这里之所以排列两个 "" 并用 CRLF 进行拼接,是为了在末尾生成 CRLF CRLF

不要省略 Host header

RFC 9112 要求在 HTTP/1.1 request 中包含 Host header field。如果因为是教学材料就只发送 GET / HTTP/1.1,就会将作为 HTTP/1.1 重要组成部分的内容给隐藏掉。

因此,即便在 localhost 实验中,我们也明确指定以下内容。

Host: localhost:8085

TcpClient 不懂 HTTP

.NETTcpClient 是处理 TCP 连接的类。通过 GetStream() 获取的 NetworkStream 用于收发字节序列(byte 列)。

也就是说,在此代码中,

flowchart TB
    A[HTTPの文字列] --> B[ASCII bytes]
    B --> C[NetworkStream]
    C --> D[TCP]
    D --> E[localhost server]

这样的层级结构是由我们自己手动搭建的。

掌握了这种感觉后,就能更容易地理解 HTTP client API 其实是在“遵循 HTTP 规则创建消息,并在其下方使用 transport”。

响应的 Content-Length 是字节数,而不是字符数

在服务器端,将 body 转换为 UTF-8 后再创建 Content-Length

$body = "Hello from localhost HTTP over TCP`n"
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($body)

$header = @(
    "HTTP/1.1 200 OK"
    "Content-Type: text/plain; charset=utf-8"
    "Content-Length: $($bodyBytes.Length)"
    "Connection: close"
    ""
    ""
) -join "`r`n"

由于 Content-Length 表示八位字节数(octet),因此当 body 中包含日语等多字节字符时,不能直接使用 .Length 的字符数,这一点非常重要。

HTTPS 并不完全是这样

本教材使用的是明文 HTTP。而在 HTTPS 中,是在 TCP 之上叠加 TLS,并在其之上收发 HTTP。

HTTP
 TLS
  TCP
   IP

因此,即使将此示例指向 443 端口并仅发送 HTTP 字符串,也无法实现正确的 HTTPS 通信。它可以作为接下来学习 TLS 的入门。

GitHub 示例

官方信息・一手资料

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。
标题和URL已复制