px普贤教程PuXian
普贤教程 › 生态包

协议与归档实战

LDAP、SNMP、NTP、Whois、XML-RPC、SOAP、Memcached、Phar、uuencode、RSS/Atom、vCard、地理计算。这些包只做「协议包的构造与解析」,不负责收发——套接字、TLS、超时由调用方管。好处是纯函数、可离线测;代价是你得自己写那 30 行网络代码。

一个脚本跑完 12 个包

px
import ldap
import snmp
import ntp
import whois
import xmlrpc
import soap
import memcached
import phar
import uuencode
import rssbuilder
import vcard
import geo

def main():
    var bind = ldap_bind_request(1, "cn=admin,dc=example,dc=com", "secret")
    print("bind " + str(len(bind)) + " 字节 " + json_stringify(bind[0:6]))
    print(json_stringify(ldap_parse_dn("cn=甲,ou=people,dc=example,dc=com")))
    print(ldap_build_dn([{"type": "cn", "value": "甲"}, {"type": "dc", "value": "example"}]))

    var pkt = snmp_build_get_request("public", "1.3.6.1.2.1.1.1.0", 1001)
    print("GET " + str(len(pkt)) + " 字节 " + json_stringify(pkt[0:6]) + " " + snmp_version_name(1))

    var req = ntp_build_request()
    print("NTP 请求 " + str(len(req)) + " 字节 首字节=" + str(req[0]))
    print("纪元偏移 " + str(ntp_to_unix([0, 0])))
    print("offset=" + str(ntp_offset(1000.0, 1000.5, 1000.6, 1001.2)))
    print("roundtrip=" + str(ntp_roundtrip(1000.0, 1000.5, 1000.6, 1001.2)))

    print(whois_build_query("example.com").trim())
    print(whois_get_server("example.com") + " / " + whois_get_server("example.cn"))
    print(whois_extract_field("Domain Name: EXAMPLE.COM\nRegistrar: IANA\n", "Registrar"))

    var xr = xr_build_request("demo.add", [2, 3])
    print("XML-RPC " + str(len(xr)) + " 字节 含 methodName=" + str(xr.contains("methodName")))

    var env = soap_build_envelope("<m:Add><a>2</a><b>3</b></m:Add>", "")
    print("SOAP " + str(len(env)) + " 字节 body=" + soap_extract_body(env))

    print(json_stringify(mc_build_set("user:1", "甲", 0, 60)))
    print(json_stringify(mc_build_get_multi(["a", "b"])))
    print("值=" + str(mc_extract_value("VALUE user:1 0 3\r\nabc\r\nEND\r\n")))

    var p = phar_new("app.phar")
    phar_add_file(p, "index.px", "print(1)")
    phar_add_file(p, "lib/util.px", "def f(): return 1")
    phar_set_stub(p, "<?php __HALT_COMPILER();")
    print("Phar " + str(phar_file_count(p)) + " 个文件 " + str(phar_total_size(p)) + " 字节 lib=" + str(phar_has_file(p, "lib/util.px")))

    var enc = uu_encode(bytes("hello uuencode"))
    print("uuencode " + str(len(enc)) + " 字符")
    print("解码回 " + bytes_to_str(uu_decode(enc)) + ",前 6 字节=" + str(uu_to_list(uu_decode(enc))[0:6]))

    var feed = rss_new("晨曦日志", "https://cx.dzty.club/", "节点周报")
    rss_add_item(feed, "v1.0 验收", "https://cx.dzty.club/v100", "用例全绿", rss_format_date(now_sec()), "v100")
    var xml = rss_build(feed)
    print("RSS " + str(len(bytes(xml))) + " 字节 items=" + str(len(rss_parse(xml)["items"])))

    var card = vc_build_vcard({"fn": "张三", "email": "zhang@example.com", "tel": "+8613800000000", "org": "秀软"})
    print("vCard " + str(len(bytes(card))) + " 字节")
    print(json_stringify(vc_parse_vcard(card)))

    print(str(geo_haversine(39.9042, 116.4074, 31.2304, 121.4737)) + " km")
    print(str(geo_bearing(39.9042, 116.4074, 31.2304, 121.4737)))
    print(geo_geohash_encode(39.9042, 116.4074, 8))
    print(json_stringify(geo_wgs84_to_gcj02(39.9042, 116.4074)))
