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開發專家)