第零章 云服务器的购买与配置

前言

2024/8/16

这篇文章本来是写给 AL 那群打物联网的选手看的,但是他们似乎积极性不高。暑假的时候陆续有人来问我问题,发现好多队伍都选择开发小程序,真的晕死。按理来说 Web 端的生态更加好,遇到问题也容易找到答案,既然都不愿意学 Web 开发,这篇文章都不知道给谁看了。罢了,发到博客上仅作为留档吧

鉴于大伙对”上云“教程的迫切需求,我决定开设这个课程,不同于传统的web全栈教学路线,该课程第一章就上云。随着章节的不断推进,会逐渐完善部署在云上的物联网 Web 应用

选型与购买

首先我们要知道在哪能买到云服务器

  1. 华为云
  2. 腾讯云
  3. 阿里云

然后我们要知道怎么买,这里以华为云为例,首先登陆华为云账号,进入右上角的控制台

截图

然后在侧边栏找到 “弹性云服务器 ECS”,点进去后找到 “购买弹性云服务器”

截图

截图

我们打比赛不需要配置多么好的云服务器,毕竟到了比赛那天,访问你 web 服务的用户也不会超过20个,拿个比较差的机器都能跑了

一般来讲,华为杯每个华为赛道的队伍能够领到 400 多人民币的代金券(到你们这届的政策我就不清楚了,反正我这届是这样的)。所以接下来我会按照 300 – 400 RMB 为预算来选型

首先区域选择 华南-广州(其实差别不大,随便选都行),计费模式选 包年/包月

截图

内存选择 2GB 到 4GB 的型号即可,CPU 性能要求也不高,所以从 通用入门型 里选即可

这里选的是 t6.large.1 规格的 ECS,如果你预算充足,可以买 t6.large.2 甚至配置更高的型号

接下来系统镜像选择 Ubuntu 20.04,这几乎是目前最好的选择了。然后系统盘大小就用最小的 40 GB(放心,我可以保证到华为杯结束,磁盘占用都不会超过 30 GB)。最后购买量选择 1 台、3 个月(或者 2 个月,看你们什么时候决赛)

截图

下一步,网络配置这边,安全组先不用管,弹性公网IP这边可以参考下图配置。3 个月下来,流量费用花费应该是 10-30 RMB

截图

下一步,高级配置,设置一下云服务器的密码,然后不购买云备份。如果你真的害怕把服务器搞炸,那就买,大约会花30人民币,不过你大概率用不上这个功能

截图

最后确认配置,付款

然后你就能在 云服务器控制台 页面上看到你刚购买的云服务器,在里面能找到它的公网 ip

img

安全组配置与 ssh 远程登陆

安全组属于云服务厂商为我们设置的安全屏障,在不同的端口中有不同的安全策略,以限制外部的访问

但是默认的安全组只放开了一些常用端口,当我们有个性化的端口访问需求的时候,就要求我们去更改安全组配置

  1. 在哪配置安全组

在云服务器控制台的侧边栏找到“安全组”选项,点进去就是安全组管理面板

截图

  1. 怎么配置

优先级使用 1 即可,然后协议端口可以使用逗号分隔以同时开放多个。源地址表示网络请求的来源,这里的 0.0.0.0/0 表示所有 ip,也就是说,下图这个配置的意义是:允许来自所有 ip 的网络请求访问 1883,8883,18083 端口

截图

我们需要开放的端口是:ssh端口:22、http/https端口:80,443、MQTT端口:1883、8883、18083

在设置好安全组后,记得将其应用到你购买的云服务器上

img

接下来我们使用 ssh 远程登陆至云服务器,从 windows10 开始,系统内置了开源的 ssh 客户端 OpenSSH,使用方法是

在终端输入以下命令后回车

ssh {username}@{ip}
ssh: 启动 OpenSSH 应用
{username}: ssh 登录身份
{ip}: ssh 目标 ip

如果不指定连接端口的话, OpenSSH 将会尝试以 22 端口连接。如果你更改了默认的 ssh 端口,请使用以下方式指定连接端口

ssh {username}@{ip} -p {port}
{port}: 指定的连接端口

在输入登录密码时,将不会有“星号”代表输入的字符,这是正常的,不要以为是自己没输进去。linux 中许多输入密码的场合也是这样

登录成功后将进入云服务器的系统终端

img

生产环境配置

第一步 安装服务器运维面板 1Panel/或者宝塔面板

1Panel 官网 1Panel 安装手册 宝塔面板官网

安装方式就是 ssh 登录你的云服务器,然后在终端输入以下命令后回车

curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh

在核心组件都安装完成后,会让你设置 1Panel 的目标端口和安全入口,然后我们要在华为云控制台的安全组中开放该端口

截图

接下来还会让你设置登录账号和密码,设置密码的时候,不会有星号提示

第二步 登录 1Panel,访问 http://云服务器ip:目标端口/安全入口 即可进入登陆页面

img

截图

第三步 配置面板防火墙,设置参考云服务器的安全组

截图

第四步 安装 EMQX 代理平台

