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):
如果从 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):
-
明确规范 (Clear Specification)
- 在协议 RFC 中明确要求使用 SRV 记录
- 定义 Service 和 Proto 字段的具体值
-
向后兼容 (Backward Compatibility)
- 考虑不支持 SRV 的旧客户端
- 提供回退机制
-
安全考虑 (Security Considerations)
- 评估 DNS 欺骗风险
- 考虑使用 DNSSEC
-
性能优化 (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):
-
标准流程 (Standard Flow)
- SRV 查询 → 排序 → 加权选择 → 连接
- 失败时回退到 A 记录查询
-
强制性要求 (Mandatory Requirements)
- 必须解析所有 RR
- 必须查询缺失的地址记录
- 不得使用端口号代替符号名称
-
故障处理 (Failure Handling)
- 截断响应: 使用 TCP 重试
- SRV 失败: 回退到 A 记录
- 连接失败: 尝试下一个服务器
-
性能优化 (Performance Optimization)
- 使用 Additional Data 部分
- 合理设置连接超时
- 实现连接池和重用