网络编程

学习网络相关知识,学习 HTTP 的原理和工作机制、请求行、状态码、Header、Body 以及什么是 REST 。

网络分层

网络分层指的是将网络节点的工作交由不同的硬件和软件模块去完成,这些工作包括数据的发送/转发、打包/拆包,以及控制信息的加载/拆出等。网络分层通常参照两个模型,分别是由国际标准化组织制定的 7 层模型:开放式系统互联模型(Open System Interconnection Model),简称「OSI 模型」。以及平时更加常用的 5 层模型:「TCP/IP参考模型」,这 5 层从下至上分别是:

  • 物理层:负责在设备间传输数据比特流,即物理传输,通俗讲就是把计算机连接起来的物理手段。
  • 数据链路层:负责控制网络层和物理层之间的通信,把从网络层接收到的数据分割成特定的可被物理层传输的帧。
  • 网络层:综合考虑发送优先权、网络拥塞程度、服务质量以及可选路由的花费来决定一个从节点 A 到节点 B 的最佳路径。
  • 传输层:为两台主机上的应用程序提供端到端的通信,传输层有两个传输协议,分别是 TCP 和 UDP 。
  • 应用层:不同的网络应用的应用进程之间,还需要有不同的通信规则,应用层将精确定义这些通信规则。主要协议有 HTTP、FTP、SMTP 。

TCP 的握手和挥手

采用 HTTP 连接网络时会进行 TCP 的三次握手,然后再传输数据:

  1. 第一次握手:建立连接,由客户端发送连接请求报文段(SYN = 1, seq = x),客户端进入SYN_SENT状态。
  2. 第二次握手:服务端收到来自客户端的 SYN 报文段并进行确认然后响应(SYN = 1, seq = y, ACK = x+1),服务端进入SYN_RCVD状态。
  3. 第三次握手:客户端收到来自服务端的 SYN+ACK 报文段,然后将 ACK 设置为y+1然后回传给服务端。至此客户端和服务端都进入了ESTABLISHED状态。

当数据传输完毕,再通过 TCP 的四次挥手来断开连接:

  1. 第一次挥手:客户端向服务端发送了一个 FIN 报文段,表示自己已经没有数据要发送了,随即进入FIN_WAIT_1状态。
  2. 第二次挥手:服务端回了一个 ACK 报文段,说“我知道了,但是你别急,我可能还有东西要发。”
  3. 第三次挥手:服务端也向客户端发送了 FIN 报文段,请求关闭连接,随即进入LAST_ACK状态。
  4. 第四次挥手:客户端收到服务端的 FIN 报文段后随即向服务端发送 ACK 报文段,然后客户端就进入了TIME_WAIT状态,而服务端在收到来自客户端的 ACK 报文段后就关闭了连接。客户端在TIME_WAIT状态下等待 2MSL 也就是最大报文段生存时间后,如果没有收到来自服务端的回复,说明服务端已经正常关闭了,最后客户端再自己关闭连接即可。

值得注意,三次握手是客户端和服务端之间一来一回,而四次挥手则是先由客户端发送一次,然后服务端发送两次,最后一次再由客户端发送。

HTTP

URL 和报⽂

URL 整体分为 3 部分:协议类型、服务器地址(和端⼝号)、路径(Path)。例如:http://example.com/users?gender=male,其格式为协议类型://服务器地址[:端⼝号]绝对路径

HTTP 报⽂分为请求报文和响应报文。请求报文可以分成四个部分:

  • 请求行。例如GET /users?gender=male HTTP/1.1,请求行由三部分组成,分别是请求方法、路径、HTTP 版本。
  • 请求报头,以键值对的形式存在。
  • 空行。
  • 请求数据。请求数据不在GET方法中使用,而在POST方法中使用,与请求数据相关的最常用的请求报头是Content-TypeContent-Length

响应报文可以分成四个部分:

  • 状态行。状态行也同样可以看作三个部分:HTTP 版本、状态码、状态信息。例如HTTP/1.1 200 OK,其中HTTP/1.1仍然是 HTTP 版本,而200则是状态码,OK则是状态信息(状态信息是状态码的文本描述)。
  • 响应报头,以键值对的形式存在。
  • 空行。
  • 响应正文。也就是服务器返回的资源。

