📘 第五章 与HTTP协作的Web服务器
📄 一句话概括

一台机器、一个 IP、一个端口,可以挂无数个网站。分派的唯一依据是请求首部里那一行 Host。

📝 本节和前面几节的关系

第四章讲完了「服务器返回什么」。从这一章开始,看站在客户端和源服务器中间的那些程序。 第一节先解决一个更基础的问题:一个 IP 怎么装下多个站点。

一、为什么不是一 IP 一站点

IPv4 地址只有 32 位,总数约 43 亿,早就分完了。 如果每个网站都要独占一个公网 IP,互联网根本装不下今天的站点数。 于是有了「虚拟主机」这个做法:一台物理机器上跑多个站点,它们共用一个 IP、一个端口, 靠请求里的域名来区分。Apache 叫 VirtualHost,Nginx 叫 server 块,都是这件事。 从外面看,alpha.example 和 beta.example 是两个网站; 从里面看,它们是同一台机器上同一个进程里两段配置。

二、Host 头是唯一的分派依据

关键点在这里:HTTP 请求行里没有域名,只有路径。 GET / HTTP/1.1 里的那个 /,不含任何「我要访问哪个网站」的信息。 DNS 只负责把域名解析成 IP。 解析完,客户端拿到的就是一个 IP 地址,至于这个 IP 上挂着几个站,DNS 一无所知,也不负责。 所以 HTTP/1.1 强制要求每个请求都带 Host 首部(RFC 7230 第 5.4 节),写明「我要的是这个主机名」。 服务端拿它去配置表里查,查到哪段配置就用哪段。 没带 Host 会怎样?HTTP/1.1 的服务端必须回 400。 Demo 里那个不带 Host 的 1.0 请求就是这样被拒的。

三、三种实现方式

方式 靠什么区分 现状
基于域名 Host 首部 绝对主流,一台机器挂几十上百个站
基于 IP 连接的目标 IP 早期做法,一个站一个 IP,太费地址
基于端口 目标端口号 内部管理后台常见,如 :8080
三种可以叠着用。云上最常见的是「一个 IP + 一个 443 端口 + 几百个域名」,全靠 Host 分。

四、书里没写、但今天必须知道的三个坑

坑一:HTTPS 时代还需要一个 SNI。HTTPS 是先做 TLS 握手,再传 HTTP 报文。 握手的时候 HTTP 还没开始,Host 首部根本不存在,服务端却必须在那时就选出对应的证书。 解决办法是 TLS 的 SNI 扩展(RFC 6066),在握手时先报一次域名。 不支持 SNI 的老客户端,只能回到「一 IP 一证书」。 坑二:HTTP/2 的连接复用会串站。HTTP/2 里域名换成了 :authority 伪首部, 而且允许把证书覆盖的多个域名复用到同一条连接上。 复用得不对时,服务端会回 421 Misdirected Request。这个码 2015 年才有,书里没有。 坑三:Host 是可以被伪造的。它是客户端自己填的,服务端不能信。 如果代码里有「用 Host 拼密码重置链接」这种写法,攻击者改个 Host 就能把链接指向自己的服务器, 受害者点开就把令牌交出去了。这叫 Host 头注入。正经做法是配置里写死允许的域名白名单,对不上就拒。

五、动手 Demo:同一个端口上的四个 Host

起一个只监听一个端口的服务端,里面配三个域名,然后分别用三个域名、一个没配过的域名、 一个不带 Host 的 1.0 请求去打它。

SITES = {
    u'alpha.example': (u'alpha', u'这是 alpha.example 的首页'),
    u'beta.example': (u'beta', u'这是 beta.example 的首页'),
    u'gamma.example': (u'gamma', u'这是 gamma.example 的首页'),
}

