標籤:

共用的抽象模型(客戶及伺服器端)- URI 模型

URI 模型

Akka HTTP 提供了自己一套特製(為了性能原因,以及更好的與組件內其它模型互動)的 Uri 類型對應 URI 定義,例如,一個 HttpRequest 的目標 URI 字元串會解析成該類型,並在解析過程中應用到所有字元轉碼和 URI 特殊語義等處理。

一個 URI 字元串的語法分析

Akka HTTP 按照 RFC 3986 標準作為 URI 字元串的語法分析規範。當開發者嘗試解析一個 URI 字元串的時候,Akka HTTP 內部會建立一個 URI 類型的實例,以及其相關的 URI 組件部分。

例如,下面這個例子建立了以一個簡單合規的 URI 實例:

URI("http://localhost")n

以下再給出幾個構造 Uri 實例的例子,其中都使用了合規的 URI 字元串,以及對比如何用構造函數 Uri.from() 和相關參數 schemehostpathquery 構造一個 Uri 實例。

Uri("ftp://ftp.is.co.za/rfc/rfc1808.txt") shouldEqualn Uri.from(scheme = "ftp", host = "ftp.is.co.za", path = "/rfc/rfc1808.txt")nnUri("http://www.ietf.org/rfc/rfc2396.txt") shouldEqualn Uri.from(scheme = "http", host = "www.ietf.org", path = "/rfc/rfc2396.txt")nnUri("ldap://[2001:db8::7]/c=GB?objectClass?one") shouldEqualn Uri.from(scheme = "ldap", host = "[2001:db8::7]", path = "/c=GB", queryString = Some("objectClass?one"))nnUri("mailto:John.Doe@example.com") shouldEqualn Uri.from(scheme = "mailto", path = "John.Doe@example.com")nnUri("news:comp.infosystems.www.servers.unix") shouldEqualn Uri.from(scheme = "news", path = "comp.infosystems.www.servers.unix")nnUri("tel:+1-816-555-1212") shouldEqualn Uri.from(scheme = "tel", path = "+1-816-555-1212")nnUri("telnet://192.0.2.16:80/") shouldEqualn Uri.from(scheme = "telnet", host = "192.0.2.16", port = 80, path = "/")nnUri("urn:oasis:names:specification:docbook:dtd:xml:4.1.2") shouldEqualn Uri.from(scheme = "urn", path = "oasis:names:specification:docbook:dtd:xml:4.1.2")n

至於在 RFC 3986 中定義的一個 URI 構成的部分,例如schemepathquery。開發者可以看看這裡的一個簡述

foo://example.com:8042/over/there?name=ferret#nosen _/ ______________/_________/ _________/ __/n | | | | |nscheme authority path query fragmentn | _____________________|__n / / n urn:example:animal:ferret:nosen

對於 URI 中的 「特殊」 字元,開發者一般使用如下例中的百分號編碼。編碼相關細節會在下面 有更仔細的討論。

// 不要重複解碼nUri("%2520").path.head shouldEqual "%20"nUri("/%2F%5C").path shouldEqual Path / """/"""n

非規 URI 字元串以及 IllegalUriException

當一個不符合 URI 規範的 URI 字元串作為參數傳遞到 Uri() 構造器時,構造器會拋出異常 IllegalUriException

//非法 scheme 字元nthe[IllegalUriException] thrownBy Uri("fo?:/a") shouldBe {n IllegalUriException(n "Illegal URI reference: Invalid input ?, expected scheme-char, EOI, #, :, ?, slashSegments or pchar (line 1, column 3)",n "fo?:/an" +n " ^")}n// 非法 userinfo 字元nthe[IllegalUriException] thrownBy Uri("http://user:?@host") shouldBe {n IllegalUriException(n "Illegal URI reference: Invalid input ?, expected userinfo-char, pct-encoded, @ or port (line 1, column 13)",n "http://user:?@hostn" +n " ^")}n// 非法百分號編碼nthe[IllegalUriException] thrownBy Uri("http://use%2G@host") shouldBe {n IllegalUriException(n "Illegal URI reference: Invalid input G, expected HEXDIG (line 1, column 13)",n "http://use%2G@hostn" +n " ^")}n// 非法 path 字元串nthe[IllegalUriException] thrownBy Uri("http://www.example.com/name with spaces/") shouldBe {n IllegalUriException(n "Illegal URI reference: Invalid input , expected /, EOI, #, ? or pchar (line 1, column 28)",n "http://www.example.com/name with spaces/n" +n " ^")}n// 非法 path 及控制字元nthe[IllegalUriException] thrownBy Uri("http:///withnewline") shouldBe {n IllegalUriException(n "Illegal URI reference: Invalid input n, expected /, EOI, #, ? or pchar (line 1, column 13)",n "http:///withn" +n " ^")}n

