NDEF 报文格式详解:记录头、TNF 与常见记录解析

NDEF(NFC Data Exchange Format,NFC 数据交换格式)是 NFC Forum 定义的标签数据存储标准。无论你用手机碰一下 NTAG 贴纸,还是用读卡器读取 Type 4 卡片,应用层读到的都是一条 NDEF 报文(NDEF Message)——由一条或多条NDEF 记录(Record)串联而成的二进制序列。本文从原始字节层面完整拆解 NDEF:记录头五个标志位、TNF 类型名格式、Text/URI/Smart Poster 等常见记录的载荷结构、分块记录的重组规则,以及实际解析中最容易踩的坑。

一、NDEF 报文与记录的整体结构

一条 NDEF 报文就是若干条记录首尾相接。每条记录由三段组成:记录头 + 类型/ID + 载荷。记录头固定 2 字节起步:

判断报文边界的规则很简单:第一条记录 MB=1,最后一条记录 ME=1。解析器读到 ME=1 的记录即宣告报文结束。

二、逐位拆解记录头第 1 字节

记录头的第 1 字节是全部解析逻辑的起点,8 个位的含义如下:

Bit7  Bit6  Bit5  Bit4  Bit3  Bit2  Bit1  Bit0
 MB    ME    CF    --    SR    IL   TNF(3位)

用 Python 提取这些标志位只需几行:

def parse_header(byte):
    mb  = (byte >> 7) & 1   # Message Begin
    me  = (byte >> 6) & 1   # Message End
    cf  = (byte >> 5) & 1   # Chunk Flag
    sr  = (byte >> 3) & 1   # Short Record
    il  = (byte >> 2) & 1   # ID Length 存在
    tnf = byte & 0x07       # Type Name Format
    return mb, me, cf, sr, il, tnf

大多数市售标签上的简单内容(一条 URI 或一条文本)都是 SR=1、IL=0 的单记录报文,头部仅 3 字节。

三、TNF:类型名格式的 7 个取值

TNF 占 3 位,告诉解析器"Record Type 字段是什么体系"。NFC Forum RTD 规范定义了 7 个值:

TNF 值名称含义典型示例
0x00Empty空记录,无类型无载荷分块记录的首块占位
0x01Well-KnownNFC Forum RTD 定义的知名类型"T" 文本、"U" URI、"Sp" 智能海报
0x02MIME互联网媒体类型"text/plain"、"application/json"、"image/png"
0x03Absolute URI绝对 URI 型类型名"urn:nfc:ext:example.com:mytype"
0x04External厂商自定义外部类型"android.com:pkg"(Android 应用包名)
0x05Unknown未知类型的不透明数据私有协议载荷
0x06Unchanged分块记录的中间块与末块仅用于 Chunked 场景

实践中 90% 以上的标签只用到 TNF=0x01(Well-Known)与 TNF=0x02(MIME)两类;android.com:pkg 是 Android 应用启动记录(AAR)的 External 类型代表,写入后碰标签可直达指定应用。

四、Text 记录:状态字节 + 语言码 + 文本

Text 记录(TNF=0x01、Type="T")的载荷第一字节是状态字节:最高位表示编码(0=UTF-8,1=UTF-16),低 6 位是语言码长度(通常为 2,如 "en"、"zh")。状态字节之后依次是语言码与正文:

# Text 载荷结构:
# [状态字节] [语言码 (2-5 字节)] [正文 (UTF-8 或 UTF-16)]

def parse_text_payload(payload):
    status = payload[0]
    utf16 = (status >> 7) & 1          # 0=UTF-8, 1=UTF-16
    lang_len = status & 0x3F            # 语言码长度
    lang = payload[1:1+lang_len]        # 如 b"zh"
    text_bytes = payload[1+lang_len:]
    encoding = 'utf-16-be' if utf16 else 'utf-8'
    text = text_bytes.decode(encoding)
    return lang, text

# 示例:中文"你好NFC"(UTF-8, 语言码 zh)
# Hex: 02 7A 68 E4 BD A0 E5 A5 BD 4E 46 43
#        │  └zh┘  └── 你好NFC 的 UTF-8 字节 ──┘
#   状态字节 0x02 = UTF-8 + 语言码长度 2

注意 UTF-16 分支用的是UTF-16BE(大端),不是平台默认字节序——这是中文开发者用 Java 处理时最常见的编码错位点。