补充:

  • GET为请求方法,代表了要做出哪一种请求,比如要获取信息用GET,提交数据用POST
  • /users是请求的路径,代表要访问哪里的资源,这条信息将会被服务器接收。HOST: example.com不是路径,而是给接收到报文的主机查看的。这两者有着不同的作用。
  • Body 有着让服务器识别的具体信息。例如要提交数据,首先使用POST请求,而提交数据的具体内容就是存放在 Body 中。
  • HTTP/1.1为 HTTP 的版本。HTTP 的大版本号有 0.9 、1.0 、1.1 、2.0 其中前两者已被废弃,2.0 则是还未普及到浏览器上,因为版本更新往往需要客户端和服务端都适配,因此相较于浏览器来说,Web 服务用 2.0 会多一些,因为 Web 服务的两端都是由厂商提供的。

请求方法

GET

获取资源;没有 Body ;具有幂等性;

报文形态:

Text
1
2
GET /users/1 HTTP/1.1
Host: api.github.com

对应 Retrofit 代码:

1
2
@GET("/users/{id}")
Call<User> getUser(@Path("id") String id, @Query("gender") String gender);

POST

增加或修改资源;有 Body 。

报文形态:

Text
1
2
3
4
5
6
POST /users HTTP/1.1
Host: api.github.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 22

name=Aiden&gender=male

对应 Retrofit 代码:

1
2
3
@FormUrlEncoded
@POST("/users")
Call<User> addUser(@Field("name") String name, @Field("gender") String gender);

PUT

只是修改资源;有 Body ;出现修改的需求时,使用POST或者PUT都行。POST不具有幂等性,PUT具有幂等性。所谓幂等性就是单次操作和多次操作所得到的结果是一致的,PUT无论操作多少次都是进行修改的动作,而POST可能只有第一次会修改,之后则会添加。

报文形态:

Text
1
2
3
4
5
6
PUT /users/1 HTTP/1.1
Host: api.github.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13

gender=female

对应 Retrofit 代码:

1
2
3
@FormUrlEncoded
@PUT("/users/{id}")
Call<User> updateGender(@Path("id") String id, @Field("gender") String gender);

DELETE

用于删除数据;没有 Body ;具有幂等性。

报文形态:

Text
1
2
DELETE /users/1 HTTP/1.1
Host: api.github.com

对应 Retrofit 代码:

1
2
@DELETE("/users/{id}")
Call<User> getUser(@Path("id") String id, @Query("gender") String gender);

GET的使用方式一样,但是区别在于GET的响应报文中有 Body ,而HEAD没有。使用场景:可以尝试在获取下载请求前先通过HEAD获取文件的信息包括大小和支不支持断点续传等等。

状态码

三位数字,⽤于对响应结果做出类型化描述。

  • 1xx:指示性消息。如:100表示等待客户端继续发送,在某些场景下客户端可能无法一次把所有内容发送完成,这时候客户端会附加一条Expect: 100-continue告知服务器信息,此时服务器就会返回HTTP/1.1 100)。101表示正在切换 HTTP 协议,浏览器可以通过发送Upgrade: h2c去确认服务器是否支持 HTTP 2 ,如果服务器返回HTTP/1.1 101代表支持,返回 200 则不支持)。
  • 2xx:请求成功。最典型的是 200(OK)、201(创建成功)。
  • 3xx:重定向。如 301(永久移动,例如从httphttps)、302(暂时移动)、304(内容未改变)。
  • 4xx:客户端错误,请求有语法错误或请求无法实现。如 400(客户端请求错误)、401(认证失败)、403(被禁⽌)、404(找不到内容)。
  • 5xx:服务器错误。如 500(服务器内部错误)。

HTTP 报头

报头由键值对组成,每行一对,键和值之间用:隔开。

通用报头

既可以出现在请求报头中,也可以出现在响应报头中。

  • Date:消息产生的日期和时间。
  • Connection:允许发送指定连接的选项。例如指定连接是连续的,或者指定close,通知服务器在响应完成后关闭连接。
  • Cache-Control:指定缓存指令。缓存指令是单向的(响应中出现的缓存指令在请求中未必会出现),也是独立的(一个消息的缓存指令不会影响另一个消息处理的缓存机制)。

