程式設計師都應該知道的URI,一文幫你全面瞭解

好未來技術團隊發表於2020-11-08

URI 是每個程式設計師都應該瞭解的概念,同時相關聯的還有 URL, URN 等概念簇。瞭解這些概念,可以幫助我們更好地窺探全球資訊網(WWW)的設計,同時也能幫我們在工作中有效解決跟 URI 相關概念的問題,更加理解 encode,decode 工作原理,更好地助力網路程式設計!

1.URI

URI(Uniform Resource Identifier) ,意為統一資源識別符號,提供了一套簡單可擴充套件的方式對資源進行標識。

1.1 URI 的前世今生

為什麼會有 URI?
隨著全球資訊網的發展,需要有各種不同型別的資源被在網路上查詢以及傳輸。因此,也就需要一種唯一的可在全球資訊網上傳播的標識,這樣的統一資源標識就稱為 URI。當然,資源在這裡是一種籠統概念,或者抽象概念,可以泛指可以被標識的實體,就像一個網頁,一本e-book, 一份 pdf 等等,只要有需要被呈現或者傳輸,都可以稱為一種資源。

全球資訊網奠基人Tim Berners-Lee關於超文字(hypertext)的提案中間接提出了用來標識超連結的想法–URL(Uniform Resource Locator)。因此,URL 也就最早被用來進行網路上可以提供訪問的地址表示。隨著HTTP, HTML 以及瀏覽器的逐步發展,越來越需要把標識資源可訪問地址以及單出命名錶示資源這兩種方式分開,因此也就提出了 URN(Uniform Resource Name),並用來表示後者。
1601481963133af1d4cf9300e41299cdd8e76d4ce49a3.png

IETF(網路工程任務小組)主要負責 URI 相關標準制訂。
160151617244204c7ab6d7b584c66aaa5fb4ee94d6e20.png

  • 1994年釋出RFC1630, 指出了 URL 和 URN 的存在,同時定義了 URI 的正式語法。
  • 94年12月,RFC1738正式提出了 absolute 和relative URL, RFC2141則補充了URN 相關的文法和語法定義。
  • 1999年的 RFC2732允許 URI 使用 IPv6地址
  • 在2005年釋出的RFC3986標準,解決了上述標準提出的一些短板,同時標誌著URI 通用語法正式稱為官方網際網路協議
  • RFC3305標準指出,雖然 URL 名詞被廣泛使用,但是其本身可能被逐漸廢棄,並且只用來做為一些 URI 作為間接提供該資源訪問地址的提示。並且指出資源識別符號不需要表示該資源通過網路的訪問地址,或者根本不需要隱含該資源是基於網路提供的。(這裡相當矛盾,其實 URL 已經作為民間事實標準並被廣泛使用,也不是標準想推翻就能立刻推翻的 - -)

1.2 URI 和 URL,URN 比較

瞭解到 URI 和 URL,URN 整體的歷史,可以看出來最早 URI和 URL 其實是一脈相源的。後來為了相容單純通過命名或者名稱來標識某個資源(並不是可被網路直接訪達或者包含包含網路訪問地址)的情況,提出了 URN標準。由此可見,這三個名稱都可以表示對一項資源的定位標識。比較有意思的問題是,在平常的工作溝通中,如何區分,並且在什麼樣的場景下該使用哪個名稱?
160281482081581bfcde4ae77465ca7966aa28b6e005c.png

1.2.1 基本概念

先具體瞭解每個名稱的基本概念:
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, 但是這個編號並沒有給出在哪裡或者如何找到這本書的資訊,它只能唯一標識了這本書。

1.2.1 三者之間的關係

先上圖來說明 URI,URL 和 URN 之間的關係。
1601520706993142c193f0da14e8bb6acaf3b173cabf2.png
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 是一種標記符
  • URL 是可以告訴你如何去訪問或者獲取該資源的一種標記符
  • 在描述網路資源地址的時候,用哪種都沒問題,需要明確的原則就是最好和你的資訊接收方用同樣的稱呼,方便理解
  • 如果覺得不好拿捏屬於 URL 或者 URN,那就可以直接使用 URI 描述

2.URI 字符集

2.1 URI的設計點

URI 需要提供一種簡單,可擴充套件的方式來唯一標識資源。同時,又需要考慮到在不同媒介上進行傳播的表示形式。因此,URI 在設計時需要考慮到以下幾點:

  • URI 需要是可移植的。