五、URI 记录:前缀缩写表

URI 记录(TNF=0x01、Type="U")载荷第一字节是前缀代码(0x00-0x24),把 "https://" 等常见 scheme 压缩成 1 字节,剩余部分是 URI 后缀。常用前缀如下:

代码前缀代码前缀
0x00(无缩写)0x04https://
0x01http://www.0x05tel:
0x02https://www.0x06mailto:
0x03http://0x23https://eid.me/ (示例)
def parse_uri_payload(payload):
    code = payload[0]
    prefix = PREFIXES.get(code, "")
    suffix = payload[1:].decode('utf-8')
    return prefix + suffix

# 示例:0x04 + "cupass.com" → "https://cupass.com"
# 完整报文(含头):
# D1 01 0C 55 04 63 75 70 61 73 73 2E 63 6F 6D
# │  │  │  └U─┘ └cupass.com──────┘
# │  │  └载荷长 12
# │  └类型长 1
# └D1 = MB+ME+SR, TNF=1

这个前缀缩写机制在容量紧张的 NTAG 213 上很有价值——每条 URI 记录可省 6-7 字节。

六、分块记录(Chunked Record)的重组

当单条记录载荷超过标签单次写入能力时,可用分块机制把载荷拆到多条记录中:

def reassemble_chunks(records):
    """把分块记录重组成完整载荷。"""
    payload = bytearray()
    record_type = None
    for rec in records:
        if rec.cf:
            if rec.mb:              # 首块:记录类型信息
                record_type = rec.type
            payload.extend(rec.payload)
        else:                       # 末块
            payload.extend(rec.payload)
    return record_type, bytes(payload)

实战建议:除非目标设备强制要求,尽量避免产生分块记录。许多手机 NDEF 栈对分块的支持并不完善,重组失败会表现为"标签读取为空"。

七、Smart Poster:报文里套报文

Smart Poster(Type="Sp")是唯一"载荷本身又是一条完整 NDEF 报文"的常见记录。它的载荷中至少包含一条 URI 记录,可再嵌可选的 Title(Text)、Icon(MIME)、Action 等记录:

def parse_smart_poster(payload):
    """Smart Poster 载荷本身是一条 NDEF 报文。"""
    records = parse_ndef_message(payload)
    uri, titles = None, {}
    for rec in records:
        if rec.type == b'U':
            uri = parse_uri_payload(rec.payload)
        elif rec.type == b'T':
            lang, text = parse_text_payload(rec.payload)
            titles[lang] = text
    return uri, titles

解析时注意递归:Smart Poster 内部的记录同样遵守 MB/ME 边界规则,与外层报文相互独立。

八、常见解析陷阱排查表

现象原因处理
第一条记录没有 MB 标志标签写入被中断,数据损坏原始 dump 场景可按"首记录默认 MB=1"容错处理
载荷 >255 字节但 SR=1写入方编码错误按 SR=0 的 4 字节长度重新解析
文本解码出现乱码状态字节 UTF-16 位与实际编码不符解码前校验状态字节 bit7;UTF-16 必须用大端
分块重组后类型丢失中间块 TNF 不是 Unchanged校验首块 TNF,其余块必须为 0x06
手机读不到内容NDEF TLV 外壳缺失或长度错误检查数据区是否以 0x03 [长度] [NDEF] 0xFE 包裹
最后一行是高频问题:NDEF 报文在 Type 2 标签(NTAG、Ultralight)上并非裸存,而是包在 NDEF Message TLV 外壳里(T=0x03,L=报文长度,V=NDEF 字节流,末尾 0xFE 结束符)。手机读不到内容时,先确认 TLV 外壳与长度字段是否正确。

九、动手验证与相关工具

读到这里,建议拿一条真实标签的 hex dump 亲手拆一遍。本站工具可以全程辅助:编码转换工具箱 做字节级十六进制转换与 Base64 对照,APDU 状态码速查 处理 Type 4 卡读取 NDEF 时的状态字,TLV 解析器 拆解 NDEF 外层的 TLV 结构。

想继续深入 NFC 主题,推荐阅读本知识库的《NFC 标签类型识别:ATQA、SAK、ATS 完全解读》《如何向 NFC 标签写入 NDEF》《MIFARE Classic 存取控制位详解》