该平台用于实现硬件与服务器之间的 MQTT 通信,安装命令如下

(确保安装包标注的系统版本和云服务器系统版本一致,下面的是 ubuntu20.04 的安装包)

wget https://www.emqx.com/zh/downloads/broker/5.6.1/emqx-5.6.1-ubuntu20.04-amd64.tar.gz
mkdir -p /opt/emqx && tar -zxvf emqx-5.6.1-ubuntu20.04-amd64.tar.gz -C /opt/emqx

第五步 安装 miniconda 并创建虚拟环境

执行下面的命令以下载并启动 miniconda 安装包

wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-py310_24.3.0-0-Linux-x86_64.sh
chmod u+x Miniconda3-py310_24.3.0-0-Linux-x86_64.sh && ./Miniconda3-py310_24.3.0-0-Linux-x86_64.sh

安装时建议不要使用默认的安装位置,将其设置为 /opt/miniconda3

完成后执行 conda info 检查安装情况,执行 conda init 初始化miniconda,然后退出终端重新进入,开始配置虚拟环境

接下来依次执行:

conda create -n myenv python=3.11  # 创建一个名为 myenv 的虚拟环境, 指定python版本为3.11
conda activate myenv               # 进入虚拟环境 myenv
pip install xxx                    # 在虚拟环境中安装第三方库, xxx是要安装的库
conda deactivate                   # 退出当前虚拟环境

执行完毕后就配置完成了 python 开发环境(miniconda 的更多使用方式请自行上网查找)

至此,云服务器的开发环境已配置完成

第一章 web前端速成

前端技术简述

前端涉及三个编程语言 HTML CSS JS

用一句话概括 Web 前端,那就是“给用户看的都是前端”。由于前端语言简单且琐碎,所以:不要学太深刻,这是浪费时间

要想用最快的速度获得构建 web 前端的能力,首先你需要:知道每个部分能做什么

所以你应该记住这样一句话:HTML 是网页的骨架,CSS 是网页的皮肤和衣服,JS是网页的思考核心

然后你需要:知道怎么描述自己的需求,让 AI 帮你构建网页的框架

接下来你需要:修改 AI 提供的代码以适应一些细节需求

会写是必须的,不能一点都不会,这篇文章里面有教学视频:–AutoLeaders–2023全栈组系列教程 第4章 HTML与CSS

请开二倍速看,速览一遍 HTML 和 CSS 都有些什么内容,并且没必要做上面文章里的作业

最后你需要:在忘记怎么写的时候查手册

手册:

  1. HTML https://www.runoob.com/html/html-tutorial.html
  2. CSS https://www.runoob.com/css/css-tutorial.html
  3. JS https://www.runoob.com/js/js-tutorial.html

部署静态网站

嗯,我说过第一节课就上云,所以我们先从最原始的网站部署方式:静态网站,开始体验“上云”

首先登录 1Panel 面板,将 html、css、js 等静态文件上传到 /var/www/html 文件夹(没有这个文件夹就自己创建)

截图

然后在云服务器的终端执行以下命令

cd /var/www/html
sudo python3 -m http.server 80

然后使用浏览器访问 http://云服务器ip/ 就能看到以下页面

截图

改变 URL 到 http://云服务器ip/test.html,就会发现我们成功访问了由 test.html 构建的页面

截图

此时我们就成功地将一个静态 web 服务部署到云服务器上了。回到终端,使用 ctrl+C 即可停止该服务

第二章 ip 协议和 http 协议

这节课不会讲 ip 协议和 http 协议的原理、流程等细节,只会讲我们在 web 开发时会怎么运用这两个协议

ip 协议

互联网上以 ip地址 标识每个设备的身份,所以当你要与某个设备进行沟通时,就要用 ip 协议来找到它

这便是 ip 协议需要我们掌握的最重要的用途:”找人“

我们可以看看自己的电脑的 ip 地址,Windows 系统使用 ipconfig 即可查看

截图

一个设备可以拥有多个 ip 地址,因为一个设备可以同时处在不同的局域网中,以上图为例,我的 PC 连接了深大的校园网,于是有

无线局域网适配器 WLAN:

   连接特定的 DNS 后缀 . . . . . . . :
   IPv6 地址 . . . . . . . . . . . . : 2001:250:3c00:239:7771:7807:34bb:115a
   临时 IPv6 地址. . . . . . . . . . : 2001:250:3c00:239:881a:6c67:92a5:c6d6
   本地链接 IPv6 地址. . . . . . . . : fe80::f351:12c4:1ed0:976e%3
   IPv4 地址 . . . . . . . . . . . . : 192.168.239.144
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : fe80::ae74:9ff:fe17:8a01%3
                                       192.168.239.33

这里的 192.168.239.144 是我的 PC 在校园网中的 ip 地址,此时同时处于校园网内的其他设备,就可以通过 192.168.239.144 访问我的 PC

同时,我的 PC 安装了 wsl2,所以我的 PC 同时处在 wsl 的局域网中,于是有

以太网适配器 vEthernet (WSL):

   连接特定的 DNS 后缀 . . . . . . . :
   本地链接 IPv6 地址. . . . . . . . : fe80::b3a9:4a96:f186:5585%51
   IPv4 地址 . . . . . . . . . . . . : 172.17.240.1
   子网掩码  . . . . . . . . . . . . : 255.255.240.0
   默认网关. . . . . . . . . . . . . :

这里的 172.17.240.1 是我的 PC 在 wsl 局域网中的 ip 地址,此时我安装的 wsl,就可以通过 172.17.240.1 访问我的 PC

我们购买的云服务器位于公网,其 ip 也被称作公网 ip,我的 PC 位于内网,上面的两个 ip 地址是内网 ip。我的 PC 可以 ping 通我的服务器,但我的服务器却 ping 不通我的 PC

http 协议

Http 协议是互联网极其常用的协议,其传递信息的形式基于 http报文,一个简单的 http 请求报文(Request)一般是这样的形式

GET /url?a=1&b=2 HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Connection: close

data

截图

一个简单的 http 响应报文(Response)一般是这样的形式

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.10.9
Date: Thu, 16 May 2024 08:00:50 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 4
Connection: close

data

截图

这里比较麻烦的概念是 请求方法 http状态码

请求方法有非常多,我们甚至可以自定义请求方法,一般来说,常用的请求方法有 GETPOST

GET 方法是 http 协议的默认请求方法,你在浏览器中随便输入一个 url,就会通过 GET 方法来访问,该方法用于向服务器获取信息

POST 方法用于向服务器提交信息,提交的内容放在请求体中。请求体支持不同的格式,常用的格式有:纯文本、json、XML、表单等等

请求体的格式通过 Content-Type 请求头进行指定,参照:HTTP content-type

这里重点讲解 表单格式json 格式

一个使用表单格式的 POST 请求一般像这样

POST /login HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Content-Type: application/x-www-form-urlencoded
Connection: close

username=admin&password=admin123

表单使用键值对的方式来表示信息,以 & 符号分隔不同的键值对

一个使用 json格式 的 POST 请求一般像这样

POST /login HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Content-Type: application/json
Connection: close

{
  "username": "admin",
  "password": "admin123"
}

json 格式表示信息的方法和 python 的字典、js 的对象很像,由于其属于通用格式,在大多数编程语言中都能使用,所以在web领域广泛应用

要具体了解 json 格式,请参照:JSON 教程

其他请求方法可以参考 HTTP 请求方法

接下来讲 http 状态码,它的数量也很多,我们没必要全部记住,比较重要的状态码有

200 OK: 请求成功,一切正常

302 Found: 资源重定向

403 Forbidden: 一切正常,但是服务器拒绝处理该请求

404 not found: 服务器无法根据客户端的请求找到资源

500 Internal Server Error: 服务器内部错误,一般是后端代码报错了

根据不同的状态码,我们可以知道 http 请求的处理情况,想要了解其他状态码请看:HTTP 状态码

Cookie 技术

Cookie 以键值对的方式存储在本地浏览器中,在发送请求报文时,将会把 Cookie 添加到请求头中

GET /home HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Cookies: userID=123abc;token=567bbd1c33e7
Connection: close


cookie 一般用于身份的校验

使用 fetch

fetch 是 Javascript 用于 发送 http 请求处理 http 响应 的 API,这个 API 比较新, Javascript 还有一套旧的 API 可以处理 http 报文,叫做 XMLHttpRequest,这里不作介绍

fetch API 的用法可以参考文档:使用 Fetch

前端与后端的沟通就要靠 fetch 来发送报文,如果你使用了其他语言/框架,则使用其提供的 API 来发送 http 报文,例如 python 的 requests 库、Vue 框架的 axios

fetch 的一些使用例

// 请求url: http://example.com/api?a=1
fetch("http://example.com/api?a=1", {
    // 设置请求方法
    method: "POST",
    // 设置请求头
    headers: {
        // 没有这个请求头后端不认 json 的
        "Content-Type": "application/json",
    },
    // 设置请求体
    body: JSON.stringify({
        username: "caterpie",
        password: "admin123",
    }),
})
.then((resp) => resp.json()) // 当响应为 json 格式时的解析方法
.then((data) => console.log(data))
// 定义异步函数(使用关键字 async)
async function send_form(url, form_id) {
    // 获取表单内容
    const form = new FormData(document.getElementById(form_id));
    // 等待 fetch 完成(使用关键字 await)
    const resp = await fetch(url, {
        method: "POST",
        headers: {
            "Content-Type": "x-www-form-urlencoded",
        },
        body: form, // 表单内容放置在请求体
    });
    return resp;
}

第三章 flask 开发速成

flask 比较系统的教程不多,建议看 Flask中文文档(2.1.x),本章只介绍一些常用的 API

安装 flask

flask 是 python 的一个轻量级的后端框架,安装方式非常的简单,使用:

pip install flask

记得在虚拟环境中安装,以防出现意外损坏主环境