不同的系統,或者不同的接收方之間都可以使用 URI 協議來標識資源。URI 可以被表示成多種形式,比如說在紙上書寫的字串,或者螢幕上的畫素,或者一系列通過編碼的二進位制流等。URI 的解析只跟這些呈現方式所關聯的字串有關,而跟具體表現方式,載體無關。
考慮到 URI 更多需要在網路場景傳輸,因此:

  • URI 是由一串字元序列組成
  • URI 可能會從非網路環境中移植到網路環境下,但是網路環境的輸入一般受制於鍵盤,滑鼠等輸入載體,因此最好由可以被這些物理載體方便輸入的字元呈現
  • URI 一般需要被人們記住並使用,所以這些字元最好是人們經常使用並且熟悉的內容

基於上述考慮,URI 為一串受限的字元所組成的字串,並選擇 US-ASCII 作為字符集。US-ASCII 字符集基本上被所有系統支援,而且相容性良好,能夠支援 URI 所需要的移植性。

  • URI 需要將標識和動作分開

這一層思想其實是需要將表示和表現分開。URI 只關注某個資源的標識,如果進行這個資源的存取或者訪問不做任何方式的保證。同資源相關的動作,引用等,在設計時被交給具體實現 URI 下 scheme 的協議來制訂,例如,http 協議會具體關心一個用’http’ scheme 表示的資源如何進行’get’, ‘update’,'delete’等一系列操作等。
這樣可以保證 URI 協議的相對穩定,以及比較好的擴充套件性

  • 層級標識

由於資源經常具有層級關係,比如在一個 example.com 站點下可能會掛有多個資源,或者下面會有一個目錄’dir’, 該目錄下會包含多個資源,這就意味著URI 需要有一種層級的組織方式。
在設計中也考慮到了這樣型別的資源組織方式,允許 URI 按照層級組織,並且在字串上按照從左到右的順序拆分元件。
類似於常用作業系統的檔案系統一樣,URI 可以用來還原具有層級關係的資源系統的組織結構。

2.2 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

2.2.1 Percent-Encoding

由於 URI 在協議中只挑選了部分ASCII 字元,數字以及符號,那麼當需要表示不在這個範圍之內的符號,字元,或者該字元在 URI 中被用來分隔符等特殊用途時,就需要對這個字元進行%編碼。百分號編碼也可以叫做URLEncode,其一般格式為:

pct-encoded = "%" HEXDIG HEXDIG 

將不能直接使用的字元先轉為位元組流表示(一般為 utf-8編碼,需要具體看上下文和 URI scheme 協議制訂),然後每個位元組轉換為%加兩個十六進位制字元來表示。例如:
“00101011” 該位元組需要編碼為 “%2B” ,在 ASCII 碼錶中表示為 "+"號

Note: 百分號編碼不關心大小寫,但是為了統一和一致,最好應該使用大寫字元

2.2.2 Reserved Characters

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

2.2.3 Unreserved Characters

允許出現在URI 中,並且不會被拿來用作保留字符集的字符集合成為 Unreserved Characters。所涉及到字元ABNF 表示為:

unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"

ALPHA = a-z / A-Z

DIGIT = 0-9 

這些字元為非保留字元,在 URI 使用過程中是不需要進行編碼的。

Note: 如果在 URI 比較中包含這些字元,那麼該字元本身或者其編碼格式都應該認為是相等的,即這些字元編碼不編碼不會影響相等性。另外,這些字元在使用時最好不要編碼,即使已經被編碼,那麼在使用時也應該先對這些字元進行解碼。

2.2.4 總結

一圖來表示在 URI 中所涉及到的保留和非保留字元,需要注意的是保留字元在不做分隔符或者具有特殊含義的時候是需要編碼的。
alphadigit.png

3.URI Component

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 部分
image.png

下文將詳細介紹各個元件部分,以及相應的語法規則。

3.1 URI component

3.1.1 Scheme

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

3.1.2 Authority

component

Authority

component 開始識別符號

//

component 結束識別符號

/  ?  #

authority component 設計的目的為設定一個名稱空間,並且標識這個名稱空間被哪個機構所管理,例如 baidu.com, google.com 等等。authority 一般由三部分組成,包含了可選的 userinfo, port 以及必選的 host 部分。
關於為什麼 Authority 部分會選擇 // 作為起始符號的原因,Tim Berners-Lee 曾回答過:

  1. 需要選擇一個命名系統來進行資源的層級化命名,/ 作為 unix 系統通用的分隔符可以在 URI 的設計中得到複用,因此使用 / 來作為 relative URI 的分隔符
  2. 需要有符號將 host 部分(類似 www.example.com)同URI 的其他部分進行區分,這部分設計參考了當時 Apollo domain system (其使用//computername/file/path進行命名)的設計方式
  3. 現在來看,他認為這個語法是比較冗餘的,更喜歡直接通過:來進行域名分隔,例如 http://www.example.com/foo/bar 轉寫為 http:www.example.com/foo/bar, 這樣寫同樣可以識別到server 並且更為簡化

由此可見,標準的設計也是需要再不斷地迭代和試驗中前進 :)

3.1.2.1 Userinfo

component

Userinfo

允許字符集

pct-encode字符集 unreserved字符集 sub-delims字符集  :

是否 case-sensitive

component 結束識別符號

@

userinfo 包含了使用者相關資訊(一般為名稱,舊式格式 user:password 由於涉及安全風險已被棄用),同時需要通過@符合和 host 進行分隔。Userinfo 部分的語法規則如下(使用 ABNF 描述):

userinfo    = *( unreserved / pct-encoded / sub-delims / ":" ) 
3.1.2.2 Host

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 ) 
3.1.2.2 Port

component

Port

允許字符集

0-9

component 結束識別符號

/

port 為可選項,同時通過十進位制進行表示。在URI語法中,port 需要跟在 : 後。port 的語法規則如下(使用 ABNF 描述):

port        = *DIGIT 

每種 scheme 一般會定義一個預設埠。例如, http 定義80預設埠,https 定義443預設埠等。

3.1.3 Path

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 / "@" 
3.1.4 Query

component

Query

允許字符集

pct-encode字符集 unreserved字符集 sub-delims字符集  @  :

component 開始識別符號

component 結束識別符號

  EOF

query 部分提供了定位資源的輔助資訊,query其內部語法並沒有明確定義,但是一般由name-value 鍵值對組成的字串組成,中間通過分隔符 & 進行分隔。例如:name1=value1&name2=value2。query 的語法規則如下(使用 ABNF 描述):

query       = *( pchar / "/" / "?" )

pchar       = unreserved / pct-encoded / sub-delims / ":" / "@" 
3.1.5 Fragment

component

Query

允許字符集

pct-encode字符集 unreserved字符集 sub-delims字符集  @  :  /  ?

component 開始識別符號

component 結束識別符號

EOF

fragment 為段落識別符號,一般用來標識一個 resource 的特定部分(一個資源子集或者一部分,或者通過這個資源來描述的一些其他資源)。 fragment 以 # 作為起始識別符號,其語法規則如下(通過 ABNF 描述):

fragment    = *( pchar / "/" / "?" )

pchar       = unreserved / pct-encoded / sub-delims / ":" / "@" 
3.1.6 小結

各個component 允許的字符集部分是我們需要特別關注的,需要注意在五個 component 之間允許使用 gen-delims 字符集,在每個 component 內(即小元件間)允許使用 sub-delims 字符集。

3.2 解析 URI

如何通過程式來解析 URI, 並得到 URI 各個 component?
如上一節 ABNF 語法規則描述,URI 滿足上下文無關文法。因此,我們可以通過語法圖來呈現整體 URI 的解析規則,如下:
16025072234541dc24351cc2b4627a4d6fac6257dbdda.png

有了上圖,使用遞迴下降,解析的虛擬碼就非常好寫了:

`/**

  • 讀取下一個字元

**/
function next() {
skip space;
read next char and return;
}

/**

  • 預掃描,檢視對應的 input 字串是否包含有 special_char,
  • 以及其位置

**/
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
}

/**

  • 對 uri 的解析函式
  • 具體的解析 component 方法為 parse_*, 需要匹配的字符集以及語法規則可參照上文中各個 ABNF

**/
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
}`

5.再論Encode 和 Decode

什麼時候該 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 在進行解碼時含義錯誤。

5.1 實現 encode 和 decode

按照上文的說法,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.
5.1.1 encode

encode 的實現中需要注意的就是對需要編碼的位元組進行%編碼,虛擬碼如下:

`/**

  • 對某一段 string s 進行 URI encode 編碼
  • 傳入 s 以及不需要編碼的字符集 dontNeedEncodingSet, 返回 URI encode後的string

*

  • dontNeedEncodingSet 字符集需要根據3.1中的 component描述來定,例如 Path 中的不需要編碼字符集
  • 一般為 unreserved字符集 sub-delims字符集 @ :(sub-delims 字符集以及@ : 如果其本身需要出現在
  • component 中而不是用來做分隔語義,那麼同樣需要進行 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;
}`

5.1.2 decode

decode 的實現中需要注意在遇到%號時讀取後續字元進行解碼,同時如果語言實現使用 utf-16編碼那麼需要對 surrogate pair 進行還原(這部分語言本身一般都提供方法來對 utf-8進行轉換),虛擬碼如下:

`/**

  • 對 s 進行解碼,返回解碼後的 string

**/
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;

}`

5.1.3 小結

相信各位已經對 URI 有了一個相對全面的瞭解,在實際工作的使用中,還需要根據語言所提供的對應 encode,decode 方法文件來進一步瞭解其編解碼所定義的 component 部分特殊保留字元,這樣會對所使用語言提供的 encode/decode 有更深入的瞭解 :)
**
Enjoy your coding trip~

作者:王陽(好未來Java開發專家)

相關文章