可以抽取 URI 組件的指令塊

如果需要抽取 URI 組件,可以參考以下指令塊的文檔

  • extractUri
  • extractScheme
  • scheme
  • PathDirectives
  • ParameterDirectives

獲取原生請求的 URI

有時候需要在收到請求里抽取「原生」(不做轉碼或解析)的 URI 值。雖然這種情況比較少見,但也偶爾會碰到。在 Akka HTTP 伺服器端的設置里把 akka.http.server.raw-request-uri-header 標記打開,就可以在需要時獲取「原生」值。注意在該標識打開時,一個 Raw-Request-URI 頭域實例會被添加到每個請求里,這個實例裡面會包有原生的 uri 字元。

URI 里的查詢字元

雖然在 URI 字元串的任何部分都可能有特殊字元,一般而言,特殊字元(通常都是百分比編碼)在查詢部分出現的更常見。

Uri 類型裡面的 query() 函數返回一個 Query 類型的查詢字元串。當用 一個 URI 字元串生成一個 Uri 類型實例時,查詢欄位將以原生值的狀態保存到該實例中,直到調用 query() 函數時才會被解析。

下面的代碼展示如何解析合規的查詢字元串。特別是可以留意一下百分比編碼的應用以及特殊字元的處理例如 + 和 ;

要點

Query() 和 Uri.query() 里的 mode 參數對比了 嚴格和寬鬆 模式

def strict(queryString: String): Query = Query(queryString, mode = Uri.ParsingMode.Strict)n

//查詢欄位 "a=b" 被解析為參數名: "a", 和值: "b"nstrict("a=b") shouldEqual ("a", "b") +: Query.Emptynnstrict("") shouldEqual ("", "") +: Query.Emptynstrict("a") shouldEqual ("a", "") +: Query.Emptynstrict("a=") shouldEqual ("a", "") +: Query.Emptynstrict("a=+") shouldEqual ("a", " ") +: Query.Empty //+ 被解析為 nstrict("a=%2B") shouldEqual ("a", "+") +: Query.Emptynstrict("=a") shouldEqual ("", "a") +: Query.Emptynstrict("a&") shouldEqual ("a", "") +: ("", "") +: Query.Emptynstrict("a=%62") shouldEqual ("a", "b") +: Query.Emptynnstrict("a%3Db=c") shouldEqual ("a=b", "c") +: Query.Emptynstrict("a%26b=c") shouldEqual ("a&b", "c") +: Query.Emptynstrict("a%2Bb=c") shouldEqual ("a+b", "c") +: Query.Emptynstrict("a%3Bb=c") shouldEqual ("a;b", "c") +: Query.Emptynnstrict("a=b%3Dc") shouldEqual ("a", "b=c") +: Query.Emptynstrict("a=b%26c") shouldEqual ("a", "b&c") +: Query.Emptynstrict("a=b%2Bc") shouldEqual ("a", "b+c") +: Query.Emptynstrict("a=b%3Bc") shouldEqual ("a", "b;c") +: Query.Emptynnstrict("a+b=c") shouldEqual ("a b", "c") +: Query.Empty //+ 被解析為 nstrict("a=b+c") shouldEqual ("a", "b c") +: Query.Empty //+ 被解析為 n

注意

Uri("http://localhost?a=b").query()n

等價於

Query("a=b")n

正如 RFTC 3986 中 section 3.4 的規定,特殊字元如 「/」 和 「?」 在查詢字元串可以不需要百分比(%)轉碼。

字元 斜杠 (「/」) 和 問號 (「?」) 可能在查詢欄位中表示某些數據。

當 URI 中的查詢參數里有另一個 URI的時候, 「/」 和 「?」 就經常會被用到。

strict("a?b=c") shouldEqual ("a?b", "c") +: Query.Emptynstrict("a/b=c") shouldEqual ("a/b", "c") +: Query.Emptynnstrict("a=b?c") shouldEqual ("a", "b?c") +: Query.Emptynstrict("a=b/c") shouldEqual ("a", "b/c") +: Query.Emptyn

