国际化域名编码

国际化域名编码（Punycode）是一种表示统一码和ASCII码的有限的字符集。例如中文“上海”会被编码为“fhqz97e”。

国际化域名编码的目的是在于国际化域名标签（IDNA）的框架中，使这些（多语言）的域名可以编码为ASCII。编码语法在文档 RFC 3492 中规定。

## 编码过程

RFC 3492中说明：国际化域名编码是一种称为Bootstring的更普遍的算法实例，它允许由部分基本的编码集合组成的字符串唯一表示由更大编码集合组成的任何字符串。配合统一码文本的特性，国际化域名编码定义了一般Bootstring算法的参数。本节以德语字符串"bücher"（书籍）为例，演示国际化域名编码的编码过程，该字符串被编码为"bcher-kva"。

### 分离ASCII字符

首先，字符串中的所有ASCII字符将被直接从输入复制到输出，任何其他字符将被跳过。例如，"bücher"被复制为 "bcher"。如果有任何字符被复制（即输入中至少有一个ASCII字符），则在输出中附加一个ASCII连字符（例如，"bücher"复制为"bcher-"，其中"ü"不是ASCII字符，不发生复制）。

由于连字符本身是ASCII字符，因此可以出现在输入中，并将被复制到输出中。这并不会引起歧义，因为如果输出包含连字符，被添加的连字符总是最后一个，这标志着ASCII字符的结束。

### 编码非ASCII字符

对于输入的非ASCII字符，依统一码中码位由低至高顺序，编码器会模拟将非ASCII字符一一插入回去除非ASCII字符后的字串过程。

计算每个非ASCII字符对应的三个数字i、n和h：

- i 表示该非ASCII字符在输入字符串中的以0为开始索引的插入位置（类似于如C语言等的编程语言，用0来表示该非ASCII字符是输入字符串的第一个字符）。
- n 表示该非ASCII字符在统一码中的码位，减去127（127为最后一个ASCII字符）
- h 表示该非ASCII字符插入时可能有的插入位置。如"ü"要插入回"bcher"中，可能有"übcher"到"bcherü"6个位置可供插入。

编码器随后计算乘法n*h+i并将得到的数字编码为一串36进制的可变长度数字，将这些数字转化为ASCII码，并将结果附加到输出字符串后。

每次计算后，该非ASCII字符会插入回去除非ASCII字符后的字串，因此h会增加1。

并且在计算下一个非ASCII字符的统一码码位时，该码位需减去上一个非ASCII字符码位。

编码方式为：0 -> 'a', ..., 25 -> 'z', 26 -> '0', ..., 35 -> '9', 数字的数字按小端序排列。

此处的36进制的编码并非通常意义下的下36进制整数，而是一种可变长度的整数。每个数字的最重要（most significant）的位（例如数字 "123 "中的 "1"）在没有上下文的情况下是可以识别的。因此，多个数字可以无需分隔地连接，不会影响原始数字的识别和提取。但此处每位数字进位的门槛阀值不同，并非每位皆是36进位。

### 用于国际化域名的ACE前缀

为了防止非国际域名中的连字符被国际化域名编码解码，会在在国际化域名中的国际化域名编码序列前加上字符串`xn--`。这被称为ACE（ASCII Compatible Encoding）。

因此，域名"bücher.tld"将会以ASCII形式表示为"xn--bcher-kva.tld"。

### 解码器

解码器是具有两个状态变量i和n的有限状态机。

i是字符串的索引，范围从0（代表可能插入的开始）到扩展字符串的当前长度（代表可能插入的结束）。i从0开始。

n从128开始（128表示第一个非ASCII的码位）。

状态递增是一个单调函数。一个状态转移要么增加i，如果i达到最大，就重置i为零并增加n。下一个状态转移时，继续增加i。在每个状态下，由n表示的码位要么被插入，要么不被插入。

编码器生成的数字代表在插入之前要跳过多少可能的结果。

在字符串 "bcher "中，有六个位置可以插入一个字符（包括第一个字符前和最后一个字符后）。在最后一个ASCII码位（127）和 "ü"（252，参见拉丁字母补充-1）之间有124个码位。有一个 "ü "的插入位置必须跳过（位置0，即在'b'之前）。

因此，在到达正确位置前，解码器将跳过共（6×124）+1=745个可能的插入位置。在该字符插入后，将有七个可能的地方插入下一个字符。

### 将码号重新编码为ASCII序列

国际化域名编码使用通用变长整数来表示这些值。例如，"kva "将用来表示码号745。

小端序的数字系统允许不需要单独的分隔符的变长编码：低于阈值的数字标志着它是最重要的数字，即是数字的结束。为了提高效率，阈值取决于数字的位置和以前的插入。同时，数字的权重也会变化。

