Skip to main content

6. 使用规则 (Usage Rules)

6.1 客户端处理流程 (Client Processing Flow)

6.1.1 标准处理程序 (Standard Processing Procedure)

规范要求

支持 SRV 的客户端 (SRV-cognizant client) 应该 (SHOULD) 使用以下程序来定位服务器列表并连接到首选服务器。


6.2 完整算法流程 (Complete Algorithm Flow)

步骤 1: 执行 SRV 查询 (Step 1: Perform SRV Query)

查询参数 (Query Parameters):

QNAME  = _service._protocol.target
QCLASS = IN
QTYPE = SRV

示例查询 (Example Query):

dig _ldap._tcp.example.com SRV

步骤 2: 检查查询响应 (Step 2: Check Query Response)

响应条件检查 (Response Condition Check):

IF (回复是 NOERROR) 
AND (ANCOUNT > 0)
AND (至少有一个 SRV RR 指定所请求的 Service 和 Protocol)
THEN
→ 继续步骤 3 (处理 SRV 记录)
ELSE
→ 跳转到步骤 8 (回退到 A 记录查询)

响应码解释 (Response Code Interpretation):

响应码含义客户端行为
NOERROR查询成功检查 ANCOUNT
NXDOMAIN域名不存在回退到 A 记录查询
SERVFAIL服务器失败重试或回退

步骤 3: 检查特殊情况 (Step 3: Check Special Cases)

唯一根域记录检查 (Single Root Domain Record Check):

IF (恰好有一个 SRV RR)
AND (其 Target 是 "." - 根域)
THEN
→ 中止 (abort)
→ 该服务在此域中明确不可用

示例场景 (Example Scenario):

; 查询
_ftp._tcp.example.com.

; 响应
_ftp._tcp.example.com. IN SRV 0 0 0 .

; 客户端行为
→ 停止处理,不尝试任何连接
→ 向用户报告: "FTP 服务在 example.com 不可用"
明确拒绝服务

Target = "." 是管理员明确声明服务不可用的方式,客户端必须遵守此声明。


步骤 4: 构建候选列表 (Step 4: Build Candidate List)

数据结构 (Data Structure):

# 伪代码
candidates = []
for each SRV_RR in response:
if SRV_RR.service == requested_service and \
SRV_RR.protocol == requested_protocol:
candidates.append((SRV_RR.priority, SRV_RR.weight, SRV_RR.target))

示例数据 (Example Data):

; SRV 记录
_http._tcp.example.com. IN SRV 0 60 80 web1.example.com.
_http._tcp.example.com. IN SRV 0 40 80 web2.example.com.
_http._tcp.example.com. IN SRV 10 100 8080 backup.example.com.

; 构建的候选列表
candidates = [
(0, 60, "web1.example.com"),
(0, 40, "web2.example.com"),
(10, 100, "backup.example.com")
]

步骤 5: 按优先级排序 (Step 5: Sort by Priority)

排序规则 (Sorting Rule):

按 Priority 排序 (最小数字在前)
sort(candidates, key=lambda x: x[0])

排序后结果 (Sorted Result):

sorted_candidates = [
(0, 60, "web1.example.com"), # Priority 0
(0, 40, "web2.example.com"), # Priority 0
(10, 100, "backup.example.com") # Priority 10
]

步骤 6: 创建优先级分组 (Step 6: Create Priority Groups)

分组逻辑 (Grouping Logic):

priority_groups = {}
for (priority, weight, target) in sorted_candidates:
if priority not in priority_groups:
priority_groups[priority] = []
priority_groups[priority].append((weight, target))

分组结果 (Grouped Result):

priority_groups = {
0: [(60, "web1.example.com"), (40, "web2.example.com")],
10: [(100, "backup.example.com")]
}

步骤 7: 使用 Weight 算法排序每个优先级组 (Step 7: Order Each Priority Level by Weight)

算法引用 (Algorithm Reference):

算法链接

使用在 "SRV 资源记录的格式" (The Format of the SRV RR) 章节中 Weight 部分描述的算法。

详细算法步骤 (Detailed Algorithm Steps):

final_ordered_list = []

for priority in sorted(priority_groups.keys()):
current_level = priority_groups[priority].copy()

while current_level:
# 1. 将 weight=0 的记录移到前面
zero_weight = [item for item in current_level if item[0] == 0]
non_zero_weight = [item for item in current_level if item[0] != 0]
ordered_items = zero_weight + non_zero_weight

# 2. 计算总权重
total_weight = sum(weight for weight, _ in ordered_items)