text
bind 43 字节 [2,1,1,96,2,1]
[{"type":"cn","value":"甲"},{"type":"ou","value":"people"},{"type":"dc","value":"example"},{"type":"dc","value":"com"}]
cn=甲,dc=example
GET 33 字节 [2,1,0,4,6,112] v2c
NTP 请求 48 字节 首字节=27
纪元偏移 -2208988800.0
offset=-0.05000000000001137
roundtrip=1.1000000000000227
example.com
whois.verisign-grs.com / whois.cnnic.cn
IANA
XML-RPC 180 字节 含 methodName=true
SOAP 161 字节 body=<m:Add><a>2</a><b>3</b></m:Add>
"set user:1 0 60 3\r\n甲\r\n"
"get a b\r\n"
值=abc
Phar 2 个文件 25 字节 lib=true
uuencode 45 字符
解码回 hello uuencode,前 6 字节=[104, 101, 108, 108, 111, 32]
RSS 432 字节 items=1
vCard 105 字节
{"fn":"张三","email":"zhang@example.com","tel":"+8613800000000","org":"秀软","title":"","note":""}
1067.3101709271293 km
153.0726752205012
wx4g0bm6
[39.9053,116.414]

ldap:BER 编码与 DN

px
ldap_bind_request(msg_id, "cn=admin,dc=example,dc=com", "secret")   # → list[int]
ldap_search_request(msg_id, "dc=example,dc=com", 2, "(uid=甲)")
ldap_parse_dn("cn=甲,ou=people,dc=example,dc=com")
# [{"type":"cn","value":"甲"}, …]
ldap_build_dn([{"type":"cn","value":"甲"}])    # cn=甲
parse_dn 返回的键是 type,不是 key往返时直接用 ldap_build_dn(ldap_parse_dn(x)) 即可。发出的字节是 BER(ASN.1),要真连 LDAP 服务器得自己开 TCP 389 写进去——包里没有网络层。

snmp:GET / GETNEXT

px
snmp_build_get_request("public", "1.3.6.1.2.1.1.1.0", 1001)   # → list[int]
snmp_build_getnext_request("public", "1.3.6.1.2.1.1", 1002)
snmp_version_name(1)    # v2c

OID 分量大于 127 时用多字节 BASE-128 编码,包已处理。解析方向只有 BER 解码的辅助函数,没有完整的 Response 解析器——拿回字节后要自己按 TLV 拆。

ntp:48 字节包与偏移计算

px
var req = ntp_build_request()        # 48 字节,首字节 0x1B(VN=4, Mode=3)
ntp_to_unix([0, 0])                  # -2208988800.0(1900 → 1970)
ntp_offset(t1, t2, t3, t4)           # 秒,正数=本机慢
ntp_roundtrip(t1, t2, t3, t4)        # 秒
ntp_parse_response(data, t1) 的 t4 算错了它把 t4 也设成传入的 t1,于是 roundtrip = (t4-t1) - (t3-t2) = -(t3-t2) 变成负数,offset 也少算半个往返。要么只用 ntp_offset / ntp_roundtrip 自己传四个时间戳,要么解析后把 t4 覆盖成收到响应时的 now_sec() 再重算。

whois:服务器推断与字段提取

px
whois_build_query("example.com")      # "example.com\r\n"
whois_get_server("example.com")       # whois.verisign-grs.com
whois_get_server("example.cn")        # whois.cnnic.cn
whois_extract_field(resp, "Registrar")

只覆盖常见 TLD(com/net/org/cn 等)。冷门后缀自己传服务器地址。whois_parse_response 返回的是键值 dict(重复键只留最后一个),要完整原文别用它。

xmlrpc / soap:构造与提取

px
xr_build_request("demo.add", [2, 3])        # XML 文本
xr_encode_value([1, "a"])                   # 单个值的 XML 片段

soap_build_envelope(body_xml, header_xml)   # 完整 envelope
soap_build_request("Add", [{"name":"a","value":"2"}], "urn:demo")
soap_extract_body(envelope)                 # 只取 Body 内容
soap_extract_fault(envelope)                # Fault → dict,正常 → null