flask 本身只提供了一些基础的后端API,更多的功能是通过扩展来提供的,可以参考 Flask常用扩展(Extentions)

本教程不会涉及到任何 flask 扩展,大家了解即可

第一个 flask 应用

这是一个最简单的 flask 应用:

from flask import Flask # 导入 flask 库

app = Flask(__name__)   # 定义 flask 应用

@app.route('/')         # 该装饰器将函数 index 与 / 路由绑定
def index():            # 当访问 / 路由时, 将会触发 index 函数
    return "hello flask"

if __name__ == "__main__":
    app.run()           # 启动 flask 服务, 默认端口是 5000

运行后访问 http://127.0.0.1:5000/ 即可看到

img

接下来稍微添加一点东西

@app.route('/hi/')
def say_hi():
    return "hi!"

@app.route('/get_name/<string:name>') # 这里使用了动态路由
def get_name(name):
    return f"you are {name}"

此时访问 http://127.0.0.1:5000/hi/,能得到:

截图

访问 http://127.0.0.1:5000/get_name/caterpie,能得到:

截图

这个例子体现了 flask 的路由控制功能,我们能通过这种方式实现:用户访问不同的 url 执行不同的逻辑,动态路由的规则请参考:https://dormousehole.readthedocs.io/en/latest/quickstart.html#id4

上面的 index() say_hi() get_name() 三个函数,被称作视图函数

发送静态文件

flask 默认将静态文件存放在同目录下的 static 文件夹中

使用下面的方式,可以使用户通过 http://youip:5000/static/filepath 访问静态文件夹的内容

from flask import Flask, send_from_directory

app = Flask(
           __name__,
           static_folder="my_static_file", # 指定静态文件夹位置, 默认为 static
          )

@app.route('/static/<path:filepath>')
def send_static_file(filepath):
    return send_from_directory(app.static_folder, filepath)

if __name__ == "__main__":
    app.run()

我们可以将 html、css、js 等文件放置在静态文件夹中供用户访问(有时 html 会使用模板来渲染,一般放在 templates 目录,这里不展开)

来改进一下第一个 flask 应用,此时访问 http://127.0.0.1:5000/ 返回的就是一个 html 页面

from flask import Flask, send_from_directory

app = Flask(__name__)

@app.route('/static/<path:filepath>')
def send_static_file(filepath):
    return send_from_directory('static', filepath)

@app.route('/')
def index():
    return send_static_file('html/index.html')

if __name__ == "__main__":
    app.run()

处理 http 报文

flask 使用 request 类来获取 http 请求报文中的信息

处理不同的请求方法:对于不同的请求方法,flask 可以执行不同的逻辑,有两种写法

第一种,在 route 装饰器中,用 methods 参数指定该函数支持的请求方法,不指定的情况下,只回应 get 请求

from flask import Flask, request

app = Flask(__name__)

@app.route('/', methods=["GET", "POST"])
def index():
    if request.methods == "GET":
        return "get"
    # for post
    return "post"

第二种,对于 get 和 post 请求,flask 有一种快捷的写法

@app.get("/")
def index_get():
    return "get"

@app.post("/")
def index_post():
    return "post"

处理查询参数:使用 request.args 来获取查询参数,该属性是一个包含了查询参数讯息的字典

添加以下内容

@app.route('/query/')
def query():
    print(request.args)
    return request.args.get('a')

此时访问 http://127.0.0.1:5000/query/?a=123&b=456,就会发现控制台中打印了:

截图

浏览器页面为:

截图

处理请求体:使用 request.form 来获取表单格式的请求体,使用 requset.json 来获取 json 格式的请求体

@app.post('/login/')
def login_post():
    content_type = request.headers.get('Content-Type')
    if content_type == "application/x-www-form-urlencoded":
        username = request.form.get("username")
        password = request.form.get("password")
    elif content_type == "application/json":
        username = request.json.get("username")
        password = request.json.get("password")
    else:
        return "不支持的格式"
    return f"you username is {username}, password is {password}"

对于文件上传的场景,文件的二进制内容也是放在请求体中的,我们该怎么处理呢

flask 提供了 request.files 来获取用户上传的文件,下面是一个处理文件上传的例子

# app.py
from flask import (
    Flask,
    send_from_directory,
    request
    )
# 这个函数是用于增强安全性的, 防止用户提交一些不合法的文件名
from werkzeug.utils import secure_filename

app = Flask(__name__)

@app.route('/static/<path:filepath>')
def send_static_file(filepath):
    send_from_directory('static', filepath)

@app.route('/')
def index():
    send_static_file("html/index.html")

@app.post('/upload/')
def upload():
    file = request.files.get("file")
    file.save(f"uploads/{secure_filename(file.filename)}")
    return "ok"

if __name__ == "__main__":
    app.run()
<!DOCTYPE html>
<!-- index.html -->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello</title>
</head>
<body>
    <h1>Hello Flask</h1>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file" id="file">
        <input type="submit" value="提交">
    </form>
</body>
</html>

处理 cookie

使用 request.cookies 来获取 cookie,该属性是一个包含请求报文中所有 cookie 的字典

