Ran Wei/计算机科学系列/11
English
计算机科学基础 — Ran Wei

模块 11: 网络与 Web

跟随数据从应用进入网络再返回,为 TCP 流分帧,构建有限本地 HTTP 客户端服务器实验。

约 5 小时4 个时段2 个实验8 道练习6 道自测题

完成后你能够

  • 描述网络分层与地址。
  • 解释 DNS 解析。
  • 区分 TCP 流与 UDP 数据报。
  • 实现有界消息分帧。
  • 解释 HTTP 方法与状态。

开始之前

建议先修模块: 02, 10.

了解字节、进程与线程。实验使用回环地址和自动端口,不访问外部服务。

目录

学习计划

5 小时

四个 75 分钟时段,包含练习。拓展任务或不熟悉的先修知识可能需要更多时间。进度本地保存,两种语言共享。

时段 275 分钟
构建与探究
时段 375 分钟
应用与拓展
时段 475 分钟
推理与复习
1

分层与包传递

应用定义消息,传输协议连接端点,IP 在网络间路由包,链路技术在本地一跳传帧。各层添加自身职责所需信息。套接字端点有地址与端口,地址选目的地,端口选服务上下文。包可经过多个路由器,路线不同于应用会话。丢失、延迟与乱序都是协议需处理的情况。

检查理解

为何主机除 IP 还需端口?

完整解答

区分主机上的服务端点。

2

名字与 DNS

DNS 把域名映射到包括地址的记录,解析器查询缓存与分布层次。缓存有生存期,修改不会立即影响所有客户端。域名并非永久唯一 IP。URL 还有协议、路径、可能的端口和查询。实验用数字回环隔离外部 DNS 与路由,并非完整公网解析演示。

检查理解

改 DNS 记录会立即使所有缓存失效吗?

完整解答

不会,缓存可能保留到生存期结束。

3

TCP 与 UDP

TCP 提供有序字节流及传输可靠性与流控,不保留应用消息边界:一次发送可需多次读取,多次发送也可一次读取。recv 返回空字节表示对端关闭发送方向。UDP 保留数据报边界,但不保证交付或顺序。按应用要求选择,超时不代表对端一定没执行工作。

检查理解

recv(100) 可返回少于百字节吗?

完整解答

可以,需循环到帧完整或连接结束。

4

分帧与防御性解析

协议可用分隔符、固定宽度或长度前缀。帧包含四字节无符号大端长度,后跟相应 UTF-8 字节。完整读头、验证最大长度、完整读体再解码。长度统计字节而非字符。辅助函数每次最多读两字节,明确练习部分读取。截断帧应拒绝,不能当有效消息。

检查理解

为何先验证长度再分配或读取?

完整解答

限制不可信输入造成的资源消耗。

5

HTTP、HTTPS 与请求语义

HTTP 请求含方法、目标与头,响应含状态、头与可选体。GET 获取表示,POST 请求资源特定处理。200 成功,404 无对应资源。JSON 是一种体格式,不等于 HTTP。HTTPS 使用 TLS 验证服务器并保护传输,但不能替代应用验证与授权。超时后重试修改需幂等设计避免重复。

检查理解

HTTPS 本身防 SQL 注入吗?

完整解答

不能,安全传输与安全查询解决不同问题。

6

常见误解

  • recv 大小是上限,不承诺完整消息。
  • 本地成功不验证公网路由、DNS 或 TLS。
7

实验准备

下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m11_stream.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。

8

实验 1 — 流分帧

有限服务器发送一帧后关闭,客户端组装部分读取并检查 1024 字节上限。

下载 m11_stream.py

"""Receive a length-prefixed message even when reads return partial chunks."""
import socket
from threading import Thread
def read_exact(stream, count):
    result = bytearray()
    while len(result) < count:
        chunk = stream.recv(min(2, count - len(result)))
        if not chunk:
            raise EOFError("connection closed before complete frame")
        result.extend(chunk)
    return bytes(result)

with socket.socket() as listener:
    listener.bind(("127.0.0.1", 0)); listener.listen(1); listener.settimeout(5)
    payload = b"Dune,Foundation"
    errors = []
    def serve():
        try:
            connection, _ = listener.accept()
            with connection:
                connection.sendall(len(payload).to_bytes(4, "big") + payload)
        except Exception as error:
            errors.append(error)
    worker = Thread(target=serve); worker.start()
    with socket.create_connection(listener.getsockname(), timeout=5) as client:
        length = int.from_bytes(read_exact(client, 4), "big")
        if length > 1024: raise ValueError("frame too large")
        result = read_exact(client, length)
    worker.join(timeout=5)
    assert not worker.is_alive() and not errors and result == payload
    print("frame bytes:", length, "decoded:", result.decode("utf-8"))
实际运行输出
frame bytes: 15 decoded: Dune,Foundation
  1. 用含中的 UTF-8 文字并重算字节长度。
  2. 声明比实际更长的体。
  3. 声明五千长度,验证早拒绝。
完整解答

先编码再统计字节。截断体在对端关闭时抛 EOFError,五千在读取体前拒绝。保留等待线程和套接字上下文以清理。

9

实验 2 — HTTP 目录响应

查看 JSON 体、类型与状态。服务器仅本地绑定并在检查后关闭;http.server 用于学习,不是生产服务器。

下载 m11_http.py

"""A finite localhost HTTP experiment with JSON, statuses and cleanup."""
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Thread
from urllib.error import HTTPError
from urllib.request import urlopen
class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/books":
            self.send_error(404); return
        body = json.dumps([{"id": 1, "title": "Dune"}]).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers(); self.wfile.write(body)
    def log_message(self, *args):
        pass

server = HTTPServer(("127.0.0.1", 0), Handler)
thread = Thread(target=server.serve_forever); thread.start()
try:
    base = f"http://127.0.0.1:{server.server_port}"
    with urlopen(base + "/books", timeout=5) as response:
        books = json.load(response)
        print("GET /books:", response.status, books)
        assert response.status == 200 and books[0]["title"] == "Dune"
    try:
        urlopen(base + "/missing", timeout=5)
    except HTTPError as error:
        print("GET /missing:", error.code); assert error.code == 404; error.close()
    else:
        raise AssertionError("missing resource must be 404")
finally:
    server.shutdown(); server.server_close(); thread.join(timeout=5)
    assert not thread.is_alive()
实际运行输出
GET /books: 200 [{'id': 1, 'title': 'Dune'}]
GET /missing: 404
  1. 加入返回 JSON 的 /health。
  2. 空目录返回空列表。
  3. 解释缺失路径与零匹配图书的区别。
完整解答

有效 /health 返回 200 与状态对象。/books 空列表仍 200,缺失路由为 404。Content-Length 应等于编码后的体字节数。

10

练习与完整解答

先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。

练习 1 — 端点★

同 IP 的两服务靠什么区分?

完整解答

传输协议与端口及地址上下文。

练习 2 — 名字与地址★

域名可对应多地址吗?

完整解答

可以,多地址记录且答案可变化。

练习 3 — 部分读取★★

六字节分为二、一、三到达,如何读?

完整解答

累积至六,不把不完整体当完成消息解码。

练习 4 — UTF-8 长度★★

A中 声明多少帧长度?

完整解答

四字节,而非两码位。

练习 5 — 状态区分★★

空 /books 与未知路径的状态?

完整解答

有效空集合 200,未知资源 404。

练习 6 — 重试歧义★★★

借阅 POST 传出后超时,能假定失败吗?

完整解答

不能,可能已提交但响应丢失;用稳定请求 ID 与保存结果去重。

练习 7 — 协议约定★★★

规定有界文字消息格式。

完整解答

四字节无符号大端长度,最多 1024 字节,随后有效 UTF-8;拒绝超大、截断与编码错误,规定零长度是否有效。

练习 8 — 传输与应用★★

TCP 可靠性保证数据库事务提交吗?

完整解答

不,传输交付不同于应用验证、执行与提交,需应用响应约定。

11

自测

选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。

1

TCP 提供什么?

2

UDP 保留什么?

3

长度前缀统计什么?

4

HTTP 404 是什么?

5

TLS 代替授权吗?

6

超时证明服务器未执行吗?

答案表
  1. A — 应用分帧另行定义。
  2. B — 边界不等于可靠性。
  3. C — 传输承载字节。
  4. A — 它是应用响应状态。
  5. B — 传输安全不授予访问权。
  6. C — 可能完成后响应丢失。
12

引导阅读

13

复习与下一步

分别跟踪字节帧与 HTTP 响应,解释超时不能证明什么。模块 12 将用关系数据库持久保存目录。

14

关键术语

术语含义
分帧在传输数据中识别消息边界。
幂等性重复应用与一次的预期效果相同。
回环指向本地主机的网络通信。