之所以叫號外是因為這篇文章不需要接著之前的內容來閱讀,可以隨時學習這篇文章的內容。
如今的遊戲無論“單機”程度有多高,也很有可能需要從網絡上獲得數據。例如你的遊戲可能有一個高分榜需要上傳分數和獲得高分榜。又或者你的遊戲接入了遊戲平臺的某種服務。
上網會上吧,打開瀏覽器,輸入地址,敲回車。
啥是網址
“網址”是URL(Universal Resource Locator)的一種。你可能還聽說過URI的說法,URL是URI的一種。例如這是一個典型的URL:
https://www.gcores.com/
這個URL屬於通常所說的“網址”,它放在瀏覽器地址欄就可以訪問。實際上並不是所有URL都用於訪問網站,實際上瀏覽器的地址欄也並不只能輸入“網址”。
URL的第一段內容是scheme,http和https可能是最常見的兩種scheme。除此之外,你可能還看到過ftp、file。
啥是HTTP(S)
協議這一術語在計算機領域中經常出現。不過它是一種抽象的協議,並不是我們第一時間想到的那種寫在紙上需要簽字那種協議。協議通常是兩個系統之間達成的一種約定,約定以某種方案來交換數據,以保證工作順利進行。協議這一術語在計算機領域中的不同語境中也有不同的具體含義(當然核心思想還是類似的)。例如蘋果的編程語言(Objective-C和Swift)中都存在protocol的概念,它實際上就相當於其它語言中常見的interface(接口)概念。
在HTTP協議下,一方會向另一方發起請求(request),另一方收到請求後會發出響應(response)。這個過程中雙方會交換一種叫HTTP Message(一般譯作報文)的數據。其中包含了這次通信的必要信息。順帶一提,發起請求的一方一般稱為客戶端(client),接受請求並響應的一方一般稱為服務(器)端(server side或者就是server)。當然在不同的語境下server可能指不同的東西,有時候人們可能說的是硬件(用於提供網絡服務的一類特殊的電腦),也有可能說的是運行在server上的一類軟件(接受、處理請求並返回客戶端所需內容)。
在使用瀏覽器訪問網站時,我們絕大部分時候就是在通過http協議進行通信(https是對http的擴展,使得通信更安全)。你可以隨意訪問一個網站,然後按下F12打開開發者工具(我能想到的主流瀏覽器都是這個快捷鍵),切換至網絡面板。當然如果你的網頁此時已經加載完畢,這裡可能看不到任何內容,提示會告訴你你點擊按鈕刷新。這裡可以看到訪問這個網址時為加載網頁內容而發生的網絡通信。點擊條目就可以看到HTTP通信的報文詳細內容:
報文的內容不能說簡單,但是作為開發者我們有一些需要關注的內容。報文內容主要分為頭部(headers)和主體(body)。頭部包含了請求和響應的一些基本信息,對應的內容就在Headers標籤頁下。
首先是Method(方法),也就是說我們要對這個URL進行什麼樣的操作。可以看到加載網頁時主要是GET操作,顧名思義它代表獲得某些內容。常見的方法還有POST、DELETE等。
狀態碼(Status code)表示此請求得到的響應的狀態。大家耳熟能詳的404就是HTTP的狀態碼之一。請求成功時一般返回的是200 OK——這個比404更常見、更受人喜愛的狀態碼反而不容易被非專業人士知曉,因為正常成功的請求一般就是顯示正常的網頁內容了。
Accept(請求)和Content Type(響應)這一對頭部字段對通信中交換的數據的類型進行了協商。Accept告知服務器我可以接受的類型,Content Type指明返回的內容的類型。例如上圖這個svg圖片的類型就是image/svg+xml,代表它是svg格式的圖片。這種指明類型的格式稱為MIME類型,它由類型和子類型構成,中間用斜槓隔開。常見的MIME類型列表可以在
這裡看到。
主體一般是請求和響應攜帶的具體數據。對於一些複雜的請求參數都會放到主體中,而響應的具體數據也會隨著主體返回給客戶端。當然,瀏覽器的開發者工具沒有說主體這兩個字,相應的主體內容放到了Request和Response標籤頁下。
計算機網絡技術是一個非常複雜的課題,這在大學裡也是一門課一門專業。這裡推薦另一位機核用戶寫的文章。
在Godot中進行HTTP通信
現在我們馬上用學到的基礎知識在Godot中實踐一下。新建一個場景,然後加入HTTPRequest節點。這個節點顧名思義就是HTTP請求的意思,我們需要通過它來發起HTTP請求:
這個節點最關鍵的方法就是request方法。它只有一個必填參數,那就是要請求的URL。這裡我們填入Godot的官網。
注意:Godot對HTTP通信的相關類型設計得略顯奇怪,和其它編程語言中的很多HTTP庫的設計有不少差別。HTTPRequest的名字有一定誤導性,它看起來像是一個專門用來表示HTTP請求的數據類型,但是它更多的是想作為一個更方便、簡單的HTTP客戶端類——因為它本身就是通過Godot的另一個類型HTTPClient實現的。HTTPClient的抽象程度更低,提供更復雜的控制方式。另外,文檔中的HTTPRequest示例代碼也有一定誤導性。和其它編程語言進行HTTP通信的常規操作一樣,HTTP客戶端類應當重複使用而不是每次發起請求時都新構造一個客戶端。利用了HTTPClient的HTTPRequest也一樣,應當重複使用——而且它本身也是一個節點。示例代碼是直接在ready中構造HTTP Request然後加入到場景樹中的,可以這樣做但不要每次請求都這樣做!
request方法的第二個參數是自定義頭部,也就是前面提到的header。第三個參數是方法,默認為0也就是GET方法。這裡我們不需要修改。
不過請求發出去了,我們如何獲得響應呢?這裡需要連接上HTTPRequest的request_completed信號以獲得響應情況。
第一個參數是定義在HTTPRequest中的一系列枚舉常量,用以體現請求是否成功。第二個參數就是HTTP狀態碼,如果成功一般都是200。這裡我們簡單判斷一下結果是否OK:
最後兩個參數分別為響應的頭部和主體。一般來說,我們主要關心的就是響應的主體,裡面有我們想要的內容。從方法簽名中可以看到,body的類型為PackedByteArray,即一種緊湊的字節數組。
前面提到過,各種數據都可以用二進制來表示,最終體現為字節數組(一串數字)。它以什麼格式編碼,需要以何種格式解析,需要雙方來協調,這就是協議存在的原因之一。通過網絡、通過HTTP協議交換的數據並不總是以人類可讀的文本形式編碼的,比如通過HTTP交換各種文件是很常見的工作。
我們可以通過響應的頭部來確認服務器返回的內容的類型。對Godot官網發起請求後,我們可以print一下頭部的ContentType。頭部的類型是PackedStringArray,可以簡單理解為一種字符串數組:
可以發現內容類型為:
HTML是超文本標記語言的縮寫,它是“網頁”的基石。瀏覽器在收到HTML頁面(和相關的代碼以及必要資源後)會把它們渲染成我們看到的頁面。
接下來,要解析這種以字節表示的數據,PackedByteArray類型提供了很多常見方法。比如我們可以直接把它當成字符串來解釋(畢竟text/html就是一種文本類型),這裡就可以用get_string系列方法。這一系列方法會把數據按照某種文本編碼來解釋,這裡我們選擇如今最常見的編碼方式UTF-8。
可以看到這就是官網的首頁的一些HTML代碼:
一般來說只要不是遊戲內瀏覽器我們不會直接對返回需要瀏覽器渲染的URL發起請求,因為這些內容沒什麼用。
實際上客戶端類應用,以及這裡談的遊戲,更多的時候需要通過各種Web API來獲得數據。
啥是Web API
HTTP不止能交換給一般用戶看的網頁內容。在很多時候我們只需要數據,而不需要渲染一個網頁。比如你手機上的天氣應用,這些實際的視覺內容都是手機上的App實現的,而數據來自各種各樣的數據來源。要獲得這些來自網絡的數據,最常見的方式就是通過各種組織提供的Web API。
API就是應用程序編程接口。它說白了就是一方提供給另一方的一些可以用來編程的東西。Godot的GDScript中提供的各種類型、函數也叫API。Web API實際上就是通過網絡提供的一些數據接口,它們通常只會返回一些特定格式的數據以便於各種客戶端來使用。
接下來我們將會利用一個免費的公開API來進行講解和實驗。
注意:Web API通常都會設有訪問頻率限制,無論它是免費還是付費,我們都應該有基本的道德底線,遵守使用條例,不濫用。很多API即使是免費的也需要註冊並獲得一個API key,在訪問API時必須帶上這個key來表明身份,其目的之一就是為了防止濫用。這裡為了方便起見就選擇的一個不需要API key的API。
wttr是一個天氣API。它的作用非常簡單,就是獲得一些天氣數據。它支持多種顯示格式,甚至可以以純文本在各種終端中顯示。比如在瀏覽器中訪問:
https://www.wttr.in/Beijing
beijing可以替換為世界上的各個地方。不過這裡我們看到的內容很多都是英文的,我們可以使用它提供的查詢參數來修改返回的格式:
https://www.wttr.in/Beijing?lang=zh-cn
結尾的?lang=zh-cn叫做查詢參數(query parameters)。很多Web API會以這種形式讓調用方直接通過URL傳遞一些參數。當然這些參數都比較簡單,更復雜的參數可能會需要按照API的要求以某種格式編碼後放到HTTP請求的body中。查詢參數出現在URL最後,以?表示查詢參數的開始。後面是以&分隔的若干參數鍵值對,鍵和值以等號分隔。例如也可以像這樣查詢:
https://www.wttr.in/Beijing?lang=zh-cn&format=j1
這裡的URL多了一個查詢參數。訪問成功後你可以看到一些數據,但是你會發現它並不是網頁。如果你看開發者工具中的內容類型,可以看到它是json格式的。因為這裡指定的format參數值j1就指的是以json格式返回。wttr提供的各種參數都可以在它的GitHub頁面上查到。
不過
啥是JSON
數據交換過程中——即使是以格式化的、人類可讀的文本形式來數據也可以以不同格式來表示。例如,XML(eXtensible Markup Language,可擴展標記語言)是一種至今仍在廣泛運用的格式——儘管由於出現了很多設計更好的數據格式,XML現在已經不如過去那麼受歡迎了。它和HTML在系譜上有一定聯繫:
XML由若干元素(element)組成。元素由標籤(tag)組成,標籤分為起始標籤和結束標籤。兩個標籤之間的內容可以是一般的文本,也可以是嵌套的其它元素。通過這樣的嵌套關係,我們可以把各種數據表示為XML。如你所見,XML看起來非常冗長,每個元素的標籤都有開始和結束兩個。畢竟它只能算是“人類勉強能看”。
如今,JSON才是更受歡迎的開放數據格式。
JSON是JavaScript Object Notation的縮寫,即JavaScript對象記法。從這個名字上來看它似乎和JavaScript有關係。確實,它的語法是JS的子集。意思就是說,任何合法的JSON內容都是合法的JS內容。但是,JS作為其超集,合法的JS內容不一定是合法的內容。
另外要注意,JSON雖然源自JS,但是它絕不是JS專用,它只是一種文本格式。JSON的格式很簡單,幾句話就能說清楚。簡單絕對是它流行的原因之一。例如把前面用XML格式表示的數據轉換為JSON:
一對花括號就是一個對象。一個對象有若干屬性,屬性用逗號隔開,一個屬性就是一個鍵值對,鍵和值用冒號隔開。屬性名(鍵)用雙引號括起來。冒號後面就是值。值的類型有且僅有字符串、數字、對象、數組。其中數組用方括號表示,其中可以有任意多個逗號隔開的對象。
如今各種編程語言都會內置處理JSON的庫。
序列化與反序列化
你可能會在各種地方看到序列化(serialize,serialization)和反序列化(deserialize,deserialization)這一對術語。序列化指的就是把程序中的對象轉換成特定的可以存儲到磁盤上或者通過網絡傳輸的格式。可以理解為就是一種轉換的過程,畢竟對象在程序運行環境以及內存中的表示,和保存到磁盤上、網絡傳輸所需要的格式差別很大。不言而喻,反序列化就是把各種格式還原成程序中的對象。
在Godot中處理JSON
在Godot中可以通過JSON類來處理JSON。
一些API可能需要調用方以JSON的形式傳遞一些參數給它,比如複雜的查詢參數,或者我們需要將一些新的數據交給服務器讓其記錄。
JSON類的stringify方法會將一個GDScript對象轉換為JSON字符串。它的第一個參數就是要轉換為JSON的對象,其類型為Variant,基本上就是說任意類型——文檔裡甚至也是這麼說的。但是這裡比較迷惑,這個方法”並不支持“字典以外的對象。倒不是說傳入非字典的對象會報錯,而是它不會按照預期那樣工作。你可以試一下傳入一個向量,它得到的字符串並不是{x:1.0, y:1.0},而是單純地把對象轉化為字符串。
總之,要使用stringyfy基本上只能傳入字典,如果你需要轉換字典以外的對象,特別是自定義的數據類型,只能自己寫方法先把需要JSON化的屬性轉換為一個字典。
順帶一提,如果使用C#開發,可以直接使用.NET標準庫的System.Text.Json中的各種類型進行強類型的JSON轉換操作。
現在我們用Godot的HTTP模塊來調用這個天氣API。
前面我們已經看到請求時指定format為j1後得到的是JSON格式的數據。我們通過JSON.parse方法將得到的JSON轉換成一個類似於字典的Variant。這樣一來我們就可以獲得返回的JSON中的各種數據。但是顯然,Variant是類型不安全的,你沒辦法知道它是否有哪些數據,在編寫代碼時也沒有自動補全來幫助我們。當然這在很多編程語言中都會面臨這種問題,為了以通用的格式傳遞數據我們需要把數據轉換成JSON,然後收到數據時又轉換回來。
我們可以自行定義一個數據類來保存獲得的天氣信息便於操作。從API返回的數據不一定和我們感興趣的數據能夠完全對應,我們可以以我們的需求優先,但是同樣需要分析API返回的數據結構。
wttr返回的JSON格式數據有四個屬性對應著四個數組:
注意是數組,摺疊後顯示的中括號也暗示了這一點。current condition是請求時查詢的位置即時天氣狀況。nearest area是和查詢的地方最近的區域,我們用正確的城市名稱查詢的話這個屬性沒啥用。request主要是經緯度相關設置,沒啥用。weather是一個包含今天、明天、後天三天的天氣狀況數組。
根據當前天氣狀況數據的結構,我們定義一個類似的類。由於我們這個數據類可能需要在不同場景中使用,所以在一個腳本文件中用class定義為內部類可能不太方便,如果相關的東西複雜起來也不好組織。
所以我們直接新建一個單獨的腳本文件。你可能很少這樣做,也可能忘了為何要這樣做。這裡簡單提一句。儘管我們很多時候都是先建一個場景,然後再建一個腳本給它。但是場景和節點只是Godot中諸多對象中的一種。我們並不總是需要場景和節點。比如這裡我們需要構建的是對數據的建模。和場景、節點沒有直接關係。
在文件系統面板中直接右鍵新建腳本。當然這裡最重要的選擇新腳本的基類,這裡我們需要選擇RefCounted。這個類直接繼承了Object——所有東西最終的基類。它主要提供引用計數功能,能夠讓Godot在沒有人使用這個對象時進行內存回收操作。
隨後,我們就在這個腳本中定義我們表示天氣數據結構:
作為演示,這裡只定義了溫度、體感溫度、溼度、文字描述。其它的屬性可以作為練習自己做一下。
然後提供一個靜態方法,使得我們能夠方便地從JSON字符串構造Weather實例:
我們在這裡訪問json對象的各個屬性填充Weather即可。
這裡有一個陷阱需要注意。
天氣狀況的描述(“晴”、“多雲”、“小雨”等等)默認是英文,我們這裡在請求參數中指明瞭用簡體中文,返回的數據中中文的天氣描述在lang_zh-cn中提供。已經多次提到Variant類似於字典,其中的各個屬性可以用點語法直接訪問——早前的文章中也提到過,GDScript中的字典本身也可以用點語法直接通過鍵來訪問值,而不一定要用方括號加上鍵。但是通過點語法訪問有一個限制,那就是你的鍵必須符合標識符命名要求才可以。標識符雖然可以包含各種字符,但是不能以數字開頭,且不能包含一些有特殊含義的字符。按照返回的JSON的結構,如果要獲得當前天氣狀況的中文描述,那麼就需要訪問“current condition的第0個元素的lang_zh-cn屬性的value屬性”。不過lang zh cn的zh和cn之間用了一個減號連接,這就直接造成了它無法使用點語法直接訪問。不過,要解決也很簡單,我們又回到方括號方式即可:
總之,我們就根據JSON的結構來獲得我們想要的信息:
接下來,你還可以做一個UI然後顯示這些天氣數據,最終做成一個天氣app!