之所以叫号外是因为这篇文章不需要接着之前的内容来阅读,可以随时学习这篇文章的内容。
如今的游戏无论“单机”程度有多高,也很有可能需要从网络上获得数据。例如你的游戏可能有一个高分榜需要上传分数和获得高分榜。又或者你的游戏接入了游戏平台的某种服务。
上网会上吧,打开浏览器,输入地址,敲回车。
啥是网址
“网址”是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!