一台机器、一个 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 秒实验
- 把
SITES里加一个域名,重跑,看新域名能不能直接访问(不需要改端口)。 - 给
alpha.example配一段不同的默认响应,再用 IP 直连(Host填 IP),看会落到哪个站。 很多服务器会落到第一个配置,这就是「默认站点」泄露。 - 想一想:如果客户端能在 Host 里随便填,服务端拿它去查数据库或拼链接,最坏的情况是什么?
常见误区
- 「一个域名必须对应一个 IP」——域名只是个标签,多少域名共用一个 IP 都行。
- 「DNS 能区分同一 IP 上的不同网站」——DNS 只到 IP,后面的事它不管。
- 「Host 头可以拿来做业务判断」——它是客户端填的,必须校验白名单。
自测三问(点开对答案)
1. HTTP/1.1 里服务端靠什么区分同一 IP 上的不同站点? —— 请求首部里的 Host 字段。请求行只有路径,不含域名。 2. HTTPS 为什么还需要额外的机制才能做到一 IP 多站? —— 因为 TLS 握手在 HTTP 报文之前,那时还没有 Host; 要靠 TLS 的 SNI 扩展先报域名,服务端才知道该出哪张证书。 3. 虚拟主机有哪三种实现方式? —— 基于域名(看 Host)、基于 IP(看目标地址)、基于端口(看端口号),可以叠用。一台机器能装下多个站点之后,下一个问题是:那些夹在客户端和服务器中间的程序又是怎么工作的。
- ☐ (本节完,下一节 5.2.1 代理)