URI 是每一个程序员都应该了解的概念,同时相关联的还有 URL, URN 等概念簇。了解这些概念,能够帮助咱们更好地窥探万维网(WWW)的设计,同时也能帮咱们在工做中有效解决跟 URI 相关概念的问题,更加理解 encode,decode 工做原理,更好地助力网络编程!html
URI(Uniform Resource Identifier) ,意为统一资源标识符,提供了一套简单可扩展的方式对资源进行标识。java
为何会有 URI?
随着万维网的发展,须要有各类不一样类型的资源被在网络上查找以及传输。所以,也就须要一种惟一的可在万维网上传播的标识,这样的统一资源标识就称为 URI。固然,资源在这里是一种笼统概念,或者抽象概念,能够泛指能够被标识的实体,就像一个网页,一本e-book, 一份 pdf 等等,只要有须要被呈现或者传输,均可以称为一种资源。git
万维网奠定人Tim Berners-Lee关于超文本(hypertext)的提案中间接提出了用来标识超连接的想法–URL(Uniform Resource Locator)。所以,URL 也就最先被用来进行网络上能够提供访问的地址表示。随着HTTP, HTML 以及浏览器的逐步发展,愈来愈须要把标识资源可访问地址以及单出命名表示资源这两种方式分开,所以也就提出了 URN(Uniform Resource Name),并用来表示后者。程序员
IETF(网络工程任务小组)主要负责 URI 相关标准制订。web
了解到 URI 和 URL,URN 总体的历史,能够看出来最先 URI和 URL 实际上是一脉相源的。后来为了兼容单纯经过命名或者名称来标识某个资源(并非可被网络直接访达或者包含包含网络访问地址)的状况,提出了 URN标准。因而可知,这三个名称均可以表示对一项资源的定位标识。比较有意思的问题是,在日常的工做沟通中,如何区分,而且在什么样的场景下该使用哪一个名称?数据库
先具体了解每一个名称的基本概念:
1.URI
统一资源标识符。
用来表示某个特定资源。设计出来能够进行任何实体或者非实体的标识,可是目前被常常用于在网络上可传输内容的标识。URI 是由一串特定字符集的字符组成,而且由 IETF 制订的标准定义了一组语法规则,用来保证某个资源的统一和惟一标识。编程
2.URL
统一资源定位符。
也能够被称为网络地址。在万维网上,每一个资源都有能够有惟一地址指向该资源,同时,经过该地址能够进行资源的读写,这样的地址标识就称为 URL。URL 包含了目前网络上常见的格式,包括 web 站点地址 http, 文件传输协议ftp, emal 地址协议 mailto以及数据库访问地址 JDBC 等。浏览器
3.URN
统一资源名称。
URN用来经过名称标识在特定命名空间的某个资源,同时但愿为资源能够提供一种较持久的,与位置和存取方式无关的表示方式。URN 并不关注这个表示名称里是否隐含了该资源的位置,或者如何获取它,也不必定表明该资源必定可用。
举个例子,在ISBN(Internal Standard Book Number)系统中,一个编号(相似9971-5-0210-0)表明了一个书本资源,该编号在 URN 中能够表示为 urn:isbn:9971-5-0210-0, 可是这个编号并无给出在哪里或者如何找到这本书的信息,它只能惟一标识了这本书。安全
先上图来讲明 URI,URL 和 URN 之间的关系。
URI 能够认为是一个抽象的概念,全部的 URL 以及 URN 都是 URI。RFC3986标准中有这样一段:网络
A URI can be further classified as a locator, a name, or both. The term “Uniform Resource Locator” (URL) refers to the subset of URIs that, in addition to identifying a resource, provide a means of locating the resource by describing its primary access mechanism (e.g., its network “location”).
rfc 3986, section 1.1.3
URI 能够被分类成 locator 或者对应的名称表示,也就是包含了 URL 和 URN 的概念。所以,日常咱们在说 URL 的时候,它其实也能够被称为 URI。
一样,这里有个很是有意思的问题,URN 其实比较好区分开,在使用惟一标识资源名称时可使用,可是 URI 和 URL 如何区分在哪一个场景进行使用?
这个问题其实和 RFC3986标准定义的不够清楚有关,请再看下面这一段:
The URI itself only provides identification; access to the resource is neither guaranteed nor implied by the presence of a URI.rfc 3986, section 1.2.2
URI 不保证提供该资源的访问方式,或者隐含保证该资源是否存在(其实语义就是该 URI 就是一个名称表示),可是在上一段中又声明了URI 会被分类成name 或者 locator,表示 URI 应该包含locator 这种访问方式。再看下面这一段:
Each URI begins with a scheme name, as defined in Section 3.1, that refers to a specification for assigning identifiers within that scheme.rfc 3986, section 1.1.1
每一个 URI 都须要包含有起始 scheme 名称。好比:https://www.example.com,这样的一串字符串就能够称为 URI,可是明确标识了应该如何去访问这个资源,同时它也是 URL,由于 URL 是用来告知接收方获取该资源的方式。
IETF在RFC3986中也有一段关于 URI 和 URL 使用方式的说明:
Future specifications and related documentation should use the general term “URI” rather than the more restrictive terms “URL” and “URN”rfc 3986, section 1.1.3
这样看来,好像IETF 更支持使用 URI 来代替 URL 这个称呼。可是考虑到 URL 目前已经成为用来描述网络上资源定位的事实名称,并且 RFC3986已经诞生超过15年了(有些条目确实跟不上时代发展速度),因此在针对互联网资源定位(即网络地址)的时候,URL 能够算是更贴切的名称。固然,若是对方跟你谈 URI等等,这也没问题,由于 URI 算是超类,而且也能够表明该资源。
下面是这个问题结论:
URI 须要提供一种简单,可扩展的方式来惟一标识资源。同时,又须要考虑到在不一样媒介上进行传播的表示形式。所以,URI 在设计时须要考虑到如下几点:
不一样的系统,或者不一样的接收方之间均可以使用 URI 协议来标识资源。URI 能够被表示成多种形式,好比说在纸上书写的字符串,或者屏幕上的像素,或者一系列经过编码的二进制流等。URI 的解析只跟这些呈现方式所关联的字符串有关,而跟具体表现方式,载体无关。
考虑到 URI 更多须要在网络场景传输,所以:
基于上述考虑,URI 为一串受限的字符所组成的字符串,并选择 US-ASCII 做为字符集。US-ASCII 字符集基本上被全部系统支持,并且兼容性良好,可以支持 URI 所须要的移植性。
这一层思想实际上是须要将表示和表现分开。URI 只关注某个资源的标识,若是进行这个资源的存取或者访问不作任何方式的保证。同资源相关的动做,引用等,在设计时被交给具体实现 URI 下 scheme 的协议来制订,例如,http 协议会具体关心一个用’http’ scheme 表示的资源如何进行’get’, ‘update’,'delete’等一系列操做等。
这样能够保证 URI 协议的相对稳定,以及比较好的扩展性
因为资源常常具备层级关系,好比在一个 example.com 站点下可能会挂有多个资源,或者下面会有一个目录’dir’, 该目录下会包含多个资源,这就意味着URI 须要有一种层级的组织方式。
在设计中也考虑到了这样类型的资源组织方式,容许 URI 按照层级组织,而且在字符串上按照从左到右的顺序拆分组件。
相似于经常使用操做系统的文件系统同样,URI 能够用来还原具备层级关系的资源系统的组织结构。
如上所属,URI 选择 经过US-ASCII 字符集来进行表示,并限制使用从其中所挑选的一部分字符,数字以及符号。并且,因为须要支持层级结构,以及 URI 自身包含了不一样的部分,所以也须要保留一些字符用来作这些有语义的部分的分隔。
Note: 因为须要对字符集或者语法进行描述,下文都是用 IETF使用的通用描述系统ABNF(Augmented Backus-Naur form), 即加强巴科斯范式。
加强巴科斯范式所定义的语法结构通常以下:
rule = definition / definition; comment CR LF
rule = *element
表示一组规则由一系列字符串组成的定义来描述,第一组 rule经过’/‘来表示定义中’或者’的关系。若是该条规则须要增长注释,那么须要经过’;'来标识注释的开始
第二组 rule 表示重复规则,其中 a标识最少重复次数,b 标识最多重复次数。例如,2*3element标识 element 最少出现两次,最多出现三次
关于加强巴科斯范式的具体内容请参照:
https://en.wikipedia.org/wiki/Augmented_Backus%E2%80%93Naur_form
因为 URI 在协议中只挑选了部分ASCII 字符,数字以及符号,那么当须要表示不在这个范围以内的符号,字符,或者该字符在 URI 中被用来分隔符等特殊用途时,就须要对这个字符进行%编码。百分号编码也能够叫作URLEncode,其通常格式为:
pct-encoded = "%" HEXDIG HEXDIG
将不能直接使用的字符先转为字节流表示(通常为 utf-8编码,须要具体看上下文和 URI scheme 协议制订),而后每一个字节转换为%加两个十六进制字符来表示。例如:
“00101011” 该字节须要编码为 “%2B” ,在 ASCII 码表中表示为 "+"号
Note: 百分号编码不关心大小写,可是为了统一和一致,最好应该使用大写字符
URI 保留字符集。
URI 自身定义时包含了 components以及 subcomponents,那么这些不一样的 components 就须要经过分隔符来进行标识。这些被用来进行表示分隔的字符就成为保留字符集,这些字符集可能会被用做(或者未来会被用做)URI 不一样部分的分隔符。
如下为 reserved character 所涉及的字符集表示:
reserved = gen-delims / sub-delims gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
gen-delims 字符集用来表示 URI component 之间的分隔符,考虑到 component 内会由不一样的 subcomponents 组成,所以须要 sub-delims 字符集来定义 subcomponents之间的分隔符。
Note:这些字符在 URI 中通常具备特殊语义,所以不能被编码。同时,若是在进行两个 URI 相等性比较时,若是其中一个对协议中component 部分不能编码的保留字符进行编码,即便解码后两个 URI 字符相同,也会被认为是两个不一样的 URI
容许出如今URI 中,而且不会被拿来用做保留字符集的字符集合成为 Unreserved Characters。所涉及到字符ABNF 表示为:
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" ALPHA = a-z / A-Z DIGIT = 0-9
这些字符为非保留字符,在 URI 使用过程当中是不须要进行编码的。
Note: 若是在 URI 比较中包含这些字符,那么该字符自己或者其编码格式都应该认为是相等的,即这些字符编码不编码不会影响相等性。另外,这些字符在使用时最好不要编码,即便已经被编码,那么在使用时也应该先对这些字符进行解码。
一图来表示在 URI 中所涉及到的保留和非保留字符,须要注意的是保留字符在不作分隔符或者具备特殊含义的时候是须要编码的。
URI 语法规则由一系列 component 组成,而且在设计时须要考虑到扩展性以及对各个资源定位类型的兼容,所以在其起始都会有一个 scheme 头来特定标识这个 URI 所定义的资源类型标识符。另外,URI 因为是全部资源类型的超集(会细分为 URL 和 URN),因此 URI 所涉及的定义都是须要被遵照的基本定义。
URI component 通常由如下 component 组成(使用 ABNF 描述):
URI = scheme ":" [ //authority ] path [ "?" query ] [ "#" fragment ] authority = [ userinfo@ ] host [ :port ]
Note:
schme 和 path 为 required
有了上述语法规则的定义,举个例子来讲明 URI 下两种不一样的标识符所定义的各个 component 部分
下文将详细介绍各个组件部分,以及相应的语法规则。
component
scheme
容许字符集
a-z A-Z 0-9 + . -
是否 case-sensitive
否
component 结束标识符
:
Note:
- 表中字符集为了呈现清晰,所以正则中经过非必要空格进行分隔,而且表或者关系
- 结束标识符表示语法解析时该 component 解析结束符
scheme用来标识URI 所对应的具体协议。每一个 URI 都必须以 scheme 开头。URI 的语法规则以下(使用 ABNF 描述):
scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
如上文所说,URI 定义通用的语法规则,scheme 所标识的具体协议会定义通用规则外的具体语法规则。例如,以 geo 为scheme 的协议 URI,表示特定地理位置标识,其语法规则以下:
geo:<lat>,<lon>[<alt>][u=<uncertainty>]
参考自 RFC 5870
URI scheme 的官方注册信息目前由 IANA(Internet Assigned Numbers Authority) 组织进行添加和维护,目前约包含了335种不一样协议 scheme,具体可参考https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
component
Authority
component 开始标识符
//
component 结束标识符
/ ? #
authority component 设计的目的为设定一个命名空间,而且标识这个命名空间被哪一个机构所管理,例如 baidu.com, google.com 等等。authority 通常由三部分组成,包含了可选的 userinfo, port 以及必选的 host 部分。
关于为何 Authority 部分会选择 // 做为起始符号的缘由,Tim Berners-Lee 曾回答过:
因而可知,标准的设计也是须要再不断地迭代和试验中前进 :)
component
Userinfo
容许字符集
pct-encode字符集 unreserved字符集 sub-delims字符集 :
是否 case-sensitive
是
component 结束标识符
@
userinfo 包含了用户相关信息(通常为名称,旧式格式 user:password 因为涉及安全风险已被弃用),同时须要经过@符合和 host 进行分隔。Userinfo 部分的语法规则以下(使用 ABNF 描述):
userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
component
Host
容许字符集
pct-encode字符集 unreserved字符集 sub-delims字符集
是否 case-sensitive
否
component 结束标识符
/ :
服务提供商经过 host来提供服务,同时基于 dns 域名解析, server 和 host 之间能够作到非一一对应。host 部分能够有三种表示方式,IPv6, IPv4或者 registered name。registered name host的语法规则以下(经过 ABNF 描述):
host = IPv6address / IPv4address / reg-name IPv6address = [ HEXDIG *( :: HEXDIG ) ] IPv4address = DIGIT "." DIGIT "." DIGIT "." DIGIT reg-name = *( unreserved / pct-encoded / sub-delims )
component
Port
容许字符集
0-9
component 结束标识符
/
port 为可选项,同时经过十进制进行表示。在URI语法中,port 须要跟在 : 后。port 的语法规则以下(使用 ABNF 描述):
port = *DIGIT
每种 scheme 通常会定义一个默认端口。例如, http 定义80默认端口,https 定义443默认端口等。
component
Path
容许字符集
pct-encode字符集 unreserved字符集 sub-delims字符集 @ :
component 结束标识符
? # EOF
path标识了 host 下特定的资源路径,包含了一系列经过 / 分隔的 segments。须要注意的是,若是URI已经包含了 authority 部分,那么 path部分或者为空,或者须要以 / 来开头。另外,URI还容许 relative-path 的使用方式,这样的方式第一段 path segment 不能包含 :(若是包含,会被 parser 认为是 authority 部分)。如下是简化的 path 语法规则(使用 ABNF 描述):
path = path-abempty / path-relative path-abempty = *( "/" segment ) path-relative = segment-nocolon *( "/" segment ) segment = *pchar pchar = unreserved / pct-encoded / sub-delims / ":" / "@" segment-nocolon = unreserved / pct-encoded / sub-delims / "@"
component
Query
容许字符集
pct-encode字符集 unreserved字符集 sub-delims字符集 @ :
component 开始标识符
?
component 结束标识符
query 部分提供了定位资源的辅助信息,query其内部语法并无明肯定义,可是通常由name-value 键值对组成的字符串组成,中间经过分隔符 & 进行分隔。例如:name1=value1&name2=value2。query 的语法规则以下(使用 ABNF 描述):
query = *( pchar / "/" / "?" ) pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
component
Query
容许字符集
pct-encode字符集 unreserved字符集 sub-delims字符集 @ : / ?
component 开始标识符
component 结束标识符
EOF
fragment 为段落标识符,通常用来标识一个 resource 的特定部分(一个资源子集或者一部分,或者经过这个资源来描述的一些其余资源)。 fragment 以 # 做为起始标识符,其语法规则以下(经过 ABNF 描述):
fragment = *( pchar / "/" / "?" ) pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
各个component 容许的字符集部分是咱们须要特别关注的,须要注意在五个 component 之间容许使用 gen-delims 字符集,在每一个 component 内(即小组件间)容许使用 sub-delims 字符集。
如何经过程序来解析 URI, 并获得 URI 各个 component?
如上一节 ABNF 语法规则描述,URI 知足上下文无关文法。所以,咱们能够经过语法图来呈现总体 URI 的解析规则,以下:
有了上图,使用递归降低,解析的伪代码就很是好写了:
`/**
**/
function next() {
skip space;
read next char and return;
}
/**
**/
function contains(input, special_char) {
start = input.start, end = input.end;
while (start < end) then
if special_char equals start then return;
end
return start
}
/**
**/
function parse(string uri) {
parse_scheme;
skip next ';' ;
if next() == "//" then
if contains(substring_uri(// until path), '@') then parse_userinfo; end parse_host; if next() == ':' then parse_port; end
end
parse_path;
if next() == '?' then
parse_query;
end
if next() == '#' then
parse_fragment;
end
}`
何时该 encode 或者 decode?
先说 URI 的设计目的,URI 被设计出并可在万维网上进行普遍传播,所以对各个子系统,浏览器等媒介的兼容性是最重要的,所以被设计使用被普遍使用的 ASCII 码进行承载。
所以,在生成 URI 过程当中,应该先完成各个 componet 部分的编码,而后在联合 gen-delimiter 拼接成 URI。因为各个 scheme 的具体协议不一样,所以只有在生成 URI 的过程当中,才能够知道具体哪些 delimiter 会须要被编码,或者会被使用做为真正的 delimiter。一旦 URI 被生成,该 URI 在传播时就应该保持其 百分号 encode 的格式。
当百分号编码的 URI 在解码时,应该先经过 gen-delimiter 以及 sub-delimiter 将各个 component 进行分离,而后再对各个 component 进行分别解码。这样能够保证按照生成的 URI 被完整解码。
另外,须要注意的是,2.2.3中提到的 unreserved 字符集能够在任意时刻被编码和解码,可是推荐在生成 URI 时不对这些字符集进行编码,同时在解码时应该优先对这些字符集的百分号编码格式进行解码。
Note: 不该该对同一个 URI 重复次编码或者解码,这样会致使 URI所表明的语义失效。例如,对已经进行百分号编码的 URI 再进行编码时,又会再次对其中的百分号进行二次编码,从而致使 URI 在进行解码时含义错误。
按照上文的说法,encode 须要先根据对应的 component 部分来组成不须要进行 escape(即不须要编码) 字符的规则,而后再进行逐一的判断和编码,以后再将编码事后的 component 拼接称为 URI(固然,若是全部的 delimiter 都不须要进行编码,那能够直接对整个 URI 进行编码,不须要 escape 的字符集直接包含这些 delimiter 字符)。 decode 则须要先将各个 component 按照 delimiter 进行拆分,而后分别对各个 component 在须要解码的字符规则下进行解码。
Note: 在标识 ASCII 之外的字符集时,通常是用 Unicode 字符集,编码方式为 UTF-8。
所以,在编码和解码过程当中,若是编程语言层面使用 UTF-16进行字符编码(相似于 Java 和 JavaScript),那么须要将其转为 UTF-8编码,同时须要针对 UTF-16带来的 surrogate pair 进行额外处理。
关于surrogate pair 描述,能够参考
https://stackoverflow.com/questions/5903008/what-is-a-surrogate-pair-in-java#:~:text=The%20term%20%22surrogate%20pair%22%20refers,values%20between%200x0%20and%200x10FFFF.&text=This%20is%20done%20using%20pairs%20of%20code%20units%20known%20as%20surrogates.
encode 的实现中须要注意的就是对须要编码的字节进行%编码,伪代码以下:
`/**
*
**/
function encode(s, dontNeedEncodingSet) {
// 声明 R 为结果字符串
def R, index = 0, strLen = s.length();
while index < strLen then
def c 为 s 在 index 下的字符表示; if c 包含在 dontNeedEncodingSet 里 then R += c; else def 临时结果 out; /** * 这里须要考虑若是是 utf-16字符编码,那么须要判断 surrogate pair **/ if c 在 surrogate pair中的第一个字符所表示的范围内 then def c2 为 ++index 位置字符; 将 c c2两个字符组成 utf-16并进行 utf-8编码; 将上述结果赋值给 out; else 若是 c 为 utf-16编码,须要转为 utf-8编码; out = c; end // 核心百分号 encode 取 out 中每个字节 out_byte; R += '%' + ((out_byte >> 4) & 0xF)转为16进制大写表示 + ((out_byte) & 0xF)转为16进制大写表示; end ++index;
end
return R;
}`
decode 的实现中须要注意在遇到%号时读取后续字符进行解码,同时若是语言实现使用 utf-16编码那么须要对 surrogate pair 进行还原(这部分语言自己通常都提供方法来对 utf-8进行转换),伪代码以下:
`/**
**/
function decode(s) {
// 声明 R 为结果 string def R, index = 0, lenStr = s.length(); while index < lenStr then def c 为 s 在 index 下的字符表示; if c == '%' then def 中间临时结果 out; while c == '%' && index + 2 < lenStr then 读取index+1, index+2 字符 c1, c2; // 核心 decode out += (字符转为 hex 表示(c1)) << 4 | (字符转为 hex 表示(c2)); index += 3; end // 异常状况报错 if c == '%' && index < lenStr then 抛出错误; // 注意:若是语言实现须要 utf-16编码,那么须要先行将 out 转为 utf-16编码 R += out; else R += c; ++index; end end return R;
}`
相信各位已经对 URI 有了一个相对全面的了解,在实际工做的使用中,还须要根据语言所提供的对应 encode,decode 方法文档来进一步了解其编解码所定义的 component 部分特殊保留字符,这样会对所使用语言提供的 encode/decode 有更深刻的了解 :)
**
Enjoy your coding trip~
做者:王阳(好将来Java开发专家)