if total_weight == 0:
# 所有权重都是 0,均匀随机选择
selected = random.choice(ordered_items)
else:
# 3. 计算累计权重
running_sum = 0
running_sums = []
for weight, target in ordered_items:
running_sum += weight
running_sums.append((running_sum, weight, target))

# 4. 生成随机数
rand_num = random.randint(0, total_weight)

# 5. 选择目标
for running_sum, weight, target in running_sums:
if rand_num <= running_sum:
selected = (weight, target)
break

# 6. 添加到最终列表并移除
final_ordered_list.append(selected[1])
current_level.remove(selected)

执行示例 (Execution Example):

初始状态 (Priority 0):
web1.example.com (weight=60)
web2.example.com (weight=40)

第一次迭代:
总权重 W = 100
累计权重: web1=60, web2=100
随机数 R = 73 (例如)
选择: web2 (因为 60 < 73 ≤ 100)
结果: [web2.example.com]

第二次迭代:
剩余: web1.example.com (weight=60)
选择: web1
结果: [web2.example.com, web1.example.com]

处理 Priority 10:
backup.example.com (唯一选择)
结果: [web2.example.com, web1.example.com, backup.example.com]

步骤 8: 查询地址记录并连接 (Step 8: Query Address Records and Connect)

对于有序列表中的每个元素 (For Each Element in Ordered List):

8.1 获取地址记录 (Obtain Address Records)

方法 A: 使用 Additional Data 部分 (Use Additional Data Section)

; SRV 响应可能包含 Additional Data
;; ANSWER SECTION:
_http._tcp.example.com. 3600 IN SRV 0 100 80 web1.example.com.

;; ADDITIONAL SECTION:
web1.example.com. 3600 IN A 192.0.2.10
web1.example.com. 3600 IN AAAA 2001:db8::10
性能优化

如果 Additional Data 部分包含地址记录,直接使用,避免额外的 DNS 查询。

方法 B: 执行新的 DNS 查询 (Perform New DNS Query)

; 如果 Additional Data 不包含地址记录
dig web1.example.com A
dig web1.example.com AAAA

8.2 尝试连接 (Attempt Connection)

连接逻辑 (Connection Logic):

for target in final_ordered_list:
# 获取地址记录
addresses = get_address_records(target)

for address in addresses:
try:
# 尝试连接 (protocol, address, service)
connection = connect(protocol, address, port)
if connection.successful():
return connection # 连接成功
except ConnectionError:
continue # 尝试下一个地址

# 如果所有地址都失败,尝试下一个 target

示例执行 (Example Execution):

尝试 web2.example.com:
1. 查询 A 记录 → 192.0.2.11
2. 连接到 192.0.2.11:80
3. 成功 → 返回连接

如果失败:
尝试 web1.example.com:
1. 查询 A 记录 → 192.0.2.10
2. 连接到 192.0.2.10:80
3. 成功 → 返回连接

如果仍然失败:
尝试 backup.example.com:
1. 查询 A 记录 → 192.0.2.20
2. 连接到 192.0.2.20:8080
3. 成功/失败

步骤 9: 回退到 A 记录查询 (Step 9: Fallback to A Record Query)

触发条件 (Trigger Conditions):

  • SRV 查询失败 (NXDOMAIN, SERVFAIL)
  • SRV 查询返回 0 条记录
  • SRV 查询返回的记录不匹配请求的 Service/Protocol

回退程序 (Fallback Procedure):

QNAME  = target
QCLASS = IN
QTYPE = A (或 AAAA)

示例 (Example):

# 原始请求
service = "_http._tcp.example.com"

# SRV 查询失败,回退
target = "example.com" # 从原始请求中提取域名
addresses = query_dns(target, "A")

# 尝试连接标准端口
for address in addresses:
try:
connection = connect("http", address, 80) # 使用默认端口
if connection.successful():
return connection
except ConnectionError:
continue
限制

回退到 A 记录查询时,客户端只能使用协议的标准端口 (standard port),无法获取自定义端口信息。


6.3 特殊规则与注意事项 (Special Rules and Considerations)

6.3.1 禁止使用端口号代替符号名称 (Prohibition of Port Numbers in Place of Symbolic Names)

强制性规则

端口号不应该 (SHOULD NOT) 用来代替符号服务或协议名称。

原因 (Rationale):

应用程序将不得不进行两次或更多次查找 (applications would have to do two or more lookups)。

错误示例 (Wrong Example):

; ❌ 错误: 使用端口号
_80._tcp.example.com. IN SRV 0 100 80 web.example.com.

