esp8266时钟+天气+提醒(五)云服务篇二
本篇主要讨论后端服务FastAPI如何在Linux上部署,同时使用ESP8266访问它。
我的服务器地址为47.102.199.118,FastAPI服务目前是开放状态,不需要身份校验,预计开放一周,欢迎萌新朋友们调试(但真的不要再玩SSH了)。
七、后端服务
1. pip
pip是Python的包管理工具,提供了一种简便的方式来安装、升级和删除Python包。这些包通常从Python包索引(PyPI,Python Package Index)获取,PyPI是一个存储Python程序员广泛使用的软件库和对应版本信息的数据库。
source venv/bin/activate
pip -V
输入以上命令可以激活虚拟环境并且查看pip的版本:
可以看到是虚拟环境中的pip,版本为23.0.1,这说明我们的操作都是正确的。
下面提供几个常用的指令:
pip install package_name
pip install --upgrade package_name
pip uninstall package_name
pip list
pip show package_name
它们分别表示安装、升级、卸载、列出已安装的包、展示包的信息。
2. 库安装
我们需要通过pip安装依赖,方法如下:
pip install fastapi uvicorn gunicorn
我们安装了三个库,分别是fastapi、uvicorn、gunicorn,下面简要介绍一下它们:
2.1 FastAPI
FastAPI是一个现代、快速(高性能)的Web框架,用于构建APIs与Web应用程序。它基于Python 3.6+类型提示,旨在快速开发,同时提供自动化的数据模型验证、序列化和文档(基于Swagger和ReDoc)。FastAPI支持异步编程,使其能够处理大量并发请求,非常适合高性能应用场景。
2.2 Uvicorn
Uvicorn是一个轻量级、超快的ASGI(Asynchronous Server Gateway Interface)服务器,用于Python。ASGI是WSGI(Web Server Gateway Interface)的异步版本,旨在提供一种标准方式来构建异步Web应用。Uvicorn基于uvloop和httptools,旨在提供高效的异步能力。作为ASGI服务器,Uvicorn可以运行任何基于ASGI的应用,如Starlette或FastAPI,提供了快速异步处理能力。
2.3 Gunicorn
Gunicorn(“Green Unicorn”)是一个Python WSGI HTTP服务器,用于UNIX系统。它是一个预分叉的工作模式的服务器,用于部署Python Web应用。Gunicorn被设计为易于使用和部署,提供了简单的命令行界面来启动Web应用。虽然Gunicorn本身不支持异步处理,但它可以与Uvicorn一起作为工作程序(worker)使用,从而结合Uvicorn的异步能力和Gunicorn的管理功能。
安装好后,使用pip list应当如下图所示:
3. 编写代码
3.1 Python代码
代码如下所示:
from fastapi import FastAPI
import uvicorn
from schemas.schemas import Data
app = FastAPI()
data_list = [
{"id": 1, "time": "9:00", "content": "洗脸刷牙1"},
{"id": 2, "time": "10:00", "content": "洗脸刷牙2"},
{"id": 3, "time": "19:00", "content": "洗脸刷牙3"},
{"id": 4, "time": "20:00", "content": "洗脸刷牙4"},
]
@app.get("/")
async def root():
return {"data": data_list}
@app.delete("/")
async def delete_data(id: int):
global data_list
data_list = [item for item in data_list if item['id'] != id]
return {"data": data_list}
@app.patch("/")
async def patch_data(data: Data):
global data_list
data_key = data.id
for i, record in enumerate(data_list):
if record["id"] == data_key:
# 为更新的记录创建一个新的字典
updated_record = data.model_dump()
updated_record["time"] = updated_record["time"].strftime("%H:%M")
# 在data_list中更新记录
data_list[i] = updated_record
break
return data_list
@app.post("/")
async def post_data(data: Data):
global data_list
data_list.append(data.model_dump())
return data_list
if __name__ == "__main__":
uvicorn.run("main:app", port=8000, host="0.0.0.0")
这是一个python程序,我们将其保存在main.py中(它与venv目录是同级的)。
在这里我们仿造一个数据库,用data_list来反映数据库中查询的结果(主要是服务器太拉胯,所以这样对付对付)。
此外我们又新建了一个目录schemas,其下有一个文件schemas.py。
mkdir schemas
cd schemas/
vi schemas.py
vi对萌新来说比较不友好,但这里不详细讲述了。但如果使用Xshell则会比较简单,直接右键->粘贴->粘贴到终端中即可,然后按ESC,输入:wq即可保存并退出(这是vi的要求)。
文件内容如下所示:
import datetime
from pydantic import BaseModel
class Data(BaseModel):
id: int
content: str
time: datetime.time
我们使用python开发,具体原理不再讲述了(可参看《Python语言与程序设计》),有兴趣的读者们可以自己做一个,或者直接魔改我的。
3.2 Gunicorn配置
刚才介绍了gunicorn,令人摸不着头脑。简单来说,我们要使用它为我们的fastapi应用保驾护航。
在当前目录下我们新建一个文件gunicorn.py,在里面输入以下内容:
loglevel = 'info'
accesslog = 'access.log'
errorlog = 'error.log'
workers = 4
worker_class = 'uvicorn.workers.UvicornWorker'
之后,我们使用以下命令就可以启动服务了:
gunicorn -c gunicorn.py main:app
接下来讲解一下这个命令:
该命令是在使用Gunicorn作为HTTP服务器来启动一个Python web应用时所使用的命令行语句,具体来说:
- gunicorn:这是命令的主体,指调用Gunicorn服务器,它可以和Uvicorn结合使用
- -c gunicorn.py:这个选项告诉Gunicorn使用一个预先定义的配置文件gunicorn.py。在这个配置文件中,你可以指定工作进程的数量、日志配置、绑定的IP地址和端口号等配置信息(比如错误日志就是error.log)。这是一个自定义的Python脚本,里面定义了Gunicorn服务器运行时的配置参数。
- main:app:这指定了Gunicorn应当寻找并运行的应用实例,我们的fastapi服务是卸载main.py里面的,所以这里是main:app。
启动服务后,我们会看到:
好像什么都没有,其实这是因为程序正在运行,如果它被访问则会打印出访问日志,但现在没人去访问,所以下面一片空白。
3.3 后台启动与结束进程
我们先输入Ctrl+C结束当前程序,看看这个目录里现在有什么:
可以看到多出来了两个log文件,它们是gunicorn.py中设置的日志,分别是访问日志和错误日志。
此外还有一个文件夹__pycache__,这是缓存文件夹,使用python都会有的。
如果像刚才一样启动服务,我们就会发现无法继续输入命令,所以我们需要让服务在后台运行,不占用我们的终端,方法如下:
gunicorn -c gunicorn.py main:app &
我们可以看到,gunicorn进程在后台启动了,8000端口的就是:
那么如何结束进程呢?我们之前是直接按下Ctrl+C来结束的,而现在我们的服务在后台运行,Ctrl+C不起作用,这该怎么办呢?
我通常采用这种方法来关闭进程:
pkill gunicorn
pkill命令用于关闭进程,上面这行则是说关闭所有gunicorn进程。而我们的fastapi服务是通过gunicorn运行的,这样就可以停止我们的服务。
4. 反向代理
如果在安全组中放行了8000端口,那么就可以通过浏览器直接访问了,不过一般还是使用HTTP默认的80端口比较好,这就需要反向代理。
反向代理的原理不在这里讲述了,有兴趣的可以自行了解。
观察上图,可知在/usr/local/nginx下有一个叫做conf的文件夹,里面是nginx的各种配置。
nginx的主配置文件是nginx.conf,我们可以编辑它。
我们在conf文件夹下又新建了一个文件夹conf,这个是我们的子配置文件夹,接着我们在其中新建配置文件:
cd /usr/local/nginx/conf
mkdir conf
cd conf/
vi main.conf
新建了一个main.conf的配置文件,我们用它来管理80端口。
在这个文件里我们写入以下内容:
server {
listen 80;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
它的意思是监听80端口,同时把对80的访问转发给8000(或者说是代理)。
接下来修改主配置文件nginx.conf,让它引入我们的main.conf。
操作如下:
cd ../
vi nginx.conf
可以看到主配置文件里已经有内容了,我们需要修改其中的一部分:
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
include ./conf/*.conf;
#charset koi8-r;
}
把http部分修改为上面的内容。
最后的include表示导入conf文件夹下一切后缀名为conf的配置文件。
完成后,我们检查一下nginx配置是否正确:
cd /usr/local/nginx/sbin
./nginx -t
nginx -t表示检查nginx配置是否正确,如果正确,结果应当如下图所示:
接下来我们重启nginx即可,重启方法为:
./nginx -s reopen
重启后稍微等一会,就可以在浏览器里观察到效果了:
5. 测试效果
以上步骤都成功之后,我们就使用esp8266试图访问,只需要模仿天气的代码段即可(并非最终版,但可用):
const char* reminder_host = "47.102.199.118";
void reminder() {
String reqRes = "/";
String httpRequest = String("GET ") + reqRes + " HTTP/1.1\r\n" + "Host: " + reminder_host + "\r\n" + "Connection: close\r\n\r\n";
Serial.println("");
Serial.print("Connecting to ");
Serial.print(host);
WiFiClient client;
// 尝试连接服务器
if (client.connect(reminder_host, 80)) {
Serial.println(" Success!");
// 向服务器发送http请求信息
client.print(httpRequest);
Serial.println("Sending request: ");
Serial.println(httpRequest);
// 获取并显示服务器响应状态行
String status_response = client.readStringUntil('\n');
Serial.print("status_response: ");
Serial.println(status_response);
// 使用find跳过HTTP响应头
if (client.find("\r\n\r\n")) {
Serial.println("Found Header End. Start Parsing.");
}
const size_t capacity = JSON_ARRAY_SIZE(4) + JSON_OBJECT_SIZE(1) + 4*JSON_OBJECT_SIZE(3) + 120;
DynamicJsonDocument doc(capacity);
deserializeJson(doc, client);
for (JsonObject data_item : doc["data"].as<JsonArray>()) {
int data_item_id = data_item["id"]; // 1, 2, 3, 4
const char* data_item_time = data_item["time"]; // "9:00", "10:00", "19:00", "20:00"
const char* data_item_content = data_item["content"]; // "洗脸刷牙", "洗脸刷牙", "洗脸刷牙2", "洗脸刷牙2"
Serial.print(data_item_content);
}
}
}
这里我们定义了一个函数reminder,它的作用主要是在串口监视器中输出提醒的内容,至于容量的配置,是通过第四章第三节中所述的方法,使用Assistant自动计算,考虑到裕量的字符串容量设为120。
此外,注意reminder_host是云服务器的公网IP,即第六章中的。
我们再修改一下loop函数:
void loop() {
if (mode == 0) {
clock_display(prevDisplay);
} else if (mode == 1) {
mode = 0;
reminder();
}
}
只是把获取天气的替换为获取提醒,在添加第二个按钮的时候我们会设置的。
运行程序后,按下按钮,触发中断,执行reminder函数,观察串口监视器:
如果如上图所示,说明我们的后端服务没有问题,esp8266也能很好地与它通信,说明成功了。
6. 其他的想法
观察原作者的代码(参见本专栏第二篇),可以发现获取的天气都是杭州的,是程序中固定好的,这就带来了比较大的局限性,比如我们批量生产、售往全国各地的时候,必然不能只查看杭州天气,为了使我们的项目具备普适性,解决方案主要有:
- 使用定位模块,获取当前地理位置。
- 通过IP地址解析出实际地理位置。
方案一的优势在于精度相较于原方案更高,原方案是城市级天气,使用定位模块可以获得更精确的位置,例如区县甚至乡镇街道,这样就能获得更准确的天气,虽然使用这种网格级天气可能要额外付费。。。而且定位模块本身也需要购买。。
方案二依赖服务器,因为对IP地址的解析是需要借助专业的解析工具或者解析网站的。我们可以在自己的后端服务中实现这个功能,获取城市信息后直接对心知天气发请求,将获得的数据再传回ESP8266。
未完待续
接下来我们要将获得的提醒数据按一定格式显示在屏幕上,此外提醒应当是可以修改的。
局域网通信的功能也将在后面实现。