@app.route('/home/')
def index():
    user_id = request.cookies.get('userID')
    token = request.cookies.get('token')
    print(f"id={user_id},token={token}")

上面的例子,对于请求报文

GET /home HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Cookies: userID=123abc;token=567bbd1c33e7
Connection: close


将会打印 id=123abc,token=567bbd1c33e7

控制响应报文

flask 借助 Response 对象来控制响应报文,使用 make_response 函数来构建一个 Response 对象

对于该对象,flask 官方是这样描述的

Class flask.Response(response=None, status=None, headers=None, mimetype=None, content_type=None, direct_passthrough=False)

The response object that is used by default in Flask. Works like the response object from Werkzeug but is set to have an HTML mimetype by default. Quite often you don’t have to create this object yourself because make_response() will take care of that for you.

If you want to replace the response object used you can subclass this and set response_class to your subclass.

比较重要的属性和方法有

response: 这个属性存放了相应体的内容

当我们在视图函数中直接 return 时,其实 flask 自动帮我们创建了 Response 对象并设置了 response 属性

from flask import Flask, make_response

app = Flask(__name__)

@app.route('/index_1/')
def index_1():
    return "I am index"

@app.route('/index_2/')
def index_2():
    # 手动创建 Response 对象
    resp = make_response("I am index")
    return resp

上面两个视图函数的效果是一样的

headers: 这个属性存放了相应头的内容

添加、设置、删除响应头可以直接通过 headers 属性进行操作:

resp = make_response('hello')
resp.headers['Content-Language'] = 'en'
del resp.headers['X-Custom-Header']

set_cookie: 这个方法可以设置用户浏览器中的 cookie 值

@app.route('/set_cookie')
def set_cookie():
    resp = make_response("Setting cookie")
    resp.set_cookie('cookie1', 'value1')
    resp.set_cookie('cookie2', 'value2')
    return resp

该函数的参数比较多,就不全部讲了,下面是官方文档的描述

set_cookie(key, value=”, max_age=None, expires=None, path=’/’, domain=None, secure=False, httponly=False, samesite=None)

Sets a cookie.

A warning is raised if the size of the cookie header exceeds max_cookie_size, but the header will still be set.

Parameters:

  • key (str) – the key (name) of the cookie to be set.
  • value (str) – the value of the cookie.
  • max_age (timedelta | int | None) – should be a number of seconds, or None (default) if the cookie should last only as long as the client’s browser session.
  • expires (str | datetime | int | float | None) – should be a datetime object or UNIX timestamp.
  • path (str | None) – limits the cookie to a given path, per default it will span the whole domain.
  • domain (str | None) – if you want to set a cross-domain cookie. For example, domain=”example.com” will set a cookie that is readable by the domain www.example.com, foo.example.com etc. Otherwise, a cookie will only be readable by the domain that set it.
  • secure (bool) – If True, the cookie will only be available via HTTPS.
  • httponly (bool) – Disallow JavaScript access to the cookie.
  • samesite (str | None) – Limit the scope of the cookie to only be attached to requests that are “same-site”.

Return type: None

使用 session

有时候我们需要跨路由使用一些信息,例如要记录下用户的登录状态,这个状态是所有路由共享的

flask 提供了 session(会话)技术来解决这个问题

我们通过以下方式来使用会话

from flask import Flask, session, redirect, url_for

app = Flask(__name__)
app.config["secret_key"] = "xxxxxxxx"

def redirect_to_route(route: str):
    url = request.url
    return redirect(url + route)

@app.route('/')
def index():
    session["username"] = "caterpie"
    redirect(url_for('user')) # 重定向到 user 视图函数

@app.route('/user/')
def user():
    if session.get("username"):
        return f"I am {session["username"]}"
    return "who are you?"

在使用 session 时,要指定 secret_key,因为 session 内的内容绝大部分时候是不允许用户更改的,所以 flask 使用 secret_key 对其进行校验,以防止用户伪造 session 导致安全问题。session 中的内容默认被编码成 json 格式,所以只允许传递一些简单的数据类型(字符串、数字、列表、字典和布尔值),但也够用了,更复杂的情况就要用到 序列化 技术,这里不展开

session 将会存储到 Cookie 中,flask 会自动读取并解析,无需使用 request.cookie 来处理

部署 flask 应用

开始之前,让我多嘴一句,这其实不算“生产环境”的部署,而是“开发环境”的部署。为了速成,所以我这样教。实际的 flask 项目部署要麻烦一些,可能会涉及到 uWSGI、Nginx 等中间件的部署。而且我教的用 sudo 来在 80 端口部署 flask 应用的方式,是不符合安全规范的,以后大家有更深刻的理解后,再换用规范的部署方式

现在我们可以将云服务器上的静态网站升级成 flask 应用了,在部署前,请将 app.run() 改为 app.run('0.0.0.0', 80)

将 python 代码和静态文件打包发送到服务器,然后在云服务器的终端中执行

