之前在项目中简单的使用过,但是追究使用它的缘由、优点以及原理,在这之前笔者也是模糊不清,所以在这期间,做了比较系统的了解后,在此记录一番。
话不多说,切入正题。可能在了解到这个协议的时候,大多数人都不知道它是做什么,或者说不知道为什么需要这个协议,那么我们就从基础开始,一点点的了解。
还是现在这里粘一个下面代码的github地址。
一、WebSocket基本知识
1.1 WebSocket简单介绍
WebSocket是一种 网络传输协议(说到网络协议,大家可能会立马想到HTTP协议,下面会有两者的对比),可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的链接,并进行双向数据传输。
WebSocket协议规范将 ws
(WebSocket)和 wss
(WebSocket Secure)定义为两个新的统一资源标识符,分别对应明文和加密连接。
1.2 为什么需要WebSocket
WebSocket最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。
在这个协议之前,很多网站为了实现 推送技术,所用的技术都是轮询。轮询是在特定的时间间隔,由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会消耗很多的带宽资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
1.3 WebSocket特点
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
1.4 WebSocket优点
在上面简单的介绍WebSocket之后,想必大家也都可以总结出一些WebSocket的优点,下面相较于HTTP再做进一步的总结
-
较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
-
更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少
-
保持连接状态:Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
-
更好的二进制支持:Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
二、WebSocket进阶知识
简单来讲,WebSocket协议由两部分组成:建立连接过程(握手) 和 数据传输。
2.1 建立连接(握手)
在第一部分的介绍中,我们提到,WebSocket在创建持久性连接之前,需要进行一次握手,而且为了兼容性考虑,WebSocket复用了HTTP的握手通道。具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成之后,后续的数据交换则遵照WebSocket的协议。
客户端:升级协议版本
首先,客户端发起协议升级请求,从下图可以看到,采用的是标准的HTTP报文格式,且只支持 GET
方法。
重点说明一下上面四处的意义:
Connection:Upgrade
:表示要升级协议Upgrade:WebSocket
:表示要升级到WebSocket协议Sec-WebSocket-Key
与后面服务器端响应首部的Sec-WebSocket-Accept
是配套的,提供基本的防护,比如恶意的链接,或者无意的连接Sec-WebSocket-Version: 13
:表示WebSocket的版本。如果服务器不支持该版本,需要返回一个Sec-WebSocket-Version
的header
,里面包含服务端支持的版本号
注意,上面请求省略了部分非重点请求首部。由于是标准的HTTP请求,类似Host、Origin、Cookie等请求首部会照常发送。在握手阶段,可以通过相关请求首部进行 安全限制、权限校验等。
服务端:响应协议升级
服务端返回内容如下,状态码 101
表示协议切换。到此完成协议升级,后序的数据交互都按照新的协议进行。
Sec-WebSocket-Accept的计算
Sec-WebSocket-Accept
根据客户端请求首部的Sec-WebSocket-Key
计算出来。
计算公式为:
将Sec-WebSocket-Key
跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接。
通过SHA1
计算出摘要,并转成base64
字符串。
伪代码如下:
>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
2.2 数据传递
一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。
因为此处涉及到了数据帧的知识,所以可以先查看2.3 数据帧格式的部分。
1、数据分片
WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN
的值来判断,是否已经收到消息的最后一个数据帧。
FIN=1
表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0
,则接收方还需要继续监听接收其余的数据帧。
此外,opcode
在数据交换的场景下,表示的是数据的类型。0x01
表示文本,0x02
表示二进制。而0x00
比较特殊,表示延续帧(continuation frame
),顾名思义,就是完整消息对应的数据帧还没接收完。
2、数据分片例子
直接看例子更形象些。下面例子来自MDN,可以很好地演示数据的分片。客户端向服务端两次发送消息,服务端收到消息后回应客户端,这里主要看客户端往服务端发送的消息。
第一条消息
FIN=1
, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。
第二条消息
-
FIN=0,opcode=0x1
,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧。 -
FIN=0,opcode=0x0
,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。 -
FIN=1,opcode=0x0
,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
2.3 数据帧格式
客户端、服务端数据的交换,离不开数据帧格式的定义。所以我们在这里看一看WebSocket的数据帧格式。
客户端、服务端数据的交换,离不开数据帧格式的定义。因此,在实际讲解数据交换之前,我们先来看下WebSocket的数据帧格式。
WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。
-
发送端:将消息切割成多个帧,并发送给服务端;
-
接收端:接收消息帧,并将关联的帧重新组装成完整的消息;
本节的重点,就是讲解数据帧的格式。
1、数据帧格式概览
下面给出了WebSocket数据帧的统一格式。
从左到右,单位是比特。比如FIN、RSV1
各占据1比特,opcode
占据4比特。
内容包括了标识、操作代码、掩码、数据、数据长度等。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
2、数据帧格式详解
针对前面的格式概览图,这里逐个字段进行讲解,可以参考协议规范。
FIN:1个比特。
如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。
RSV1, RSV2, RSV3:各占1个比特。
一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。
Opcode: 4个比特。
操作代码,Opcode
的值决定了应该如何解析后续的数据载荷(data payload
)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection
)。可选的操作代码如下:
-
%x0
:表示一个延续帧。当Opcode
为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。 -
%x1
:表示这是一个文本帧(frame) -
%x2
:表示这是一个二进制帧(frame) -
%x3-7
:保留的操作代码,用于后续定义的非控制帧。 -
%x8
:表示连接断开。 -
%x9
:表示这是一个ping
操作。 -
%xA
:表示这是一个pong
操作。 -
%xB-F
:保留的操作代码,用于后续定义的控制帧。
Mask: 1个比特
表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
如果Mask
是1,那么在Masking-key
中会定义一个掩码键(masking key
),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask
都是1。
Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位
假设数Payload length === x
,如果
-
x为0~126:数据的长度为x字节。
-
x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
-
x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
此外,如果payload length
占用了多个字节的话,payload length
的二进制表达采用网络序(big endian,重要的位在前)。
Masking-key:0或4字节(32位)
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key
。如果Mask为0,则没有Masking-key
。
备注:载荷数据的长度,不包括mask key的长度。
Payload data:(x+y) 字节
载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。
-
扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
-
应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。
3、掩码算法
掩码键(Masking-key
)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:
首先,假设:
-
original-octet-i
:为原始数据的第i字节。 -
transformed-octet-i
:为转换后的数据的第i字节。 -
j
:为i mod 4
的结果。 -
masking-key-octet-j
:为mask key
第j字节。
算法描述为: original-octet-i
与 masking-key-octet-j
异或后,得到 transformed-octet-i
。
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
2.4 连接保持(保持长连接)
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
- 发送方->接收方:
ping
接收方->发送方:`pong` `ping`、`pong的操作`,对应的是WebSocket的两个控制帧,`opcode
分别是0x9、
0xA`。
在这一部分最后,在说明两个知识点(不做详细说明)
1.Sec-WebSocket-Key/Accept
的作用:主要作用在于提供基础的防护,减少恶意连接、意外连接。
作用大致归纳如下:
-
避免服务端收到非法的websocket连接(比如http客户端不小心请求连接websocket服务,此时服务端可以直接拒绝连接)
-
确保服务端理解websocket连接。因为
ws
握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key
来确保服务端认识ws
协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key
,但并没有实现ws协议。。。) -
用浏览器里发起ajax请求,设置header时,
Sec-WebSocket-Key
以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade
) -
可以防止反向代理(不理解ws协议)返回错误的数据。比如反向代理前后收到两次
ws
连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回)。 -
Sec-WebSocket-Key
主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept
的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。
2. 数据掩码的作用:
WebSocket协议中,数据掩码的作用是增强协议的安全性(并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks
)等问题。)。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。
三、 WebSocket实例
3.1 客户端代码示例
<input type="text" id="sendTxt">
<button id="sendBtn">发送</button>
<div id="recv"></div>
<script>
/**
* WebSocket对象作为一个构造函数,用于新建WebSocket实例
* 执行下面的语句之后,客户端就会个服务器进行连接
*/
let webSocket = new WebSocket("wss://echo.websocket.org");
/**
* 下面结合实际讲一下WebSocket实例对象的属性和方法
* 1. 属性
* 1.1 webSocket.readyState(属性返回实例对象的当前状态)
* . CONNECTING:值为0,表示正在连接。
* . OPEN:值为1,表示连接成功,可以通信了。
* . CLOSING:值为2,表示连接正在关闭。
* . CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
*/
/**
* 1.2 webSocket.onopen(用于指定连接成功后的回调函数)
*/
webSocket.onopen = function () {
console.log("webSocket open");
document.getElementById('recv').innerHTML = "Connected";
};
/**
* 1.3 webSocket.onclose(用于指定连接关闭之后的回调函数)
*/
webSocket.onclose = function () {
console.log("webSocket close");
}
/**
* 1.4 webSocket.onmessage(用于指定收到服务器数据后的回调函数)
*/
webSocket.onmessage = function (e) {
console.log(e.data);
document.getElementById('recv').innerHTML = e.data;
}
//发送信息
document.getElementById('sendBtn').onclick = function () {
var text = document.getElementById('sendTxt').value;
/**
* 2. 方法
* 2.1 webSocket.send() (用于向服务器发送数据)
*/
webSocket.send(text);
}
</script>
客户端的API上面的代码中有简单的介绍以及使用,如果想要查看更加具体的文档说明,可以在MDN进行查看。
对比于服务端的实现,客户端的使用略显简单,那么接下来我们继续实现服务端的WebSocket。
3.2 服务端的实现
因为笔者目前局限于JS,所以服务端的实现是使用的Node,常用的Node实现有以下三种:
-
µWebSockets
-
Socket.IO
-
WebSocket-Node
因为在项目中使用的是Socket.IO
,所以在这里笔者就结合自己的亲身经历去讲解以下,其他的实现方式应该也是差不多的,有兴趣的话是可以自己实现一些的。
Socket.IO
想实现双向通信,当然WebSocket是必不可少的技术了,不过Socket.IO
不仅仅是WebSocket的封装,在不支持WebSocket的环境中,Socket.IO
还有多种轮询解决方案,确保它能够正常运行。
既然用到了Socket.IO
,那我们就要扒一扒有关于它的介绍,基本使用等等内容,先在这里贴一个官方文档。因为官方文档为全英,这里笔者找到了一个中文文档,建议两者对比着看,有能力的当然还是看全英的比较好,内容更加准确。
Socket.IO
Socket.Io
主要由两个部分组成:
-
socket.io
模块,集成到Node.js
的http
模块的服务器 -
socket.io-client
,在浏览器中运行的客户端Socket.Io
支持多种传输机制,例如WebSocket、Adobe Flash Sockets、XHR轮询、JsonP轮询
,它们被隔离在统一的接口之下,这意味着任何浏览器都可以作为客户端。
标准的WebSocket服务器并不能和Socket.Io
客户端进行直接通信,需要注意这一点。
1.1 介绍
Socket.io
是一个WebSocket库,包括了客户端的js
和服务器端的nodejs
,它的目标是构建可以在不同浏览器和移动设备上使用的实时应用。它会自动根据浏览器从WebSocket
、AJAX
长轮询、Iframe
流等等各种方式中选择最佳的方式来实现网络实时应用,非常方便和人性化,而且支持的浏览器最低达IE5.5。
1.2 使用
现在Node.js的框架非常的多,譬如:Express、ThinkJS、Koa、Egg.js等,每一个框架有可能进一步对Socket.io
进行了封装,比如笔者使用过的Egg.js框架就提供了 egg-socket.io
插件,使用这些插件就要遵循框架的一些约束,所以对于框架中的使用,还是需要读者根据文档要求使用,因为这个因素,所以读者只在这里介绍在不使用任何框架的情况下的使用。
安装
$ npm install socket.io
使用 Node http 服务器
先直接上代码(最基础),之后会根据官方文档讲解其他内容。
// index.html
<script src="./node_modules/socket.io-client/dist/socket.io.js"></script>
<script>
let socket = io('http://localhost');
socket.on('news', (data) => { //监听'news'事件,有结果后输出
console.log(data);
socket.emit('my other event', { //触发'my other event'事件
my: 'data'
})
})
</script>
// app.js
let app = require('http').createServer(handler);//使用Node创建一个Http服务
let io = require('socket.io')(app); //此处为绑定上面创建的服务器
let fs = require('fs');
app.listen(80);
var handler = (req, res) => {
fs.readFile(__dirname + './index.html', (err, data) => {
if (err) {
res.writeHead(500);
return res.end('Error loading index.html');
}
res.writeHead(200);
res.end(data);
})
}
io.on('connection', (socket) => {
socket.emit('news', { //触发'news'事件
hello: 'world'
});
socket.on('my other event', (data) => { //监听'my other event'事件,有结果后输出
console.log(data);
})
})
上面代码完成后,运行 node app.js
,之后打开 index.html
,之后再打开浏览器的控制台,会发现浏览器的控制台上先打印出{hello: "world"}
,之后编辑器的控制台上打印出{my: "data"}
,注意两个是有先后顺序的,这个看代码就明白了,不多说。
1.3 emit和on
emit
和 on
是最重要的两个api,分别对应 发送 和 监听 事件。
-
socket.emit(eventName[, ...args])
:发射(触发)一个事件 -
socket.on(eventName, callback)
:监听一个emit
发射的事件
我们可以非常自由的在服务端定义并发送一个事件emit
,然后在客户端监听 on
,反过来也一样。
发送的内容格式也非常自由,既可以是基本数据类型 Number,String,Boolean
等,也可以是Object,Array
类型,甚至还可以是函数。而用回调函数的方式则可以进行更便携的交互。
1.2 部分的示例代码就是这两个api的使用,这里就不多说了。
1.4 广播(broadcast)
broadcast
默认是向所有的socket连接进行广播,但是不包括发送者自身。
注意:socket连接要确保是同一个命名空间下的
代码解释:
io.on('connection', (socket) => {
//发送给除自己以外的其他客户端
socket.broadcast.emit('news', {
hello: 'world'
})
})
此时,要想查看效果,可以在创建一个HTML页面,代码一样即可,之后在浏览器上同时打开两个页面,刷新一个页面时(刷新一次页面就相当于触发一次事件),本页面控制台没有输出任何内容,另一个页面的控制台则会输出内容(可以创建更多页面查看效果)。
如果想要自身也可以收到消息,此时可以
io.on('connection', (socket) => {
//发送给自己
socket.emit('news', {
hello: 'world'
})
})
1.5 命名空间(namespace)
所谓的命名空间,就是指在不同的域当中发消息只能给当前的域的socket收到。
作用:可以最大限度地减少资源(TCP连接)的数量,,并为应用提供频道划分功能。(这样多个应用模块可以共享单个TCP连接)
如果想隔离作用域,或者划分业务模块,这时候就可以使用命名空间,命名空间相当于建立新的频道,使你可以在一个socket.io服务上隔离不同的连接,时间和中间件。
默认的命名空间是/
,Socket.IO 客户端默认连接到这个命名空间,服务端默认监听的也是这个命名空间。
自定义命名空间
重要提示:命名空间是 Socket.IO 协议的一个实现细节, 与底层传输的实际 URL 无关。底层传输的实际 URL 默认是/socket.io/…
。
使用命名空间的方式一:直接在链接后面加子域名,这种方式其实还是用同一个socket服务进程---软隔离
服务端代码:
io
.of('my-nsp')
.on('connection', (socket) => {
// 发送给除自己以外的其他客户端
socket.broadcast.emit('news', {
hello: 'world'
})
//发送给自己
socket.emit('news', {
hello: 'world'
})
})
客户端需要修改的代码:
let socket = io('http://localhost:3000/my-nsp');
使用命名空间的方式二:path
参数,这种方式就是真正的重新开启了一个socket服务。
1.6 Room
这里说一下,namespace
、room
和socket
的关系
socket
会属于某一个 room
,如果没有指定,那么会有一个默认的room
。这个room
又会属于某个namespace
,如果没有指定,那么就是默认的/
。(一个命名空间下可以有多个room
)
客户端连接时指定自己属于哪一个 namespace
,服务端看到namespace
就会把这个socket
加入到指定的namespace
中,如果客户端没有指定具体的room
,则服务端会放入默认的room
,或者服务端通过代码socket.join(bar)
放入bar
的room
中。
默认情况下,每一个id
便自成一个房间,房间名是 socket.id
(指定命名空间之后,前面会带上命名空间,socket会自动加入到以此ID来标识的房间);自定义房间之后,原先的默认控件仍然存在;房间为一个对象,包含当前进入房间的sockets以及长度。
代码示例:
io
.on('connection', (socket) => {
//在服务端将一个socket加入到一个房间中
socket.join('manannan', () => {
console.log(socket.rooms);
});
//进入到该房间中,之后的事件发布仅仅在这个房间
io.to('manannan').emit('news', {
hello: 'world'
})
//离开房间
socket.leave('mananan')
})
以上内容就是基本的使用,然而在实际项目中,肯定会比这些更加复杂,这里就不一一赘述,当我们用到我们之前没有用过的东西时,一定要善于查看官方文档以及百度。
所以关于客户端API和服务端API等更多的内容,需要时查看官方文档就可以了。