; ✅ 正确: 使用符号名称
_http._tcp.example.com. IN SRV 0 100 80 web.example.com.

变体名称问题 (Variant Name Issues):

; ❌ 不允许使用变体名称
_www._tcp.example.com. IN SRV 0 100 80 web.example.com.
_web._tcp.example.com. IN SRV 0 100 80 web.example.com.

; ✅ 必须使用标准名称
_http._tcp.example.com. IN SRV 0 100 80 web.example.com.

6.3.2 截断响应处理 (Truncated Response Handling)

规则 (Rule):

RFC 2181 遵从

如果从 SRV 查询返回截断的响应 (truncated response),则应应用 [RFC 2181] 中描述的规则。

RFC 2181 规定 (RFC 2181 Requirements):

截断标志 (TC bit) 设置时:
1. 客户端应该通过 TCP 重新查询
2. 或者使用部分响应 (如果可接受)
3. 不应假设响应是完整的

处理示例 (Handling Example):

response = dns_query_udp("_http._tcp.example.com", "SRV")

if response.truncated:
# 方法 1: 通过 TCP 重新查询
response = dns_query_tcp("_http._tcp.example.com", "SRV")

# 方法 2: 使用 EDNS0 增加 UDP 响应大小
response = dns_query_udp_edns("_http._tcp.example.com", "SRV", bufsize=4096)

6.3.3 完整解析所有资源记录 (Complete Parsing of All RRs)

强制性要求

客户端必须 (MUST) 解析回复中的所有资源记录。

原因 (Rationale):

  • 确保不会遗漏任何可用的服务器
  • 正确实现优先级和权重机制
  • 获取完整的故障转移选项

错误示例 (Wrong Example):

# ❌ 错误: 只处理第一条记录
first_srv = parse_first_srv_record(response)
connect_to(first_srv.target, first_srv.port)

# ✅ 正确: 处理所有记录
all_srvs = parse_all_srv_records(response)
ordered_list = order_by_priority_and_weight(all_srvs)
for srv in ordered_list:
if try_connect(srv.target, srv.port):
break

6.3.4 Additional Data 部分的处理 (Handling of Additional Data Section)

规则 (Rule):

条件性查询

如果 Additional Data 部分不包含所有 SRV 资源记录的地址记录,并且客户端可能想要连接到所涉及的目标主机,则客户端必须 (MUST) 查找地址记录。

常见场景 (Common Scenarios):

场景 1: 地址记录 TTL 较短
SRV RR TTL: 86400 (24 小时)
A RR TTL: 300 (5 分钟)

→ A 记录过期后,Additional Data 可能不包含它
→ 客户端必须单独查询

场景 2: DNS 响应大小限制
SRV RR: 多条记录
A RR: 因大小限制未包含在 Additional Data

→ 客户端必须单独查询缺失的地址记录

实现示例 (Implementation Example):

def get_all_addresses(srv_records, additional_data):
addresses = {}

for srv in srv_records:
target = srv.target

# 首先尝试从 Additional Data 获取
if target in additional_data:
addresses[target] = additional_data[target]
else:
# 如果不存在,执行单独查询
addresses[target] = query_dns(target, ["A", "AAAA"])

return addresses

6.3.5 未来协议的设计考虑 (Design Considerations for Future Protocols)

建议 (Recommendation):

协议设计指南

未来的协议可以 (could) 设计为使用 SRV 资源记录查找作为客户端定位其服务器的方式。

设计要点 (Design Points):

  1. 明确规范 (Clear Specification)

    • 在协议 RFC 中明确要求使用 SRV 记录
    • 定义 Service 和 Proto 字段的具体值
  2. 向后兼容 (Backward Compatibility)

    • 考虑不支持 SRV 的旧客户端
    • 提供回退机制
  3. 安全考虑 (Security Considerations)

    • 评估 DNS 欺骗风险
    • 考虑使用 DNSSEC
  4. 性能优化 (Performance Optimization)

    • 合理设置 TTL
    • 利用 Additional Data 部分

协议设计示例 (Protocol Design Example):

新协议 "MyProtocol" 的 SRV 使用规范:

1. Service 名称: _myprotocol
2. Proto 名称: _tcp
3. 默认端口: 9999
4. SRV 记录格式:
_myprotocol._tcp.domain.com. IN SRV Priority Weight 9999 server.domain.com.

5. 客户端行为:
- 必须首先尝试 SRV 查询
- 如果失败,回退到 A/AAAA 查询 + 默认端口
- 必须支持优先级和权重机制

6. 安全要求:
- 推荐使用 DNSSEC 验证 SRV 记录
- 支持 TLS/SSL 加密连接

6.4 完整实现示例 (Complete Implementation Example)

Python 实现 (Python Implementation)

import dns.resolver
import socket
import random

class SRVClient:
def __init__(self, service, protocol, domain):
self.service = service
self.protocol = protocol
self.domain = domain
self.query_name = f"_{service}._{protocol}.{domain}"

def connect(self):
"""主连接方法"""
try:
# 步骤 1: 尝试 SRV 查询
servers = self._query_srv()
if servers:
return self._connect_to_servers(servers)
except Exception as e:
print(f"SRV 查询失败: {e}")

# 步骤 9: 回退到 A 记录
return self._fallback_connect()

def _query_srv(self):
"""执行 SRV 查询并排序"""
# 步骤 1: DNS 查询
answers = dns.resolver.resolve(self.query_name, 'SRV')

# 步骤 2: 检查响应
if not answers:
return None

# 步骤 3: 检查特殊情况
if len(answers) == 1 and answers[0].target == dns.name.root:
raise Exception("服务明确不可用")

# 步骤 4: 构建候选列表
candidates = [(rdata.priority, rdata.weight, rdata.port, str(rdata.target))
for rdata in answers]

# 步骤 5-7: 排序和加权选择
return self._order_servers(candidates)

def _order_servers(self, candidates):
"""按优先级和权重排序服务器"""
# 步骤 5: 按优先级分组
priority_groups = {}
for priority, weight, port, target in candidates:
if priority not in priority_groups:
priority_groups[priority] = []
priority_groups[priority].append((weight, port, target))

# 步骤 6-7: 对每个优先级组应用权重算法
ordered = []
for priority in sorted(priority_groups.keys()):
group = priority_groups[priority][:]

while group:
# 权重为 0 的记录放在前面
zero_weight = [item for item in group if item[0] == 0]
non_zero = [item for item in group if item[0] != 0]
ordered_group = zero_weight + non_zero

# 计算总权重
total_weight = sum(w for w, _, _ in ordered_group)

if total_weight == 0:
selected = random.choice(ordered_group)
else:
# 加权随机选择
rand = random.randint(0, total_weight)
running_sum = 0
for weight, port, target in ordered_group:
running_sum += weight
if rand <= running_sum:
selected = (weight, port, target)
break

ordered.append((selected[1], selected[2])) # (port, target)
group.remove(selected)

return ordered

def _connect_to_servers(self, servers):
"""尝试连接服务器列表"""
# 步骤 8: 遍历服务器列表
for port, target in servers:
try:
# 查询地址记录
addresses = self._get_addresses(target)

# 尝试每个地址
for address in addresses:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((address, port))
print(f"成功连接到 {address}:{port}")
return sock
except socket.error:
continue
except Exception:
continue

raise Exception("所有服务器连接失败")

def _get_addresses(self, target):
"""获取目标的地址记录"""
addresses = []
try:
answers = dns.resolver.resolve(target, 'A')
addresses.extend([str(rdata) for rdata in answers])
except:
pass
return addresses

def _fallback_connect(self):
"""回退到 A 记录查询"""
try:
addresses = self._get_addresses(self.domain)
default_port = self._get_default_port()

for address in addresses:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((address, default_port))
print(f"回退连接成功: {address}:{default_port}")
return sock
except socket.error:
continue
except Exception:
pass

raise Exception("回退连接失败")

def _get_default_port(self):
"""获取协议的默认端口"""
port_map = {
'http': 80,
'https': 443,
'ldap': 389,
'ldaps': 636
}
return port_map.get(self.service, 80)

# 使用示例
if __name__ == "__main__":
client = SRVClient("http", "tcp", "example.com")
try:
connection = client.connect()
print("连接建立成功")
except Exception as e:
print(f"连接失败: {e}")

6.5 本章小结 (Chapter Summary)

使用规则的核心要点 (Key Points):

  1. 标准流程 (Standard Flow)

    • SRV 查询 → 排序 → 加权选择 → 连接
    • 失败时回退到 A 记录查询
  2. 强制性要求 (Mandatory Requirements)

    • 必须解析所有 RR
    • 必须查询缺失的地址记录
    • 不得使用端口号代替符号名称
  3. 故障处理 (Failure Handling)

    • 截断响应: 使用 TCP 重试
    • SRV 失败: 回退到 A 记录
    • 连接失败: 尝试下一个服务器
  4. 性能优化 (Performance Optimization)

    • 使用 Additional Data 部分
    • 合理设置连接超时
    • 实现连接池和重用

导航 (Navigation)