cd /path/to/you_app                 # 进入flask应用所在的文件夹
conda activate myenv                # 激活虚拟环境
# 方式一
# 在后台运行 flask 应用
sudo env PATH=$PATH python app.py &
# 方式二
# 在后台运行 flask 应用, 并将输出写入 output.log
nohup sudo env PATH=$PATH python app.py > output.log 2>&1 &

还能使用 screen 命令来使 python 进程在后台运行,这是我推荐使用的方式

screen -dmS screen_name     # 创建一个名为 sreen_name 的会话
screen -r screen_name       # 进入该会话
# 在会话中执行命令
cd /path/to/you_app
conda activate myenv
sudo env PATH=$PATH python app.py
# 使用 ctrl-a + d 退出当前会话
# 退出后, 会话中的进程会保持运行, 即使当前用户登出

screen 是很强大的,善用它可以实现一些复杂的需求,具体用法可以参考 Linux screen命令教程

这里贴出我写过的一个 shell 脚本,当时的需求是:同时启动多个 python 进程,并且要实时查看到终端输出以方便调试

#!/bin/bash

echo "项目准备中..."
do_in_screen(){
        screen -x -S ${1} -p 0 -X stuff "${2}"
        screen -x -S ${1} -p 0 -X stuff $"\n"
}
launch(){
    screen -dmS ${1}
    do_in_screen ${1} "source activate"
    do_in_screen ${1} "conda activate iknowisee"
    do_in_screen ${1} "python ${2}"
    echo "已启动 ${1}.py"
}

# 设置文件路径
file1="/root/python/Dlib_face_recognition_from_camera-master/main.py"
file2="/root/python/Dlib_face_recognition_from_camera-master/newtest.py"
file3="/root/python/Dlib_face_recognition_from_camera-master/newtest1.py"
echo "关键路径设置成功"

# 运行Python文件
launch "main" "$file1"
launch "newtest" "$file2"
launch "newtest1" "$file3"

echo "完成!"
echo "创建了以下终端"
screen -ls

shell 脚本的编写不作要求,这里仅用作展示 screen 命令的强大之处

在运行 flask 应用后,我们应该能使用 http://云服务器ip/ 访问我们编写的服务了

第四章 数据库 sqlite 速成

SQLite 安装和使用

sqlite 是一个轻量级的嵌入式数据库,轻量到什么程度呢?python 的自带库中就有 sqlite,所以配置完成 python 环境,就意味着我们安装完成了 sqlite

我们可以在命令行输入 sqlite3.exe 启动 sqlite

使用下面的命令来打开指定的数据库

sqlite3.exe {dbfilename}
dbfilename: 数据库文件名称

touch dbfilename.db && sqlite3.exe dbfilename.db # 创建并打开一个数据库

sqlite 是关系型数据库,什么是 “关系型” 数据库,三言两语讲不清楚,可以参考:关系数据库概述

常用 SQL 语句

我们分为 增删改查 四个部分来讲

增:创建数据表、插入数据

# 下面这个句话用于创建数据表
create table table_name (
    column_name1 column_type other,
    column_name2 column_type other,
    ...
);
# 例如: 创建一个 user 表, 其具有 id/username/password 等列
create table user (
    /*该列是整型的, primary key表明该列为"主键", autoincrement表示自增*/
    id integer primary key autoincrement,
    /*该列数据是变长字符型的, 最大长度为20, not null 表示该列非空*/
    username varchar(20) not null,
    password varchar(20) not null
);
# 这句话用于向数据表内插入一行数据
insert into table_name(column1, column2 ...) values(value1, value2 ...);
# 例如: 向 user 表添加一个用户信息
insert into user(username, password) values("caterpie", "123456"); /*id 是自增主键, 不需要手动指定*/

可以看到数据库中每列具有不同的数据类型,如 id 列的 integer,username 列的 varchar(20)

由于 SQLite 是动态类型的,所以声明字段类型的时候不需要太严谨(实际上你可以完全不指定字段的数据类型),sqlite基本数据类型有

数据类型 说明
NULL 表示空值
INTEGER 整型数据,如 1,2,3
int 整型数据,integer 的同义类型
REAL 浮点数,存储至少 32 位精度浮点数
double 浮点数,存储至少 64 位精度浮点数
float 浮点数,real 的同义类型
TEXT 文本字符串,会使用 UTF-8、UTF-16LE 等编码储存
char 定长字符串,用 char(xxx) 来指定其长度
varchar 变长字符串,用 varchar(xxx) 来指定其最大长度
BLOB 存放二进制数据,不会进行编码

查:按条件查询表中数据

先学会怎么查数据再学删改,这样能够验证每个操作的影响

# 这句话用于查询指定数据表的所有内容
# 这里的 * 属于通配符, 匹配所有字段
select * from table_name;
# 例如: 查询 user 表中的所有内容
select * from user;

# 这句话用于查询指定数据表中的指定字段
select column_name1, column_name2 ... from table_name;
# 例如: 查询 user 表中 username 字段的所有数据
select username from user;

