📘 第十章 构建Web内容的技术
📄 一句话概括

静态页的地址指向一个文件,Web 应用的地址指向一段会算东西的程序。 同一个 /calc,参数不同结果就不同,参数不合法就回 400。

📝 本节和前面几节的关系

上一节那棵树是「结果」。 这一节看「结果是怎么算出来的」——服务器那边跑的不再是「取文件」,而是一段程序。

一、从「给你文件」到「帮你算」

早期的 Web 服务器只干一件事:把路径映射成磁盘上的文件,读出来发走。/index.html 就是那个文件。 Web 应用不是这样。 /calc 这个路径在磁盘上根本不存在,它是程序里的一条路由:收到请求,取参数,算, 把结果当正文发回去。 Demo 里四次请求就能看出来:/calc?a=3&b=4 回 7,/calc?a=100&b=-58 回 42, /calc?a=x&b=4 回 400,而 POST /calc 的路径里一个参数都没有,结果照样算出来了 15。 这条分界线就是「静态」和「动态」的分界:同一个地址,不同的时候可以给不同的东西。

二、参数放哪,后果不一样

GET 把参数拼在 URL 里,POST 放在请求正文里。技术上都行,但位置不同带来三个后果。 一、长度。 URL 会进服务器日志、进浏览器历史、进 Referer 首部,所以它有实际的长度上限,各家不同但都不大。 正文没有这个问题。 二、编码。URL 里不能直接放非 ASCII 字符,中文要变成百分号编码。 Demo 量了一下:「客户」两个字本来是 6 字节,编码后 18 字节, 正好 3 倍——因为每个字节都变成「百分号加两位十六进制」。 三、会不会被记住。URL 里的东西会被存下来、会被分享、会留在日志里;正文不会。 所以密码、令牌这类东西只能放正文,还得配上 HTTPS。

三、无状态带来的麻烦

HTTP 本身不保存状态(2.3)。Web 应用偏偏需要——登录过的用户,下一个请求怎么还认得? 答案在 2.8 和 8.5.2:用 cookie 带一个会话 ID,服务端拿这个 ID 去查会话表。 所以「Web 应用」这套东西是 HTTP 之上搭出来的,它借了 HTTP 的传输,状态得自己想办法存。 这也解释了为什么 Web 应用特别怕「会话固定」(8.5.2)和「CSRF」(第十一章)——身份全靠那一个 ID, 而 ID 是能被摆弄的。

四、动手 Demo:一个会算加法的地址

本机起一个服务端,/calc 不是文件,是一条路由:取 a 和 b 两个参数,算完把结果当正文发回去。 分别用 GET 和 POST 各试几次,再量一下中文参数编码前后的字节数。

qs = u''
if u'?' in target:
    target, qs = target.split(u'?', 1)
if method == u'POST':
    qs = body.decode('utf-8')          # 参数在正文里
q = parse_qs(qs)
a, b = int(q[u'a'][0]), int(q['b'][0])
return 200, u'%d + %d = %d' % (a, b, a + b)

实跑输出:

--------------------------------------------------------------------
同一个地址,参数不同,结果不同
--------------------------------------------------------------------
  GET /calc?a=3&b=4      → 200  3 + 4 = 7
  GET /calc?a=100&b=-58  → 200  100 + -58 = 42
  GET /calc?a=x&b=4      → 400  参数不是整数
  POST /calc             → 200  7 + 8 = 15

  四次请求的路径             两个不同,还有两个相同
  POST 那次路径              /calc(参数不在 URL 里)

  这就是「Web 应用」和「静态页」的分界:地址本身不指向一个文件,而是指向一段**会算东西的程序**。
  同一个 `/calc`,参数不同结果就不同;参数不合法,回 400。

  参数值「客户」本身         6 字节
  百分号编码之后             %E5%AE%A2%E6%88%B7 → 18 字节
  膨胀                       3 倍

  每个非 ASCII 字节变成三个字节(`%` 加两位十六进制),所以中文在 URL 里
  正好是三倍。这也是为什么 GET 不适合传长参数——除了膨胀,URL 本身还有长度上限。

  POST 那次的正文            a=7&b=8,7 字节
  它在哪                     请求正文里,不在 URL 里

三次 GET 分别回 200/200/400。 第三个参数 a=x 不是整数,服务端直接拒绝——这就是「程序」和「文件」的区别:它会判断。 POST /calc 的路径里没有问号、没有参数,正文是 a=7&b=8,结果 15 照样算出来了。 参数放哪,程序自己读就行。 「客户」两个字 6 字节,百分号编码后 18 字节,正好 3 倍。 所以 GET 传中文参数,长度一旦上去,URL 很快就顶到上限。 POST 那次的正文是 7 字节。 它不进 URL、不进日志、不进 Referer——这三条是「敏感数据只能走正文」的直接理由。

30 秒实验

  1. 把 a 传成一个超长字符串(比如一千个字符),看服务端和浏览器谁先拒绝。
  2. 用 curl 对比 GET 和 POST 同一个接口,看 -v 输出里请求行长差多少。
  3. 想一想:为什么登录表单一定要用 POST,不能因为「方便分享」用 GET?

常见误区

  1. 「GET 和 POST 的区别只是有没有正文」——还有长度、编码、会不会被记录这些差别。
  2. 「Web 应用也是取文件」——路径往往是路由,磁盘上不一定有对应文件。
  3. 「参数放 URL 里也能传密码,反正加密了」——URL 会进日志和历史记录,加密也挡不住。
自测三问(点开对答案) 1. Web 应用和静态页的分界在哪? —— 静态页的地址对应文件,Web 应用的地址对应一段会算东西的程序。 2. 中文放进 URL 会膨胀多少倍?为什么? —— 三倍。每个非 ASCII 字节变成百分号加两位十六进制,共三个字节。 3. 为什么敏感参数不能放 URL? —— URL 会进服务器日志、浏览器历史、Referer 首部,等于泄露到多处。

这段「会算东西的程序」在服务器上怎么被叫起来?最早的办法叫 CGI。

相关笔记