但是,有一些其它的特殊字元如果不要百分比編碼的話,則會拋出異常 IllegalUriException

the[IllegalUriException] thrownBy strict("a^=b") shouldBe {n IllegalUriException(n "Illegal query: Invalid input ^, expected +, =, query-char, EOI, & or pct-encoded (line 1, column 2)",n "a^=bn" +n " ^")n}nthe[IllegalUriException] thrownBy strict("a;=b") shouldBe {n IllegalUriException(n "Illegal query: Invalid input ;, expected +, =, query-char, EOI, & or pct-encoded (line 1, column 2)",n "a;=bn" +n " ^")n}n

//兩個 = 在查詢欄位是不符合規範的nthe[IllegalUriException] thrownBy strict("a=b=c") shouldBe {n IllegalUriException(n "Illegal query: Invalid input =, expected +, query-char, EOI, & or pct-encoded (line 1, column 4)",n "a=b=cn" +n " ^")n}n//在 %之後, 應該有對應的轉碼, 但 "%b=" 不是一個合規的百分比編碼nthe[IllegalUriException] thrownBy strict("a%b=c") shouldBe {n IllegalUriException(n "Illegal query: Invalid input =, expected HEXDIG (line 1, column 4)",n "a%b=cn" +n " ^")n}n

嚴格 和 寬鬆 模式

Uri.query() 函數 和 Query() 構造函數都可以讀入一個參數 mode, 這個參數的類型可以是 Uri.ParsingMode.StrictUri.ParsingMode.Relaxed。模式不同的選擇也會帶來不同的字元串不同的解析行為。

def relaxed(queryString: String): Query = Query(queryString, mode = Uri.ParsingMode.Relaxed)n

以下兩個例子在 Strict 模式下會拋出 IllegalUriException

the[IllegalUriException] thrownBy strict("a^=b") shouldBe {n IllegalUriException(n "Illegal query: Invalid input ^, expected +, =, query-char, EOI, & or pct-encoded (line 1, column 2)",n "a^=bn" +n " ^")n}nthe[IllegalUriException] thrownBy strict("a;=b") shouldBe {n IllegalUriException(n "Illegal query: Invalid input ;, expected +, =, query-char, EOI, & or pct-encoded (line 1, column 2)",n "a;=bn" +n " ^")n}n

但在 Relaxed 模式下就會正常解析

relaxed("a^=b") shouldEqual ("a^", "b") +: Query.Emptynrelaxed("a;=b") shouldEqual ("a;", "b") +: Query.Emptyn

值得注意的是,即使在 Relaxed 模式下,還是有一些非合規的特殊字元需要百分比編碼。

//兩個 = 在查詢欄位是不符合規範的, 即使在 Relaxed 模式nthe[IllegalUriException] thrownBy relaxed("a=b=c") shouldBe {n IllegalUriException(n "Illegal query: Invalid input =, expected +, query-char, EOI, & or pct-encoded (line 1, column 4)",n "a=b=cn" +n " ^")n}n//在 %之後, 應該有對應的轉碼, 但 "%b=" 不是一個合規的百分比編碼n//即使在 Relaxed 模式,還是不合規nthe[IllegalUriException] thrownBy relaxed("a%b=c") shouldBe {n IllegalUriException(n "Illegal query: Invalid input =, expected HEXDIG (line 1, column 4)",n "a%b=cn" +n " ^")n}n

mode 除了可以作為一個參數在函數里設定(例如使用指令快時)不同的模式,開發者還可以在配置文件裡面進行設置。如下:

# Sets the strictness mode for parsing request target URIs.n # The following values are defined:n #n # `strict`: RFC3986-compliant URIs are required,n # a 400 response is triggered on violationsn #n # `relaxed`: all visible 7-Bit ASCII chars are allowedn #n uri-parsing-mode = strictn

需要拿到原始未被解析過的 URI 查詢欄位時,可使用 Uri 類型 的 rawQueryString 成員。

抽取查詢參數的相關指令件

如果開發者需要使用相關指令件抽取查詢參數,可以參考以下鏈接

  • parameters
  • parameter

推薦閱讀:

請教Spark 中 combinebyKey 和 reduceByKey的傳入函數參數的區別?
為什麼 Scala 不建議用 return?
Scala起源
Spray中對複雜JSON的序列化與反序列化

TAG:Akka | HTTP | Scala |