📚 第八章 应用协议
📄 一句话概括

LDAP 是一棵按名字组织的目录树,天生读多写少。 定位靠 DN,筛选靠过滤器,性能靠「范围写小」。

📝 本节和前面几节的关系

这是全书的最后一个应用协议。前面那些协议传的是「数据」,LDAP 传的是「关于人的数据」, 所以它的结构长得像一个组织架构图。 Demo 把 DN 拆开、把过滤器筛一遍、把三种搜索范围的代价摆出来。

一、目录不是数据库

LDAP 的祖先是一个叫 X.500 的标准,那套东西重到几乎没人实现得动, 于是有人做了一个「轻量版」,名字里的 L 就是 lightweight。 它和数据库的差别不是性能,是定位:

维度 目录(LDAP) 数据库
读写比例 读极多、写极少 读写都多
擅长 按名字查一条 任意条件组合查询
事务 没有 有,且很严格
表间关联 没有,只有一棵树 有外键和连接

「读多写少」这四个字决定了一切:一个人一天登录几十次,改一次手机号, 所以目录被优化成了「按名字取一条」的机器,代价就是放弃了事务和关联。 它的典型用途只有三类:登录认证、组织架构、设备清单。 共同点是「有唯一的名字,按名字查,很少改」—— 订单和库存完全没有这个形状,所以它们不在 LDAP 里。

二、DN:一条倒着写的路径

LDAP 里每个条目都有一个全局唯一的名字,叫 DN,可分辨名。 它的写法和文件路径正好相反:最左边是最具体的一层,最右边是最笼统的一层。 举个完整的例子:cn=张三,ou=研发部,dc=example,dc=com。 读起来是「人、部门、公司、顶级域」,而文件路径会写成 /com/example/研发部/张三。 一个是自下而上,一个是自上而下。 为什么要倒着写:因为这样从左往右读,就是「从叶子往根走」, 而每往右一层,范围就大一圈——这个顺序让「向上归并」变得自然,也正是权限继承需要的方向。 每一小段叫 RDN,dc 是域名组件、ou 是组织单位、cn 是通用名,uid 是账号名。 这几个前缀不是随便起的,是 schema 规定的,写错前缀服务器会直接拒绝。

三、过滤器与搜索范围

过滤器是 LDAP 的查询语言,它用前缀表达式: & 是与、| 是或、! 是非,全部写在括号最前面。 比如 (&(title=工程师)(cn=王五)) 就是「既是工程师、又叫王五」。 这个写法来自 Lisp 那一脉, 好处是不用记运算符优先级——括号已经把结构说清楚了,从左往右读就是。 代价是人读起来别扭,所以真实的查询通常由界面拼出来,而不是手写。 比过滤器更影响性能的是「搜索范围」:

范围 查多深 代价
base 只查这一个条目 最快,读一条属性
one 只查直接子节点 快,一层而已
sub 整个子树 慢,可能遍历几万条

第三行是 LDAP 性能问题的头号来源: 在 dc=example,dc=com 上做 sub 搜索,等于把整个目录扫一遍。 企业目录动辄十万条,这一下就是几十秒,而且是把服务器的内存和磁盘一起拖住。 所以有三条铁律:范围写小、过滤器写准、只要需要的属性。 第三条常被忘: 默认会回全部属性,而你往往只要一个邮箱——指定属性列表,能把返回的数据量砍掉一大半。 最后说一句现实: 你可能从没直接用过 LDAP,但一定用过它——Windows 域里的 AD 就是 LDAP 加 Kerberos, 公司的统一登录、邮件系统的通讯录,背后都是这棵树。

四、动手 Demo:LDAP 用「路径」找人,用「过滤器」筛人

把一个 DN 拆成四层,再拿五个过滤器筛三个条目,最后看三种搜索范围的代价。

dn = "cn=张三,ou=研发部,dc=example,dc=com"
for i, p in enumerate(dn.split(","), 1):
    print(i, p)

entries = {
    "cn=张三": "工程师",
    "cn=李四": "经理",
    "cn=王五": "工程师",
}
hit = [k for k, v in entries.items() if v == "工程师"]
print(len(hit), "、".join(hit))

实跑输出:

