🎬 第十章 Nginx搭建直播平台
📄 一句话概括

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 秒实验

  1. 把 rtmp 块挪进 http 里,看树怎么变——它的层号变成 2,成了 http 的子块,这就是错的写法。
  2. 在 location 里加一条 add_header,想想 server 里那几条怎么了——全被覆盖掉,不是合并。
  3. 给解析器加上「引号内不切分」,想想为什么需要——因为有的参数值本身带分号或大括号。

常见误区

  1. 「nginx.conf 是平铺的键值对」——是树,层级决定含义。
  2. 「include 是一个块」——它是一条指令,把别的文件插进来。
  3. 「子块会继承并合并父块指令」——是覆盖,写一条就顶掉整组。
自测三问(点开对答案) 1. `rtmp` 块应该写在哪一层? —— **main 层,和 `http` 平级**,不能塞进 http 里。 2. 两个 `server` 块名字一样,怎么区分? —— **看它的父块**:http 下的是网站,rtmp 下的是流媒体服务实例。 3. `add_header` 在子块写一条会怎样? —— **父块那一组全被覆盖**,不是合并。

相关笔记