def responder(raw, st):
    first, hdrs, _b = H.parse_headers(raw)
    host = hdrs.get(u'host', u'')        # 分派只看这一个
    if host in SITES:
        name, text = SITES[host]
        return reply(u'HTTP/1.1 200 OK',
                     [(u'X-Site', name)], text.encode(u'utf-8'))
    if not host:
        return reply(u'HTTP/1.1 400 Bad Request', [], b'must have Host')
    return reply(u'HTTP/1.1 404 Not Found', [], b'no such site')

实跑输出:

--------------------------------------------------------------------
一台机器上的三个站点:分派依据是 Host 头,不是 IP
--------------------------------------------------------------------
  服务端只监听了一个端口:127.0.0.1:57315
  配置了 3 个域名,共用这一个 IP 和这一个端口

  Host: alpha.example        HTTP/1.1 200 OK | 请求 39 字节 | 响应 144 字节
                             正文:这是 alpha.example 的首页
                             响应的 X-Site 首部:alpha

  Host: beta.example         HTTP/1.1 200 OK | 请求 38 字节 | 响应 142 字节
                             正文:这是 beta.example 的首页
                             响应的 X-Site 首部:beta

  Host: gamma.example        HTTP/1.1 200 OK | 请求 39 字节 | 响应 144 字节
                             正文:这是 gamma.example 的首页
                             响应的 X-Site 首部:gamma

  Host: nobody.example       HTTP/1.1 404 Not Found | 请求 40 字节 | 响应 147 字节
                             正文:没有配置这个域名:nobody.example

  命中统计                   alpha.example=1 | beta.example=1 | gamma.example=1 | nobody.example=1

  再发一个不带 Host 的 HTTP/1.0 请求,看服务端还能不能分派:
  HTTP/1.0 无 Host           HTTP/1.1 400 Bad Request
                             正文:HTTP/1.1 必须有 Host

  一台机器、一个 IP、一个端口,靠 Host 头认出三个站点。
  域名只是邮件地址式的标签,真正落到哪台机器还得靠 DNS 解析;
  而落到同一台机器之后,靠的就是请求首部里那一行 Host。

三个域名打同一个端口,各自拿到了自己的正文(这是 alpha.example 的首页 等), 响应里还带了 X-Site: alpha/beta/gamma 三个不同的标记。 nobody.example 没配过,回 404;不带 Host 的 1.0 请求回 400。 请求字节数 38 到 40 不等,差的就是域名长度。 注意服务端配置了三个站,但只开了一个监听端口——这就是虚拟主机,地址不够用时的标准解法。

30 秒实验

  1. 把 SITES 里加一个域名,重跑,看新域名能不能直接访问(不需要改端口)。
  2. 给 alpha.example 配一段不同的默认响应,再用 IP 直连(Host 填 IP),看会落到哪个站。 很多服务器会落到第一个配置,这就是「默认站点」泄露。
  3. 想一想:如果客户端能在 Host 里随便填,服务端拿它去查数据库或拼链接,最坏的情况是什么?

常见误区

  1. 「一个域名必须对应一个 IP」——域名只是个标签,多少域名共用一个 IP 都行。
  2. 「DNS 能区分同一 IP 上的不同网站」——DNS 只到 IP,后面的事它不管。
  3. 「Host 头可以拿来做业务判断」——它是客户端填的,必须校验白名单。
自测三问(点开对答案) 1. HTTP/1.1 里服务端靠什么区分同一 IP 上的不同站点? —— 请求首部里的 Host 字段。请求行只有路径,不含域名。 2. HTTPS 为什么还需要额外的机制才能做到一 IP 多站? —— 因为 TLS 握手在 HTTP 报文之前,那时还没有 Host; 要靠 TLS 的 SNI 扩展先报域名,服务端才知道该出哪张证书。 3. 虚拟主机有哪三种实现方式? —— 基于域名(看 Host)、基于 IP(看目标地址)、基于端口(看端口号),可以叠用。

一台机器能装下多个站点之后,下一个问题是:那些夹在客户端和服务器中间的程序又是怎么工作的。

相关笔记