请求报头

客户端用请求报头通知服务器要请求的信息。

  • Host:请求的主机名,Host 仅用于找到目标主机后确认主机域名和端口目标主机,而不是用于在⽹络上寻址。
  • User-Agent:发送请求的浏览器类型、操作系统等信息。
  • Accept:客户端可识别的内容类型列表,指定了客户端接收哪些类型的信息,如text/html
  • Accept-Encoding:客户端可识别的数据编码。如 gzip 。
  • Connection:允许客户端和服务器指定与请求/响应连接有关的选项,例如Keep-Alive,则表示保持连接。
  • Transfer-Encoding:告知接受端为了保证报文的可靠传输,对报文采用了什么编码方式。在某些请求下,服务器需要响应的数据量过大,这时候可以等全部数据处理完了然后一次性发完,但是这样就会造成过长的等待时间。如果转而使用Transfer-Encoding: chunked,就可以让数据分段发出,整体减少等待时间。

响应报头

服务器用于传递自身信息的响应。

  • Location:用于重定向接收者到一个新的位置,常用在更换域名的时候。
  • Server:服务器用来处理请求的系统信息,与User-Agent相对应。

实体报头

用来定义被传送资源的信息,既可以用于请求,也可以用于响应(请求和响应都可以传送一个实体)。

  • Content-Type:发送给接收者的实体正文的媒体类型,包括:

text/html

表示 HTML 文本(页面),当浏览器请求网页后,响应返回的是text/html。报文形式:

Text
1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 853
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
......

x-www-form-urlencoded

表示 Web ⻚⾯纯⽂本表单。对应的 Retrofit 注解为@FormUrlEncoded,字段则使用@Field修饰。报文形式:

Text
1
2
3
4
5
6
POST /users HTTP/1.1
Host: api.github.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 22

name=aiden&gender=male

对应 Retrofit 代码:

1
2
3
@FormUrlEncoded
@POST("/users")
Call<User> addUser(@Field("name") String name, @Field("gender") String gender);

multipart/form-data

表示含有⼆进制⽂件(例如上传图片)时的表单。返回的 content-type 中除了有multipart/form-data标识,还有boundary,其作用是将提交表单中各个部分分开(因为通过Content-Length只能知道总大小),而纯文本表单只需要&就可以分开。报文形式:

Text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /users HTTP/1.1
Host: api.github.com
Content-Type: multipart/form-data; boundary=----
WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 2382

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="name"

aiden
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="avatar.jpg"
Content-Type: image/jpeg

JFIFHHvOwX9jximQrWa......
------WebKitFormBoundary7MA4YWxkTrZu0gW--

对应 Retrofit 代码:

1
2
3
4
5
6
7
8
9
@Multipart
@POST("/users")
Call<User> addUser(@Part("name") RequestBody name, @Part("avatar") RequestBody avatar);

...

RequestBody namePart = RequestBody.create(MediaType.parse("text/plain"), nameStr);
RequestBody avatarPart = RequestBody.create(MediaType.parse("image/jpeg"), avatarFile);
api.addUser(namePart, avatarPart);

application/json

⽤于 Web Api 的响应或者 POST / PUT 的请求。例如请求的报文形式:

Text
1
2
3
4
5
6
POST /users HTTP/1.1
Host: api.github.com
Content-Type: application/json; charset=utf-8
Content-Length: 32

{"name":"aiden","gender":"male"}

对应 Retrofit 代码:

1
2
3
4
5
@POST("/users")
Call<User> addUser(@Body("user") User user);
...
// 需要使⽤ JSON 相关的 Converter
api.addUser(user);

响应的报文形式:

Text
1
2
3
4
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 234
[{"login":"mojombo","id":1,"node_id":"MDQ6VXNlcjE=","avatar_url":"https://avatars0.githubusercontent.com/u/1?v=4","gravat......

image/jpeg | application/zip

作用单个文件,⽤于 Web Api 的响应或者 POST / PUT 的请求。请求中提交二进制内容:

Text
1
2
3
4
5
6
POST /user/1/avatar HTTP/1.1
Host: api.github.com
Content-Type: image/jpeg
Content-Length: 1575

一些二进制数据吧啦吧啦

对应 Retrofit 代码:

1
2
3
4
5
@POST("users/{id}/avatar")
Call<User> updateAvatar(@Path("id") String id, @Body RequestBody avatar);
...
RequestBody avatarBody = RequestBody.create(MediaType.parse("image/jpeg"), avatarFile);
api.updateAvatar(id, avatarBody)

响应中包含二进制内容:

Text
1
2
3
4
5
HTTP/1.1 200 OK
content-type: image/jpeg
content-length: 1575

一些二进制数据吧啦吧啦
  • Content-Length:代表实体正文的长度。长度表示实体正文中有几个字节,可是为什么要发送长度呢?在接收 HTTP 消息时,消息不仅可以是文字也有可能是二进制数据,如果消息只是文字,那就可以通过设置一个标志(比如说换行符\n)来确定解析范围以确保解析实体正文的结果是正确的,但是是二进制数据的话可能就会出现它数据中的某一个字节是标志,这样就会形成错误的解析范围导致解析提前终止。如果不能通过标志来自行判断什么时候结束,那就只能通过提前告知长度来判断了。

  • Content-Language:描述资源所用的自然语言。

  • Content-Encoding:实体报头被用作媒体类型的修饰符,它的值指示了已被应用到实体正文的附加内容的编码。

Range

按范围取数据,作⽤:断点续传、多线程下载。

  • Accept-Range: bytes在响应报⽂中出现,表示服务器⽀持按字节来取范围数据。
  • Range: bytes=<start>-<end>在请求报⽂中出现,表示要取哪段数据。
  • Content-Range:<start>-<end>/total在响应报⽂中出现,表示当前发送的是哪段数据以及总共有多少数据要发送。

HttpURLConnection

  • 在 Android 2.2 及之前的版本中的 HttpURLConnection 存在一些 BUG ,所以 HttpClient 是较好的选择。
  • 在 Android 2.3 及之后的版本中,使用 HttpURLConnection 则更佳(API 简单,体积较小,压缩和缓存机制可以有效地减少网络访问的流量)。主要是从 Android 6.0 API 23 开始,HttpClient 被移除了。
  • HttpURLConnection是抽象类,不能直接实例化对象,而是需要通过URLConnection类的openConnection()来获得。
  • 对 HttpURLConnection 对象的配置都需要在connect()执行之前完成,因为connect()会根据 HttpURLConnection 对象的配置生成 HTTP 头部信息。
  • connect()实际上只是建立了一个与服务器的 TCP 连接,并没有发送 HTTP 请求。HTTP 请求实际上直到获取服务器响应数据(调用getInputStream()getResponseCode()等方法)时才正式发送出去。
  • HttpURLConnection 是基于 HTTP 协议的,其底层通过 socket 通信实现。如果不设置超时,在网络异常的情况下,可能会导致程序僵死而不继续往下执行。
  • HTTP 正文的内容是通过OutputStream流写入的,向流中写入的数据不会立即发送到网络,而是存在于内存缓冲区中,待流关闭时,根据写入的内容生成 HTTP 正文。
  • 调用getInputStream()时,返回一个输入流,用于从中读取服务器对于 HTTP 请求返回的信息。
  • 可以使用connect()手动发送一个 HTTP 请求,但是在获取 HTTP 响应的时候,请求也会自动发起,比如使用getInputStream()的时候,所以其实也没有必要调用connect()

POST 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
fun main() {
thread {
useConnectionPost("https://ip.taobao.com/outGetIpInfo")
}
}

private fun getHttpURLConnection(url: String): HttpURLConnection {
var connection: HttpURLConnection? = null
try {
val mUrl = URL(url)
connection = (mUrl.openConnection() as HttpURLConnection).apply {
connectTimeout = 15000 // 连接超时时间
readTimeout = 15000 // 读取超时时间
requestMethod = "POST" // 请求参数
setRequestProperty("Connection", "Keep-Alive") // 设置请求报头
doInput = true // 接收输入流
doOutput = true // 需要传递参数时设为 true
}
} catch (ex: IOException) {
ex.printStackTrace()
}
return connection!!
}

/**
* 组织请求参数并将请求参数写入输出流
*/
private fun postParams(output: OutputStream, param: Pair<String, String>) {
val stringBuilder = StringBuilder().append(URLEncoder.encode(param.first, "UTF-8")).append("=")
.append(URLEncoder.encode(param.second, "UTF-8"))
BufferedWriter(OutputStreamWriter(output, "UTF-8")).also { writer ->
writer.write(stringBuilder.toString())
writer.flush()
writer.close()
}
}

private fun streamToString(stream: InputStream): String {
val reader = BufferedReader(InputStreamReader(stream))
val sb = StringBuffer()
var line:String? = null
while (true) {
line = reader.readLine()
if (line != null) sb.append(line + "\n")
else break
}
return sb.toString()
}

/**
* 请求连接并处理返回的结果
*/
private fun useConnectionPost(url: String) {
val connection = getHttpURLConnection(url)
try {
postParams(connection.outputStream, Pair("ip", "203.119.241.32"))
connection.connect()
val inputStream = connection.inputStream
val code = connection.responseCode
println("code: $code \n result: ${streamToString(inputStream)}")
inputStream.close()
} catch (e: IOException) {
e.printStackTrace()
}
}

这里是访问淘宝 IP 地址库,将会得到一串 JSON 格式的数据。

GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
fun main() {
thread {
useConnectionGet("https://www.google.com")
}
}

private fun getHttpURLConnection(url: String): HttpURLConnection {
var connection: HttpURLConnection? = null
try {
val mUrl = URL(url)
connection = (mUrl.openConnection() as HttpURLConnection).apply {
connectTimeout = 15000 // 连接超时时间
requestMethod = "GET" // 请求参数
doInput = true // 接收输入流
doOutput = false // 需要传递参数时设为 true
}
} catch (ex: IOException) {
ex.printStackTrace()
}
return connection!!
}

private fun streamToString(stream: InputStream): String {
val reader = BufferedReader(InputStreamReader(stream))
val sb = StringBuffer()
var line: String?
while (true) {
line = reader.readLine()
if (line != null) sb.append(line + "\n")
else break
}
return sb.toString()
}

/**
* 请求连接并处理返回的结果
*/
private fun useConnectionGet(url: String) {
val connection = getHttpURLConnection(url)
try {
val inputStream = connection.inputStream
val code = connection.responseCode
println("code: $code \n result: ${streamToString(inputStream)}")
inputStream.close()
} catch (e: IOException) {
e.printStackTrace()
}
}

GET 比 POST 简单些,这里访问 Google ,将得到 200 状态码以及网页源码。

参考资料

OkHttp

OkHttp 是一个高效的 HTTP 客户端。

GET 请求

1
2
3
4
5
6
7
8
// 创建一个客户端实例
val client = OkHttpClient()
// 创建一个请求
val request = Request.Builder().url("http://www.baidu.com").build()
// 调用 newCall() 来创建一个 Call 对象,并调用其 execute() 来发送请求并获取服务器返回的数据
val response = client.newCall(request).execute()
// 将服务器返回的数据进行打印
response.body?.let { println(it.string()) }

执行代码将打印百度的网页源码。

POST 请求

OkHttp 3 在 POST 请求中用FormBody取代了 OkHttp 2 中FormEncodingBuilder这个类。

1
2
3
4
val requestBody = FormBody.Builder().add("ip", "59.82.61.78").build()
val request =
Request.Builder().url("https://ip.taobao.com/outGetIpInfo").post(requestBody).build()
OkHttpClient().newCall(request).execute().body?.let { println(it.string()) }

同样访问淘宝 IP 库,执行代码将输出:

{“data”:{“area”:””,”country”:”中国”,”isp_id”:”xx”,”queryIp”:”59.82.61.78”,”city”:”济南”,”ip”:”59.82.61.78”,”isp”:”XX”,”county”:””,”region_id”:”370000”,”area_id”:””,”county_id”:null,”region”:”山东”,”country_id”:”CN”,”city_id”:”370100”},”msg”:”query success”,”code”:0}

JSON 解析

比起 XML ,JSON 的主要优势在于体积更小,在网络上传输的时候更省流量。缺点在于语义性较差,看起来不如 XML 直观。

JSONObject

JSONObject是 Android 自带的 JSON 解析,并且只能在 Android 中使用,如果在 Java 中使用就会报错:java.lang.RuntimeException: Stub!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val jsonData = """
[{"name": "Aiden","age": 23}, {"name": "Marcus","age": 25}]
""".trimIndent()
try {
val jsonArray = JSONArray(jsonData)
for (i in 0 until jsonArray.length()) {
jsonArray.getJSONObject(i).also {
Log.d("@@@", "name is ${it.getString("name")}")
Log.d("@@@", "age is ${it.getInt("age")}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}

上述例子中首先编写了一个 JSON 字符串,然后使用JSONArray()对其进行解析,得到一个JSONArray对象。然后循环遍历这个 JSONArray ,从中取出的每一个元素都是一个JSONObject对象。然后再在 JSONObject 中根据键名取出值。所以输出结果为:

name is Aiden
age is 23
name is Marcus
age is 25

GSON

GSON 是 Google 提供的用于将 Java 对象转换成 JSON 数据或者把 JSON 数据转换成 Java 对象的 序列化/反序列化 库。

1
2
3
4
5
6
7
8
9
10
fun main() {
val jsonData = """
{"name": "Aiden","age": 23}
""".trimIndent()

val aiden = Gson().fromJson(jsonData, Person::class.java)
println("name is ${aiden.name}, age is ${aiden.age}")
}

data class Person(val name: String, val age: Int)

输出结果是:

name is Aiden, age is 23

如果是 JSON 数组的话会稍微麻烦一些,需要借助TypeToken将期望解析成的数据类型传入fromJson()中。

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
val jsonData = """
[{"name": "Aiden","age": 23}, {"name": "Marcus","age": 25}]
""".trimIndent()

val typeOf = object : TypeToken<List<Person>>() {}.type
val people = Gson().fromJson<List<Person>>(jsonData, typeOf)
for (person in people) {
println("name is ${person.name}, age is ${person.age}")
}
}

data class Person(val name: String, val age: Int)

其它概念

Cache

Cache(缓存)和 Buffer(缓冲)的区别,缓存意在多次使用,缓冲常常出现在工作流中。例如路由器的网络请求很多,那么无法处理的请求将会存储在缓冲中,当能够处理的时候再从缓冲中取出请求,这种情况属于上游生产过快,下游消费不动。还有一种情况是下游将会进行大量消费,所以上游需要先进行大量生产并存储以供下游稍后进行大量消费(麦当劳在下班前会大量准备汉堡)。

REST

REST 是一种架构风格,其具有以下特点:

  • Client-server architecture(CS 架构,服务器负责数据,客户端负责显示)

  • Statelessness(无状态)

  • Cacheability(可缓存)

  • Layered system(分层性,即客户端在连接服务器(集群)时应该是无感知的)

  • Code on demand(服务器返回的数据中可以包含可执行的代码(Javascript))

  • Uniform interface(统一接口)

    • Resource identification in requests(通过请求来描述资源,例如通过一个 URL 来访问一个资源)
    • Resource manipulation through representations
    • Self-descriptive messages(自描述信息,当服务器返回资源时,需要给客户端进行解释说明返回的是什么类型,也就是Content-Type中的内容)
    • Hypermedia as the engine of application state(简称HATEOAS,简单理解就是类似于api.github.com,作为一个主页,提供了所有可访问的子页面(API))

事实上 HTTP 也具备以上绝大部分特点,也就是说 REST 本身就是使用 HTTP 的一种规范,只要正确使用 HTTP 就是 RESTful :

  • 使⽤资源的格式来定义 URL 。
  • 规范地使⽤ method 来定义⽹络请求操作。
  • 规范地使⽤ status code 来表示响应状态。
  • 其他符合 HTTP 规范的设计准则。

网络编程
http://example.com/post/Network-programming/
发布于
2022年9月4日
更新于
2023年2月28日
许可协议