关于云变量

我们先来看下云变量的样子:

Scratch团队在FAQ里解释了

什么是云变量

云变量可以让作品里保存的数据与Scratch社区的其他人所共享。你可以利用云变量发布调查或其他作品,而社区中的其他人可以访问和修改这些数据。

谁能看见云变量中存放的数据?

当你运行一个用了云变量的作品时,你使用过程中产生的数据会存放在你的用户名下,其他人可以看到这些数据。

云变量可以存放什么类型的数据?

云变量只能存放数字。

我能使用云变量创建聊天室吗?

不可以。虽然在技术上是可能的,但Scratch网站不允许这样做。

谁可以改变云变量的内容?

只有你和项目的查看者才能将数据存储在项目的云变量中。 如果人们进入项目内部源码或重新混合(remix)你的代码,将会创建变量的副本,而不会影响或更改原始变量。

可以用云变量创作多人游戏吗?

由于网速和同步的问题,创作多人游戏比较困难。但仍然有一些Scratcher别出心载,使用云变量制作回合制游戏以及其他类型的游戏。

云变量会在后台产生日志。每个项目最多有10个云变量。

Scratch新用户可能无法使用云数据。Scratch团队不希望Scratch新手滥用云变量,因为它可能会给系统带来很大的负担。

云变量会自动更新,利用这个特性,可以建立联机游戏和聊天室,但官方不希望有高频交互,这将带来服务器压力。此外,实时性也不能保证,如果你希望建立实时的强连接,参考codelab-adapter的虫洞(wormhole)


说了半天,你可以看一个带有云变量的项目Google Chrome Dino Run 2 remix

解释完了云变量,我们进入技术分析部分,我们试图回答: 云变量是怎么实现的,并给出简单的实现例子。

分析

和之前的分析一样,借助Chrome DevTools,我们来观察创建和使用云变量的过程中都发生了什么。

创建变量

注意: 你的账号不能是新注册的,否则你没有创建云变量的权限。

创建我们的第一个云变量: test_cloud_var

来看看请求细节(Headers)

Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的。websocket中似乎没有处理用户凭证相关的问题。

接着看看Frames:

向上的箭头表示的数据流向为 client(scratch-gui)-> server 向下的箭头表示的数据流向为 server-> client

创建变量的过程非常简单:

  • 建立websocket通道
  • 在websocket通道中来回传送json。采用类似json-rpc的交互方式。

后端的云变量服务地址位于wss://clouddata.scratch.mit.edu/,通过这个线索,我们可以找到与云变量相关的前端源码, 源码非常简单。

设置变量

值得注意的是,设置变量的过程,数据从前端发往服务端,服务端不做响应。

刷新项目

项目中存在云变量时,我们重新打开这个项目,默认将拉去云变量中存储的的值

删除云变量

权限问题

前端源码中似乎没看到权限相关的部分。

明天有空做个实验,试试未登录状态是否能与云变量服务进行通信。

1
2
3
4
5
6
7
8
9
var ws = new WebSocket('wss://clouddata.scratch.mit.edu/');
ws.onopen = function(evt) { 
  var data = {"method":"handshake","user":"wwj718","project_id":"291228938"}
  ws.send(JSON.stringify(data)+"\n");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
};    

实验发现只有在登陆情况下,才能建立websocket连接。websocket在建立连接的时候会携带cookie,这就是实现用户身份验证的关键。

实现一个云变量服务

下边我们给出兼容Scratch开源前端的云变量后端实现。这个实现只作为原理展示,如果你要用于生产环境,需要做些调整。

该实现基于Python的websockets

 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
import json
import asyncio
import websockets
# json-rpc

def handshake(req_json):
    # 返回项目中已有的云变量
    project_id = req_json.get("project_id")
    result = {"method":"set","project_id":project_id,"name":"☁ test","value":"0"}
    return result

def create(req_json):
    # {"method":"create","user":"wwj718","project_id":"291229535","name":"☁ test1","value":0}
    name = req_json.get("name")
    return {"method": "ack", "name": name, "reply": "OK"}

def set(req_json):
    # {"method":"set","user":"wwj718","project_id":"291229535","name":"☁ test1","value":"0"}
    return None

def delete(req_json):
    # {"method":"delete","user":"wwj718","project_id":"291229535","name":"☁ test1"}
    return None

method_map = {}
method_map["handshake"] = handshake
method_map["create"] = create
method_map["set"] = set
method_map["delete"] = delete

async def handle_cloud_data(websocket, path):
    async for message in websocket:
        # from IPython import embed;embed()
        # message结尾有换行符\n, 流
        req_json = json.loads(message.strip())
        print("request data:",req_json)
        method = req_json.get("method")
        handle = method_map.get(method)
        if handle:
            result = handle(req_json)
        await websocket.send(json.dumps(result))


asyncio.get_event_loop().run_until_complete(
    websockets.serve(handle_cloud_data, '0.0.0.0', 8765))
print("server is running")
asyncio.get_event_loop().run_forever()

测试代码与scratch-gui中的代码基本一致:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var ws = new WebSocket('ws://127.0.0.1:8765');
ws.onopen = function(evt) { 
  var data = {"method":"handshake","user":"wwj718","project_id":"291228938"}
  ws.send(JSON.stringify(data)+"\n");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
};    

// var data = {"method":"set","user":"wwj718","project_id":"291228938","name":"☁ test1","value":"1"}
// ws.send(JSON.stringify(data)+"\n")

如果你要用于生产环境,还需要处理cookie和多个客户端一同读写云变量的问题(考虑到读写频率,推荐使用redis, 如果你的后端也用python异步api实现,推荐使用aioredis)。

websocket测试工具

Socket Wrench

其他值得留意的问题

速率限制

throttle 10:

Send a message to the cloud server at a rate of no more than 10 messages/sec

this.sendCloudData = throttle(this._sendCloudData, 100);

参考: