04.03 HTTP 与 WebSocket 入门:从历史查询到双向消息通道
返回第四卷 · 前置:04.02 地址与名称 · Go 主线
04.03 HTTP 与 WebSocket 入门:从历史查询到双向消息通道
DeepTutor 初稿经技术与教学审阅后的静态课程。HTTP 报文、WebSocket 握手、帧和失败结果都是教学示例或待实践的预期;本次没有运行 Go、发送真实请求、启动网络服务或构建站点。
为什么先查询历史,再讨论实时消息
04.01 已让你区分应用消息、字节和传输确认;04.02 已讲名称、IP、端口与连接失败。现在假设未来教学 IM 有两个任务:u-a 想读取会话 c-a 的最近历史;之后 u-b 新发来 m-a,希望 A 不断刷新页面也能收到通知。前者适合先用一次请求—响应来讲 HTTP;后者再引出保持双向通道的 WebSocket。
当前课程项目仍只有本地 imhistory 文件 CLI,没有线上 HTTP 或 WebSocket 服务。u-a/u-b/c-a/m-a、chat.example.test 都是虚构教学标识;下方报文不会被发送到真实网络。本章先解释协议字节与语义,再给个人未来实现与验收阶梯,不把纸上 200、101 或 Ping/Pong 当作真实送达证据。
| 第一遍 | 第二遍 | 留下的练习成果 |
|---|---|---|
| HTTP 方法、资源、请求与响应 | WebSocket 升级、帧与应用载荷 | 一份分层失败矩阵与两条消息时间线 |
先修只需 04.01–04.02 和 01.08 的 JSON、错误与文件。TCP 细节在 04.04–04.06,Go 网络 I/O 与帧读取在 04.08,身份授权在 09 卷,可靠消息与已读在 S5。不要为了读本章先假设这些机制已在项目中运行。
一、先把“查询历史”写成用户可核对的合同
用户需求可以说成:已获授权的 u-a 请求会话 c-a 的至多 20 条历史,服务返回有明确范围与顺序的消息列表;若没有消息,返回合法空列表,而非把“空”自动当错误。真正的 API 还需说明排序键、分页游标、时间范围、权限和旧客户端兼容;这里只选最小只读教学例子,不能由一页报文推断历史完整。
用一个虚构资源位置表示这个任务:
https://chat.example.test/v1/conversations/c-a/messages?limit=20
└── 服务名称 ──┘└──────────── 路径 ────────────┘└─查询参数─┘https 表示按 HTTPS 方式访问的方案;名称如何解析、连接到哪个 IP 与端口由 04.02 的步骤决定。路径指“会话 c-a 的消息资源”,查询参数 limit=20 限定本次请求的数量,不表示数据库已经恰好有 20 条。u-a 的身份不能由 URL 中随便写个 user_id 就成立,未来服务必须从可信认证上下文核对会话权限。这里省略认证字段仅为讲解报文结构,并不表示历史查询可以匿名开放。
一个合格合同至少要对以下输入有决定:合法 limit 与最大值、缺失或负数的处理、会话不存在或不可见时怎样回应、空历史怎样表示、分页后怎样知道还有没有下一页。GET 的“读”语义要求调用者可请求资源的表示,而不应靠 GET 实施“删除历史”等有意的业务修改;服务器仍可能记录日志或指标,那不使 GET 自动变成业务写入方法。
二、读懂 HTTP 请求:方法、目标、头部与正文
HTTP 是应用层请求—响应协议。用 HTTP/1.1 文本形式示意一份历史读取请求:
GET /v1/conversations/c-a/messages?limit=20 HTTP/1.1
Host: chat.example.test
Accept: application/json第一行给出方法 GET、本次服务器要处理的请求目标和 HTTP 版本。Host 帮助区分同一网络入口服务的名称;Accept 表示客户端希望收到的表示格式。空行结束头部。这份 GET 示例没有请求正文;未来发送消息的写操作会选择另一个经合同定义的方法和请求体,不能因为“聊天消息也是文本”就把它直接塞进 GET 路径或查询字符串。
代码块用可读换行表示报文行;真正的 HTTP/1.1 文本报文按协议使用 CRLF 分隔行,接收方还要按消息定界规则解析。本章借报文外形讲语义,不用 Markdown 的显示换行代替完整线级解析器。
若服务使用非默认端口 8080,HTTP/1.1 的 Host 应包含 :8080,例如 Host: chat.example.test:8080。上面的 URL 默认端口只为分解结构;本章既不部署也不实际解析该 .test 名称。HTTP/2 和 HTTP/3 的线级表示与此文本不同,但请求方法、资源、状态等语义仍有连续性;版本深入 04.10。
头部提供关于请求、表示或连接的元数据,正文承载可选内容,两者不相等。比如 Content-Type 说明正文的媒体类型,不能把它当 JSON 内容本身;Accept 是客户端的偏好,不能保证服务一定按此返回。收到的 limit 是不可信外部输入,应解析为整数、限上限并决定非法值的错误,而非直接拿它申请无限内存。
GET 在 HTTP 语义中属于安全方法:客户端不应通过它请求改变目标资源的状态;它也具有可重复请求的语义基础。实际实现仍需避免把“读历史”顺手做成会修改消息已读、删除游标或发送通知的隐式业务操作。日志、访问计数等附带效果可以存在,但应用承诺的资源操作仍应是读取。缓存和私密历史的安全策略另需明示,不能因为使用 GET 就默认允许共享缓存保存用户消息。
三、读懂 HTTP 响应:状态、头部和表示
假定已获授权,c-a 在这个教学查询范围内没有消息。下面的 HTTP/1.1 字节示意正文全是 ASCII,长度正好是 39 B,所以 Content-Length: 39 与正文一致:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Content-Length: 39
{"conversation_id":"c-a","messages":[]}状态行的 200 表示该 HTTP 请求按响应方合同处理成功;Content-Type 让客户端按 JSON 解释正文;Cache-Control: no-store 是本教学响应对敏感历史的缓存限制选择,不是认证或加密机制。Content-Length 的单位是字节,正文若含 UTF-8 中文、空格或不同字段,值必须重新计算;不能把字符数直接当字节数。实际实现可能使用其他 HTTP 消息定界方式,本例只演示最直接的一种。
39 B 只计上面那段 JSON 自身的 ASCII 字节,不包含 Markdown 代码块为排版显示的末尾换行;若实际发送正文时另加换行,长度就要随之改变。
messages:[] 是显式空数组,表示本次查询范围内没有返回消息。它不说明历史从未有过 m-a,也不说明其他页、其他设备或尚未同步的来源都为空。返回的 conversation_id、排序、分页游标和时间范围应由未来 API 合同补齐,避免让客户端猜“空”究竟表示无权限、无历史还是查询窗口错了。
HTTP 状态码有标准语义,应用再决定自己的错误映射:
| 状态或现象 | 最低限度的 HTTP 含义 | IM 教学 API 还须说明 |
|---|---|---|
200 OK | 响应方报告请求成功 | 返回范围、排序、是否还有下一页 |
400 Bad Request | 请求有问题 | 非法 limit、格式、版本如何标识 |
401 Unauthorized | 请求缺少有效的认证凭据或需认证挑战 | 怎样安全地重新认证;响应应符合 HTTP 认证要求 |
403 Forbidden | 响应方理解请求但拒绝完成 | 是会话权限、策略还是其他拒绝 |
404 Not Found | 找不到目标资源的当前表示,或不愿披露存在 | 是否有意隐藏无权访问的会话 |
500 Internal Server Error | 响应方内部无法完成 | 是否可重试、是否可能部分处理 |
| 没拿到 HTTP 响应 | 结果可能停在 DNS、连接、等待或中途断开 | 不能强行编一个 404/500 |
404 不严格等于“数据库里绝无此会话”:RFC 的语义也允许服务不披露某资源是否存在。401/403 的具体使用与凭据、策略有关,不能仅靠数字推断是 Linux 文件权限失败。还要确认谁返回了该 HTTP 状态:代理或错误目标上的 HTTP 服务也能返回 200/404。本章的数字都是静态合同示意,不代表已存在的 IM API。
一个“HTTP 200”也不等于消息 m-a 已送达 B 或 u-b 已读。它只覆盖本次历史查询的响应范围;实时投递要另有事件与确认点。
四、从请求—响应到双向通道
历史查询天然有“客户端问一个范围、服务端答一个表示”的节奏。实时消息则还需要服务端在有新事件时及时通知客户端。可以反复轮询、使用长轮询,也可以建立持续的双向通道;它们在请求次数、连接资源、延迟和故障处理上的取舍不同。本章选择 WebSocket 解释一种双向通道,不把它说成所有 IM 系统的唯一方案。
在这里的 HTTP/1.1 WebSocket 握手中,客户端先发特殊 HTTP 请求,服务端按协议同意后返回 101 Switching Protocols,之后同一连接上传递 WebSocket 帧,而不是把每条聊天消息都做成一条新的普通 HTTP 请求。ws:// 与 wss:// 分别表示普通 WebSocket 和采用 TLS 的 WebSocket 地址形式;证书、身份与加密保证留到 04.07 深入。当前静态例子不建议在真实网络上使用无加密通道。
连接建立、WebSocket 握手通过、服务端接受 m-a、B 设备收到、用户已读依然是五个不同阶段。HTTP 101 只表示双方按这一握手要求切换协议,不能把它当消息投递回执。服务还要认证客户端、检查会话权限、限制消息大小和处理断线;这些并不会因采用 WebSocket 自动出现。
五、逐行看一次 HTTP/1.1 升级握手
下例使用 RFC 6455 的配对示例 key/accept 值,改用本课程虚构域名与事件路径。它说明字段关系,不是向真实 chat.example.test 发出的请求。先看客户端的开场:
GET /v1/events HTTP/1.1
Host: chat.example.test
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13它仍是一份 HTTP/1.1 GET 请求。Upgrade: websocket 提出切换协议;Connection: Upgrade 标明这个升级字段用于当前连接;Sec-WebSocket-Version: 13 声明本例采用的协议版本;Sec-WebSocket-Key 是客户端提供的随机挑战值,服务器要据协议计算回答。这里固定的 Key 只为复核 RFC 示例中的配对结果,真实握手应为每次连接生成新的值。这个 Key 不是访问令牌或用户身份凭据,不能因为它格式正确就让客户端读取 c-a。
若响应方同意并完成此握手,可返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=这组 Sec-WebSocket-Accept 与上面的示例 Key 按 RFC 6455 对应;客户端应核对所需握手字段与计算关系,不是“只看到 101 就无条件信任”。101 本身表示协议切换,不表示服务器已接受 m-a、有权订阅 c-a 或设备 B 已在线。握手若收到普通 HTTP 错误、版本不被支持、关键字段不匹配或中间代理未按预期转发,故障阶段也不同;要保留实际响应和所处边界,不能把它统称成“WebSocket 消息丢了”。
浏览器发起时还可能有 Origin 等字段,服务端应按自身安全策略处理;真实认证也需要另定机制。这里省略了身份与授权过程,只是为了先读懂升级的协议形状,并不构成可部署的开放接口。握手之后的字节遵循 WebSocket 协议,不再按普通 HTTP 请求行一条条解释。
六、TCP 字节流、WebSocket 帧与 IM 业务消息
04.01 已强调:TCP 提供字节流,一次应用写入和一次网络读取没有天然的一条消息边界。WebSocket 在其上定义帧与消息:帧中包含类型信息(opcode)、载荷长度、是否为消息最后一帧(FIN)等;一条 WebSocket 消息可能由多个帧组成,控制帧可用于 Ping、Pong 和关闭。故“收到一个帧”与“得到了完整 IM 消息”也不是等号。
TCP 字节流
→ WebSocket 协议解析出一个或多个帧
→ 组合/解释为 WebSocket 消息
→ 教学 IM 应用再解析 type、message_id、conversation_id、body 等字段
→ 业务规则决定接受、拒绝或继续处理WebSocket 的客户端发往服务端的帧必须按协议掩码处理,服务器发给客户端的帧不能使用客户端掩码规则。掩码是帧协议要求,不是加密或身份认证。学习者不必在本节手写位级解析器;04.08 会深入长度、短读短写和上限。即使某个 WebSocket 库替你处理了帧,应用仍要定义并校验自己的消息版本、ID、会话、正文大小与错误回答。
例如未来应用可能在 WebSocket 载荷里放这样的待设计 JSON:
{"type":"message","message_id":"m-a","conversation_id":"c-a","body":"你好"}这只是静态讨论对象,不是本地历史 JSON v1 文件格式,也不是已实现的在线协议。服务不能仅凭载荷自称某个发送者就授予权限;应从经过验证的连接或请求身份出发,再核对 c-a 会话规则。若一条消息因分帧尚未完整、长度超限或版本未知,不能把部分载荷交给业务层当成功。关闭帧与 TCP 连接结束也需要区分;发生断线后上次请求是否已经被处理,可能是未知结果。
Ping/Pong 可以帮助观察 WebSocket 通道在某个时刻是否有协议级往返,但不说明 m-a 已被持久化、B 设备已展示或 u-b 已读。若应用需要这些状态,应定义自己的业务确认及其含义。04.05 才把心跳、连接生命周期与重连放进更完整的时序;S5 再处理离线、去重与恢复。
七、按阶段整理失败和确认,不用一个“成功”遮住问题
把历史 HTTP 查询与实时 WebSocket 通道放在同一张表里,就能看到它们各自的证据边界:
| 阶段 | 可能观察到什么 | 能说明的范围 | 仍不能推出 |
|---|---|---|---|
| DNS/连接 | 名称失败、拒绝、超时 | 尚未得到对应 HTTP 响应 | c-a 无历史、权限被拒 |
| HTTP 历史查询 | 200、400、401、403、404、500 | 某个 HTTP 响应者的本次结果 | 实时消息已送达 |
| WebSocket 握手 | 101、HTTP 错误、关键字段不符 | 是否按该连接切换协议 | 用户有权加入会话 |
| WebSocket 数据 | 完整消息、帧错误、Ping/Pong、关闭 | 通道与帧协议当前状态 | 应用已保存 m-a |
| 应用业务确认 | 自行定义的接受/保存回执 | 回执明说的检查点 | B 收到或用户已读,除非另有证据 |
| B 设备/用户事件 | 接收、展示、已读各自记录 | 对应设备或阅读状态 | 所有设备永远一致 |
几个容易混淆的反例:历史查询 200 只说明那次查询成功,不能证明新发消息投递;WebSocket 101 只说明握手成功,不能证明会话授权;Ping/Pong 往返只说明通道协议在这个观察窗口内有回应,不能证明业务端已保存;发送后超时可能是请求未到达,也可能是业务已处理但回答丢失,不能直接重试并断言不会重复。
HTTP 是请求—响应,但 HTTP/1.1 连接可以复用,不能教成“每个 GET 必然新建 TCP 连接”。WebSocket 适合持续双向消息,但连接长期存在要承担描述符、缓冲、心跳、关闭和重连责任;不能因为希望低延迟就把所有历史分页和一次性查询都强制搬进 WebSocket。选择协议要从查询、推送、权限、资源与恢复要求出发,再由真实测试比较;本章只给理论边界。
八、个人课程项目阶梯与分层反馈
学习者后续可以在自己的隔离目录分两轮实现和验证,课程仓库当前不添加服务软件:
- 先设计只读 HTTP 历史查询:写明资源、GET、
limit上限、身份来源、空列表、分页、400/401/403/404 与 500 的处理。先画报文和决策表,再在具备 Gonet/http前置后个人实现;记录真实代码版本、请求、状态、正文与授权负例。 - 再设计双向通道:先画 101 握手及失败路径,再给应用消息版本、ID、字节上限和业务确认命名;最后才在自己的测试环境接入 A/B 两个客户端。分别记录“握手完成”“服务接受”“B 接收”,不把没有实际观测的阶段写成已通过。
分层练习:先回答,再核对反馈
1. 为什么先教历史 GET 再教 WebSocket?
一次只读请求—响应能先建立方法、目标、状态和正文,之后再增加持续双向通道的状态。
2. `limit=20` 表示服务端一定返回 20 条吗?
不是。它是本次返回数量的请求上限,还要看权限、范围和实际历史。
3. GET 的方法和资源各在哪里?
请求行的 GET 是方法,路径和查询部分表示目标资源及其参数。
4. `Host` 和 `Accept` 是正文吗?
不是。它们是 HTTP 头部字段,正文在头部结束的空行之后。
5. GET 的安全语义允许故意删除历史吗?
不允许把“删除目标资源”作为 GET 的预期业务效果;日志等附带效果另论。
6. 响应中的 `Content-Length: 39` 按字符还是字节算?
按字节。本例正文全 ASCII,正好 39 B;改成中文需重新编码计数。
7. `messages:[]` 一定说明会话从未有过消息吗?
不能。它只表示本次已定义查询范围返回了空数组。
8. HTTP 200 能证明 `u-b` 已收到新发的 `m-a` 吗?
不能。本章 200 示例只是一次历史查询响应。
9. HTTP 404 一定证明会话在数据库里不存在吗?
不能。响应方可能不愿披露资源存在,还要确认实际响应者和 API 合同。
10. 没收到 HTTP 响应时可以编一个 500 吗?
不可以。结果可能停在 DNS、连接、等待或中途断开,应报告实际观察。
11. WebSocket 开场请求为什么仍有 HTTP GET 行?
本章的 HTTP/1.1 升级方式先通过 HTTP 请求协商协议切换。
12. WebSocket 的 101 表示什么?
表示该连接的协议切换成功,不是 IM 消息受理或送达回执。
13. `Sec-WebSocket-Key` 是 `u-a` 的登录令牌吗?
不是。它是握手挑战值,身份验证必须另行设计。
14. 握手后每条聊天消息都是新 HTTP 请求吗?
不是。握手后这条连接上传递 WebSocket 帧与消息。
15. TCP 字节流能自动保留 `m-a` 的边界吗?
不能。WebSocket 帧与应用载荷各自定义上层边界。
16. 一条 WebSocket 消息只能有一帧吗?
不一定。它可被分成多个帧;接收方应按协议组合。
17. 客户端帧的掩码等于加密吗?
不等于。掩码是 WebSocket 帧协议要求,机密性依赖另行设计的安全传输。
18. Ping/Pong 回来就说明 `m-a` 已保存吗?
不能。它是通道协议级往返,不是业务保存回执。
19. 客户端在发送后超时,能认定服务端没有处理吗?
不能。回答可能丢失,实际处理结果可能未知。
20. HTTP/1.1 的每个 GET 都要新建 TCP 连接吗?
不一定。连接可以复用,不应把请求数直接当连接数。
21. 为什么不能仅靠客户端载荷里的 `sender_id` 授权?
它是可控输入,服务必须由可信身份上下文验证发送者及会话权限。
22. 本章的 101、200、消息帧示例能证明线上 IM 已运行吗?
不能。它们是静态教材;实际服务与 A/B 设备结果需独立实现和验证。
来源与下一步
- RFC 9110:HTTP 语义、RFC 9112:HTTP/1.1:方法、资源、状态与文本报文格式。
- RFC 6455:WebSocket:HTTP/1.1 升级握手、101、帧和掩码规则。
- Go
net/http:后续个人 HTTP 历史查询实现的标准库入口。
按学习路线,下一章先读05.01 并发任务与生命周期,再回到 04 卷深化传输和网络 I/O。离开本章前,请能准确说出“HTTP 200”“WebSocket 101”“业务接受”“设备接收”“用户已读”各自属于哪条证据链。