--------------------------------------------------------------------
LDAP 用「路径」找人,用「过滤器」筛人
--------------------------------------------------------------------
**LDAP 是一棵目录树**,**每个条目用一个 DN(可分辨名)定位,写法和文件路径反过来读。**
**下面把 DN 拆开看,再拿过滤器筛一遍。**

  层     属性     值          位置
  --------------------------------------------------------
  1      cn       张三        **第 1 级(从叶子往上数)**
  2      ou       研发部      **第 2 级(从叶子往上数)**
  3      dc       example     **第 3 级(从叶子往上数)**
  4      dc       com         **第 4 级(从叶子往上数)**

**注意顺序**:**最左边是「最具体」的一层(这个人),最右边是「最笼统」的一层(顶级域)。**
**所以 DN 读起来是「从人到公司」**,**而文件路径是从根往下写**,比如 `/com/example/研发部/张三`。
**两者正好相反。**
**这个「倒着写」的设计当初是为了让树能从上往下遍历。**

**第二件事是「过滤器」,也就是 search filter 的语法**:

  过滤器                           命中     命中的条目
  --------------------------------------------------------------------
  `(objectClass=person)`           3        cn=张三、cn=李四、cn=王五
  `(title=工程师)`                 2        cn=张三、cn=王五
  `(&(title=工程师)(cn=王五))`     1        cn=王五
  `(|(cn=李四)(cn=张三))`          2        cn=张三、cn=李四
  `(!(title=工程师))`              1        cn=李四

**第三到第五行是「前缀表达式」**:**`&` 是与、`|` 是或、`!` 是非,都写在括号最前面。**
**这个写法和 Lisp 一样,好处是不用考虑运算符优先级**——**括号已经把结构说清楚了。**

**第三件事是「搜索范围」,它比过滤器更影响性能**:

  范围         查多深                 代价
  -----------------------------------------------------------
  **base**     **只查这一个条目**     **最快,读一条属性**
  **one**      **只查直接子节点**     **快,一层**
  **sub**      **整个子树**           **慢,可能遍历几万条**

**第三行是 LDAP 性能问题的头号来源**:
**在 `dc=example,dc=com` 上做 sub 搜索,等于把整个目录扫一遍。**
**企业目录动辄十万条,这一下就是几十秒。**
**所以「范围写小、过滤器写准、只要需要的属性」是三条铁律。**

**最后说清它和数据库的区别**:**LDAP 是「读多写少」的目录**,
**它的强项是「按名字查一条」和「按属性筛一批」,弱项是事务和复杂关联。**
**所以它用在「登录认证、组织架构、设备清单」这类地方,不用在「订单、库存」这类地方。**

第一张表最该盯的是「位置」那一列:第 1 级是「张三」,第 4 级是「com」,越往右越笼统。 而文件路径的 /com/example/研发部/张三 是反过来的,同一个树,两种读法。 第二张表里第三到第五行是前缀表达式:&、|、! 都写在括号最前面,不看优先级。 第四行 (|(cn=李四)(cn=张三)) 命中两条, 顺序还和写的时候一样——LDAP 不保证返回顺序,要排序得在客户端做。 第三张表是这三张里最实用的:base 只读一条、one 只读一层、sub 读整棵子树, 三者的代价差着好几个数量级。 很多「登录变慢」的工单,最后查出来就是某段代码用了 sub。

30 秒实验

  1. 算一下:十万条、每条 1 KB,sub 全扫要读多少数据?(约 100 MB,而且是一条条读出来的)
  2. 想一下:为什么 LDAP 用 DN 而不是数字主键?
  3. 想一下:为什么「只要需要的属性」也能明显提速?

常见误区

  1. 以为「LDAP 是数据库」。 它是目录:读多写少、无事务、无关联,形状完全不同。
  2. 以为「DN 从左往右是从上往下」。 正好相反,最左边是最具体的叶子。
  3. 以为「sub 搜索最方便所以到处用」。 它是性能头号杀手,十万条的目录能扫出几十秒。
自测三问(点开对答案) 1. DN 应该怎么读? —— **从最具体读到最笼统**,也就是从人到公司,和文件路径相反。 2. `(&(a=1)(b=2))` 是什么意思? —— **两个条件同时满足**,`&` 是与,写在整个表达式的最前面。 3. LDAP 适合什么场景? —— **按名字查一条、按属性筛一批**:认证、组织架构、设备清单。

第八章到此结束。下一章讲网络安全:前面所有协议都假设「对方是善意的」, 最后一章把这个假设拿掉。

相关笔记