nginx.conf 不是一堆参数,是一棵树。 指令以分号结尾,块用大括号包住,层级决定一切。
10.3 讲的是怎么把 Nginx 编出来,从这一节开始讲怎么配。 10.4.2 讲指令能写在哪,10.4.3 专讲 RTMP 那一段。
一、四个层级
从外到内只有四层:
① main:最外层,不属于任何块。
worker_processes、error_log 都在这。
② events:事件模型相关。
worker_connections 在这。
③ http:所有 HTTP 相关的东西。
包括 server 与 location。
④ server / location:一个虚拟主机、一个路径规则。
RTMP 不在 http 里,它和 http 是并列的兄弟块。
这一点最重要:把 rtmp 塞进 http 里,启动直接失败。
很多人凭直觉写错,然后看到 unknown directive,
以为是模块没编进去,白折腾半天。
二、块名会重名,位置才是身份
一份同时开了 HTTP 与 RTMP 的配置里,有两个 server 块。
一个在 http 下,一个在 rtmp 下。
名字一样,含义完全不同:
http 下的 server 是「一个网站」,里面的 listen 听 HTTP 端口;
rtmp 下的 server 是「一个 RTMP 服务实例」,listen 听 1935。
所以判断一条配置对不对,不能只看指令名, 必须看「它在哪个块里」。 这也是为什么 Nginx 的报错会带行号——它知道出错位置,你照着跳过去看那一行的上下文就行。
三、两个反直觉的细节
① include 不是块,是一条指令。
它把另一个文件的内容原样插进来。
include mime.types; 插进来的是一张扩展名到类型的映射表。
所以被 include 的文件里能写什么,取决于它在哪一层被插进来。
② 指令的继承是「覆盖」不是「合并」。
写在 http 里的 sendfile on; 会被里面的 server 继承。
但只要子块里写了一条同类指令,父块那一整组就全部失效。
这一条在 add_header 上最容易踩:
你在 location 里加一条自定义头,server 里那几条全局头就全没了。
正确做法是在子块里把父块的也抄一遍,
或者用 include 把公共那部分抽出来。
四、动手 Demo:配置文件结构——它不是一堆参数,是一棵树
真写一个解析器,把配置切成块与指令, 再把整棵树按层号打出来。
def parse_conf(text):
tokens, buf = [], ""
for ch in text:
if ch in "{};":
if buf.strip():
tokens.append(("word", buf.strip()))
buf = ""
tokens.append((ch, ch))
else:
buf += ch
if buf.strip():
tokens.append(("word", buf.strip()))
root = {"name": "(root)", "dirs": [], "blocks": []}
stack, pend = [root], []
for kind, val in tokens:
if kind == "word":
pend.append(val)
elif kind == ";":
if pend:
stack[-1]["dirs"].append((pend[0], " ".join(pend[1:])))
pend = []
elif kind == "{":
blk = {"name": pend[0] if pend else "?",
"dirs": [], "blocks": []}
stack[-1]["blocks"].append(blk)
stack.append(blk)
pend = []
elif kind == "}":
if len(stack) > 1:
stack.pop()
pend = []
return root
def walk(node, depth=0, out=None):
out = [] if out is None else out
out.append((depth, node["name"], len(node["dirs"]), len(node["blocks"])))
for b in node["blocks"]:
walk(b, depth + 1, out)
return out
for depth, name, nd, nb in walk(parse_conf(CONF)):
print("%d %s%s 指令 %d 子块 %d"
% (depth, " " * depth, name, nd, nb))
实跑输出:
--------------------------------------------------------------------
配置文件结构——它不是一堆参数,是一棵树
--------------------------------------------------------------------
**① 先建立「树」这个心智模型**
**nginx.conf 里只有两种东西:指令和块。**
**指令以分号结尾,块用大括号包住一批指令。**
**所以整个文件天然是一棵树,不是一串平铺的键值。**
**四个层级,从外到内:**
**① main**:**最外层,`worker_processes`、`error_log` 在这。**
**② events**:**跟事件模型相关,`worker_connections` 在这。**
**③ http**:**所有 HTTP 相关的东西都在这,**
**包括 `server` 与 `location`。**
**④ server / location**:**一个虚拟主机、一个路径规则。**
**RTMP 不在 http 里,它和 http 是并列的兄弟块。**
**这一点特别容易搞错:把 `rtmp` 块塞进 `http` 里,直接启动失败。**
**② 真解析一遍,把树打出来**
**下面这份配置有 4 个块层、几十条指令。**
**解析器只做三件事:切词、遇 `{` 进一层、遇 `}` 退一层。**
层 块名 直属指令数 子块数
-----------------------------------------------------------
0 (root) 2 3
1 events 1 0
1 http 3 1
2 server 2 2
3 location / 2 0
3 location /live 1 0
1 rtmp 0 1
2 server 1 1
3 application live 4 0
**一共 8 个块、16 条直属指令。**
**看层号那一列:**
**http 在第 1 层,rtmp 也在第 1 层——它们是兄弟。**
**而 server 有两个,一个在 http 下、一个在 rtmp 下,**
**名字一样但含义完全不同。**
**这就是为什么「只看指令名」判断不了对错,必须看它在哪个块里。**
**③ 两个容易被忽略的细节**
**① `include` 不是块,是一条指令**,**它把另一个文件的内容原样插进来。**
**`include mime.types;` 插进来的是一堆 `types` 映射。**
**② 指令分「能继承」和「不能继承」两种。**
**写在 http 里的 `sendfile on;` 会被里面的 server 继承,**
**但 `listen` 这类只能在 server 里写,写在外面就报错。**
**继承这件事有个反直觉的地方:**
**只要在子块里写了一条同类指令,父块的那一整组就全部失效,**
**不是「合并」,是「覆盖」。**
**这在 `add_header` 上最容易踩:**
**你在 location 里加一条,server 里那几条就全没了。**
解析器只做三件事:切词、遇 { 进一层、遇 } 退一层。
层号那一列最有用,http 与 rtmp 都是第 1 层,所以是兄弟。
两个 server 层号不同,一个是 http 的子,一个是 rtmp 的子。
指令数和子块数是分开算的,这样能一眼看出哪个块最重。
30 秒实验
- 把
rtmp块挪进http里,看树怎么变——它的层号变成 2,成了 http 的子块,这就是错的写法。 - 在
location里加一条add_header,想想 server 里那几条怎么了——全被覆盖掉,不是合并。 - 给解析器加上「引号内不切分」,想想为什么需要——因为有的参数值本身带分号或大括号。
常见误区
- 「nginx.conf 是平铺的键值对」——是树,层级决定含义。
- 「
include是一个块」——它是一条指令,把别的文件插进来。 - 「子块会继承并合并父块指令」——是覆盖,写一条就顶掉整组。
自测三问(点开对答案)
1. `rtmp` 块应该写在哪一层? —— **main 层,和 `http` 平级**,不能塞进 http 里。 2. 两个 `server` 块名字一样,怎么区分? —— **看它的父块**:http 下的是网站,rtmp 下的是流媒体服务实例。 3. `add_header` 在子块写一条会怎样? —— **父块那一组全被覆盖**,不是合并。- ☐ (本节完,下一节 10.4.2 Nginx 配置文件的指令解析)