# where用于按照某种规则进行查询
select column_name1, column_name2 ... from table_name where condition;
# 例如: 查询 user 表中 username 为 'caterpie' 的数据
select * from user where username='caterpie';

SQL 的条件表达式由下面这些运算符组成,只介绍常用的

算数运算符 + - * / %

比较运算符

运算符 描述
==、=、<> 这三个都表示“相等”逻辑
< 小于
> 大于
>=、<= 大于等于、小于等于
!=、!>、!< 不等于、不大于、不小于

逻辑运算符

运算符 描述
and 逻辑与
or 逻辑或
not 逻辑非
like 用于把某个值与使用通配符运算符的相似值进行比较

还有一些特殊的查询选项

将查询结果按照某个规则排序 order by(默认是按照主键的值进行排序)

select name, score from students order by score;      // 按成绩升序排列
select name, score from students order by score desc; // 按成绩降序排列

分页查询 limit a offset b (从第 b 条数据开始查询 a 条数据)

# 查询第 10 到第 30 条数据
select * from students order by name limit 20 offest 10;
# 查询 user 表中有 vip 身份的前 10 条数据
select uid, username from user
where is_vip = 1
limit 10 offset 0;

删:删除数据表、删除表中数据

# 这句话用于删除指定数据表
drop table if exists table_name;
# 例如: 删除 user 表(如果该表格存在)
drop table if exists user;
# 这句话用于删除符合指定条件的数据行
delete from table_name where condition;
# 例如: 删除 user 表中 age 介于 10 到 20 的数据
delete from user where age >= 10 and age <= 20;

改:更改表中数据

# 这句话用于更新符合指定条件的数据行
update table_name set column_name1=value1, column_name2=value2 ... where condition;
# 例如: 将用户 caterpie 的 uid 改为 '123abc'
update user set uid='123abc' where username='caterpie';
# 例如: 将分数小于 60 的数据行的 result 字段设置成 'F'
update students set result='F' where score < 60;

python 连接 sqlite

import sqlite3

db = sqlite3.connect("database.db", check_same_thread=False)  # 连接 sqlite 数据库
curcor = db.cursor()                                          # 创建数据库光标

curcor.execute("select xxx from xxx where xxx=?", ("xxx",))   # 使用 sql 语句操作数据库
result = curcor.fetchall()                                    # 获取查询结果
db.close()                                                    # 关闭连接

(写不动了,自行搜集资料学习吧)

这下全栈组需要用到的语言基本上就学完了。写一个 web 项目的时候,需要同时使用到 html、css、javascript、python、sql

其中 python 可能替换成 php、java 等其他后端语言(甚至可能是 javascript),视项目使用的后端框架而定

第五章 MQTT 与 EMQX

MQTT 协议讲解

为什么使用 MQTT?因为 MQTT 的:开销低、报文长度短,适合性能较低、工作条件较差的物联网设备(其实你用 http 协议也是完全可以的,如果你的设备够好的话)

MQTT 分为 发布者 订阅者 消息代理(Broker) 三种身份,其中 发布者和订阅者属于客户端,消息代理属于服务端

我们的 EMQX 平台就属于消息代理,我们的设备都要通过它来进行消息的发布和订阅

发布-订阅模式

mqtt 协议采用发布-订阅模式来工作,这里服务端负责消息的分发

下图展示了 MQTT 的工作模式,温度传感器发布温度数据到 主题Temperature,我们的 消息代理Broker 就将其转发给 订阅了Temperature主题 的客户端

a6baf485733448bc9730f47bf1f41135.webp

主题命名规范

MQTT 的主题命名和 url 有相似之处,都使用 / 来进行层级的分隔,一个符合标准的主题就像:

myhome/room_1/5/tempereture
myhome/room_2/brightness

mqtt 主题将 #+ 作为通配符使用

+ 表示单层通配符,一个订阅了 myhome/+/brightness 的客户端将会接收到以下信息

myhome/room_1/brightness
myhome/room_2/brightness
...

# 表示多层通配符,例如 a/# 匹配 a/x、a/b/c/d,一个订阅了 myhome/room_1/# 的客户端将会接受到以下信息

myhome/room_1/tempereture
myhome/room_1/brightnes
myhome/room_1/1/tempereture
myhome/room_1/2/tempereture
myhome/room_1/box/1/brightness
...

注意:使用通配符的主题只能用于订阅,不能用于发布

消息质量

MQTT 提供了三种消息质量

  • QoS 0:消息最多传送一次。如果当前客户端不可用,它将丢失这条消息
  • QoS 1:消息至少传送一次
  • QoS 2:消息只传送一次

从 Qos 0 到 Qos 2,消息质量逐步上升,开销也在逐步上升

对于 Qos 0,消息可能会产生丢失的情况,适合传输一些高频且不那么重要的数据,比如传感器数据,周期性更新,即使遗漏几个周期的数据也可以接受

对于 Qos 1,消息丢失概率很低,但是可能会产生重复的情况,适合传输一些重要的数据。数据需要允许重复,或者你已经做好了去重处理

对于 Qos 2,开销最大,但是无需担心数据丢失与重复,适合设备性能较高时使用,一般用在金融、航天等领域