两个都是字符串拼装,不做 XML 解析校验。要处理复杂响应(命名空间、编码)用 xmlstream 或 pxml。

memcached:文本协议

px
mc_build_set("user:1", "abc", 0, 60)   # set user:1 0 60 3\r\nabc\r\n
mc_build_get("k")                       # get k\r\n
mc_build_get_multi(["a","b"])
mc_parse_response(text)                 # → dict
mc_extract_value("VALUE k 0 3\r\nabc\r\nEND\r\n")   # abc
长度按字节算,中文值不会再写坏协议mc_build_set("user:1", "甲", 0, 60) 发出的是 set user:1 0 60 3 —— 声明 3 字节(UTF-8 实际长度),服务器读满 3 字节才收尾,缓冲区不会错位。二进制值用 mc_build_set_bytes(key, data, flags, exptime);读回时 mc_parse_full(text) 按声明的字节数精确切片,mc_extract_value 只取能转成文本的部分。

phar:归档清单

px
var p = phar_new("app.phar")
phar_add_file(p, "index.px", "print(1)")
phar_set_stub(p, "<?php __HALT_COMPILER();")
phar_list_files(p)        # ["index.px", …]
phar_build_manifest(p)    # 清单文本
phar_total_size(p)

构造的是 Phar 的清单结构,不是可直接运行的 phar 文件(没有 zlib 压缩、没有签名段)。解析方向能吃真实 phar 的 manifest 文本,用来做「包里有什么文件」的检查。

uuencode:编解码

px
var enc = uu_encode(bytes("hello uuencode"))   # 文本形态(带每行长度前缀)
var dec = uu_decode(enc)                        # bytes
bytes_to_str(dec)                               # hello uuencode
uu_to_list(dec)                                 # [104, 101, …] 要逐字节看时用
uu_decode_list(enc)                             # 旧形态:直接拿 list[int]
uu_encode_raw(bytes("x"))                       # 不带行首长度字符的裸编码
uu_decode 返回 bytes可以直接 bytes_to_str / bytes_to_hex;要 list[int] 用 uu_to_list(dec) 或 uu_decode_list(enc)。

rssbuilder:RSS 2.0 与 Atom

px
var feed = rss_new("标题", "https://site/", "描述")
rss_add_item(feed, "文章", "https://site/1", "摘要", rss_format_date(now_sec()), "guid-1")
var xml = rss_build(feed)
rss_parse(xml)["items"]        # → list[dict]

atom_new("标题", "https://site/", atom_format_date(now_sec()))
atom_add_entry(feed, "文章", "https://site/1", "id-1", atom_format_date(now_sec()))

日期格式:RSS 用 RFC 822(rss_format_date),Atom 用 ISO 8601(atom_format_date)。解析是逐标签字符串匹配,命名空间前缀变化可能认不出——自己站点的 feed 往返没问题。

vcard:联系人格式

px
var card = vc_build_vcard({"fn":"张三","email":"z@example.com","tel":"+861380…","org":"秀软"})
vc_parse_vcard(card)
# {"fn":"张三","email":"…","tel":"…","org":"…","title":"","note":""}
vc_fold(line) / vc_unfold(text)    # 75 字符行折叠与展开

行折叠、转义、参数(TEL;TYPE=CELL:)都实现了。iCalendar 复用同一套属性语法(vc_parse 不区分 VCARD/VCALENDAR 外层名)。

geo:距离、方位、坐标系、GeoHash

px
geo_haversine(39.9042, 116.4074, 31.2304, 121.4737)   # 1067.31 km
geo_bearing(…)                                        # 153.07 度
geo_geohash_encode(39.9042, 116.4074, 8)              # wx4g0bm6
geo_wgs84_to_gcj02(39.9042, 116.4074)                 # [39.9053,116.414](火星坐标)
geo_gcj02_to_bd09 / geo_bd09_to_gcj02 / geo_wgs84_to_bd09
geo_bbox_contains(bbox, lat, lon)
国内地图坐标要先转GPS 拿到的是 WGS84,高德/腾讯用 GCJ02,百度用 BD09。三套互转都在包里,六种组合齐全。距离用的是 haversine(球面),城市尺度误差可忽略,跨半球长距离误差可到 0.5%。