在使用36个符号的数字系统中，不区分大小写的'a'-'z'表示十进制数字0-25，'0'到'9'表示十进制数字26-35。因此，"kva"，表示十进制数字序列"10 21 0"。

为了解码这串符号，需要阈值序列。本例中，阈值序列为（1，1，26，26，...）。最不重要的数字（least-significant digit）的权重（或位置值）总是1，具有权重1的'k'等于10（1×10=10）。下一个数字的权重取决于第一个阈值：一般来说，对于任意n，第n+1个数字的权重是第n个数字的权重乘以（36-第n个数字的阈值）。所以第二个符号的权重是36减去前一个阈值，即为35（36-1=35）。因此，前两个符号'k'和'v'之和为10×1+21×35。由于第二个符号不小于其阈值1，因此还有更多符号。然而，由于本例中第三个符号是'a'（a=0），可以忽略其权重的计算。因此，"kva "代表十进制数字（10×1）+（21×35）=745。阈值本身由一个算法为每个编码字符序列决定，使其保持在1和26之间（包括1和26）。字符的大小写可以用来提供关于原始字符串的大小写信息。由于特殊字符按法编码算的码位排序，在插入"bücher"中特殊字符"ü"时，第一种可能是编码为 "bcher-kvaa "的 "büücher"，第二种可能是编码为 "bcher-kvab "的 "bücüher"，等等等等。在编码为 "bcher-kvae "的 "bücherü "之后是代表插入ý的编码，即ü之后的统一码字符，从编码为 "bcher-kvaf "的 "ýbücher "开始（与编码为 "bcher-jvab "的 "übücher "不同），等等。

为了使编码、解码算法简单，编码时并没有试图阻止一些编码不允许的统一码值：然而，解码时应该检查这些值。

国际化域名编码的设计使其成可以在所有脚本中工作，并通过在操作过程中尝试适应字符串中的字符集范围进行自我优化。它针对字符串由包含0个或更多ASCII字符和来自其他脚本系统的字符组成的情况进行了优化，同时也能处理任意的统一码字符串。在使用DNS时需要注意，域名字符串被认为已经使用nameprep进行了规范化处理，并且顶级域名在被国际化域名编码之前被假设已经根据官方注册的语言表进行了处理。同时，DNS协议对输出国际化域名编码字符串的可接受长度也有所限制。

## 例子

下表展示了不同类型输入的国际化域名编码的例子。

输入｜输入的国际化域名编码｜对输入的描述

空字符

a｜a-｜仅有ASCII字符，一个小写字母

A｜A-｜仅有ASCII字符，一个大写字母

3｜3-｜仅有ASCII字符，一个数字

仅有ASCII字符，一个连字符

--｜仅有ASCII字符，两个连字符

London｜London-｜仅有ASCII字符，多于一个，没有连字符

Lloyd-Atkinson｜Lloyd-Atkinson-｜仅有ASCII字符，一个连字符

This has spaces｜This has spaces-｜仅有ASCII字符，包含空格

-> $1.00 $1.00 <--｜仅有ASCII字符，多种符号混合

а｜80a｜没有ASCII字符，一个西里尔字母

ü｜没有ASCII字符，一个拉丁字母补充-1中的字符

α｜没有ASCII字符，一个希腊字母

例｜没有ASCII字符，一个CJK字符

😉｜n28h｜没有ASCII字符，一个emoji字符

αβγ｜mxacd｜没有ASCII字符，多于一个字符

München｜Mnchen-3ya｜混合字符，包含一个非ASCII字符

Mnchen-3ya｜Mnchen-3ya-｜München的两次国际化域名编码

München-Ost｜Mnchen-Ost-9db｜混合字符串，包括一个非ASCII字符和一个连字符

Bahnhof München-Ost｜Bahnhof Mnchen-Ost-u6b｜混合字符串，包括一个空格，一个连字符和一个非ASCII字符

abæcdöef｜abcdef-qua4k｜混合字符串，包括两个非ASCII字符

правда｜80aafi6cg｜俄语字符，不包括ASCII字符

ยจฆฟคฏข｜22cdfh1b8fsa｜泰语字符，不包括ASCII字符

도메인｜hq1bm8jm9l｜韩语字符，不包括ASCII字符

ドメイン名例｜eckwd4c7cu47r2wf｜日语字符，不包括ASCII字符

MajiでKoiする5秒前｜MajiKoi5-783gue6qz075azm5e｜日语字符，以及ASCII字符

“bücher”｜bcher-kva8445foa｜混合的非ASCII字符（拉丁字母补充-1及CJK字符）