启动和配置 EMQX

在第零章中,我们将 EMQX 安装到了 /opt/emqx 路径下,所以我们应使用以下命令来启动 EMQX

cd /opt/emqx/bin && ./emqx start

要停止的话用

cd /opt/emqx/bin && ./emqx stop

启动完成后我们就应该能从 http://云服务器ip:18083/ 登录 EMQX 的 web 后台

截图

使用默认账号密码进行登录;账号: admin 密码: public

EMQX 平台的功能很丰富,可以参照官方手册 EMQX 文档 进行使用

使用 Webhook

Webhook 是 EMQX 提供的,将消息数据集成到外部 HTTP 服务的功能

在侧边栏找到 webhook 选项

截图

添加新的 webhook,这里的 127.0.0.1 是一个保留 ip,代表”本机”

截图

这里启动一个用于测试的 flask 服务

from flask import (
    Flask,
    request,
)
import json

app = Flask(__name__)

@app.route("/")
def index():
    return "hello, flask"

@app.post("/api/mqtt/webhook/")
def webhook():
    if "token" not in request.headers:
        return json.dumps({"error":"fobidden"})
    if request.headers.get("token") != "password":
        return json.dumps({"error":"fobidden"})
    print(request.json)
    return json.dumps({"message":"success"})


if __name__ == "__main__":
    app.run("0.0.0.0", 80)

此时向 EMQX 发布数据,flask 就会有如下输出

img

可以看到,request.json 中包含了 topicpayload 等关键信息,此时 EMQX 就和 flask 服务关联起来了

在 python 中使用 MQTT

python 通常使用 paho-mqtt 这个第三方库来进行 mqtt 的开发,安装方式:

pip install paho-mqtt

下面用面向过程的方式给出该第三方库的几种用法

创建 mqtt 客户端并连接到指定 Broker

from paho.mqtt import client as mqtt_client
from random import randint

def connect_to_broker(broker: str, port=1883, client_id: str):
    # 定义回调函数
    def on_connect(client, userdata, flags, return_code):
        if return_code == 0:
            print("连接成功")
        else:
            print("连接失败, 错误代码: %d\n", return_code)
    # 创建 mqtt_client 对象
    client = mqtt_client.Client(client_id)
    # 绑定回调函数
    client.on_connect = on_connect
    # 尝试连接
    client.connect(broker, port)
    # 将成功连接后的 Client 对象返回
    return client

client = connect_to_broker("borker.mqtt.io", f'python-{randint(0, 10**5)}')
client.loop_start() # 启动客户端
client.loop_stop()  # 关闭客户端

向指定主题发布消息

from paho.mqtt import client as mqtt_client
import json

def publish(client, topic: str, payload: str, qos=0) -> int:
    # 使用 mqtt_client 对象的 publish 方法发布消息
    result = client.publish(topic, payload, qos)
    # result 的结果是 0/1
    if result:
        print(f"发布成功:\n主题 {topic}\n消息 {payload}")
    else:
        print("发布失败")
    return result

publish(client, "myhome/test", json.dumps({'test':'test'}), qos=1)

由于我们使用了 Webhook,订阅主题的功能就不需要实现了。如果确实有订阅需求的话,可以像这样写

from paho.mqtt import client as mqtt_client

def subscribe(client: mqtt_client, topic: str) -> int:
    # 定义回调函数
    def on_message(client, userdata, msg):
        print(f"接受到消息:\n主题 {msg.topic}\n载荷 {msg.payload}")
    try:
        # 订阅主题
        client.subscribe(topic)
        # 绑定回调函数
        client.on_message = on_message
        print(f"已订阅: {topic}")
        return 1
    except:
        print("订阅失败")
        return 0

上述功能可以使用面向对象的方式自行封装一下,由于是速成课,我就不花这个时间去讲面向对象了,有时间的可以去了解一下

怎么串联

可能有人看完这几个章节还是很迷惑,不知道该怎么配合这些知识点。这里给出几个可行方案

方案一:不使用物联网协议,硬件直接通过 http 协议上云

数据上行:客户端发送 http 请求报文,利用请求报文携带上行数据

数据下行:客户端进行 http 轮询,利用响应报文携带下行数据

img

方案二:硬件直接通过 mqtt 协议上云,用户则通过 http 协议和后端服务交互

数据上行:客户端发布消息,mqtt 代理通过 webhook 通知 web 服务

数据下行:客户端订阅相关 mqtt 主题,web 服务向该主题发布消息

img

方案三:当硬件不支持 mqtt 时,可以添加物联网网关(一些IoT云服务厂商用的就是这个方案)

数据上行:物联网网关整合本地数据后,通过 mqtt 发送至云服务

数据下行:web 服务向对应主题发布消息,物联网网关接收后,转换为硬件支持的协议,再发布到硬件

img

这个方案中的物联网网关可以用树莓派自己实现,也可以是厂商封装好的网关产品,例如霍尼韦尔的 jace8000

参考资料:物联网网关,原来是这么回事