使用树莓派主机托管具有动态内容和数据库的网站

您可以使用免费软件在非常轻量级的计算机上支持 Web 应用程序。
159 位读者喜欢这篇文章。
Digital creative of a browser on the internet

树莓派的单板机为廉价、实际的计算设定了标准。凭借其 Model 4,树莓派可以托管 Web 应用程序,并配备生产级 Web 服务器、事务数据库系统以及通过脚本实现的动态内容。本文通过完整的代码示例解释了安装和配置的详细信息。欢迎来到在非常轻量级的计算机上托管的 Web 应用程序。

降雪量应用程序

想象一个足够大的下坡滑雪区,拥有微气候,这意味着整个区域的降雪量可能差异很大。该区域划分为多个区域,每个区域都有记录厘米降雪量的设备;记录的信息然后指导有关造雪、压雪和其他维护操作的决策。这些设备例如每 20 分钟与服务器通信一次,服务器更新数据库以支持报告。如今,此类应用程序的服务器端软件可以是免费的并且是生产级的。

此降雪量应用程序使用以下技术

  • 运行 Debian 的 树莓派 4
  • Nginx Web 服务器:免费版本托管超过 4 亿个网站。此 Web 服务器易于安装、配置和使用。
  • SQLite 关系数据库系统,它是基于文件的:数据库(可以容纳许多表)是本地系统上的一个文件。SQLite 很轻量级,但也符合 ACID 标准;它适用于低到中等容量。SQLite 可能是世界上使用最广泛的数据库系统,并且 SQLite 的源代码位于公共领域。当前版本是 3。更强大(但仍然免费)的选择是 PostgreSQL。
  • Python:Python 编程语言可以与 SQLite 等数据库和 Nginx 等 Web 服务器交互。Python(版本 3)随 Linux 和 macOS 系统一起提供。

Python 包括一个用于与 SQLite 通信的软件驱动程序。有多种选项可以将 Python 脚本与 Nginx 和其他 Web 服务器连接。一种选择是 uWSGI(Web 服务器网关接口),它更新了 20 世纪 90 年代古老的 CGI(通用网关接口)。

有几个因素支持 uWSGI

  • uWSGI 非常灵活。它可以用作轻量级并发 Web 服务器,也可以用作连接到 Nginx 等 Web 服务器的后端应用程序服务器。
  • 它的设置非常简单。
  • 降雪量应用程序涉及对 Web 服务器和数据库系统的低到中等容量的访问。一般来说,按照现代标准,CGI 技术速度不快,但 CGI 对于部门级 Web 应用程序(例如这个)来说性能足够好。

各种首字母缩略词描述了 uWSGI 选项。以下是三个主要选项的草图

  • WSGI 是 Python 规范,用于 Web 服务器(在一侧)与应用程序或应用程序框架(例如 Django)(在另一侧)之间的接口。此规范定义了一个 API,其实现是开放的。
  • uWSGI 通过提供应用程序服务器来实现 WSGI 接口,该服务器将应用程序连接到 Web 服务器。uWSGI 应用程序服务器的主要工作是将 HTTP 请求转换为 Web 应用程序可以使用的格式,然后在之后将应用程序的响应格式化为 HTTP 消息。
  • uwsgi 是 uWSGI 应用程序服务器实现的二进制协议,用于与功能齐全的 Web 服务器(例如 Nginx)通信;它还包括轻量级 Web 服务器等实用程序。Nginx Web 服务器“开箱即用”地“说”uwsgi。

为方便起见,我将使用“uwsgi”作为二进制协议、应用程序服务器和非常轻量级的 Web 服务器的简写。

设置数据库

在基于 Debian 的系统上,您可以像往常一样安装 SQLite(使用 % 表示命令行提示符)

% sudo apt-get install sqlite3

此数据库系统是 C 库和实用程序的集合,所有这些库和实用程序的大小约为 500KB。没有数据库服务器需要启动、停止或以其他方式维护。

安装 SQLite 后,在命令行提示符下创建一个数据库

% sqlite3 snowfall.db

如果成功,该命令将在当前工作目录中创建文件 snowfall.db。数据库名称是任意的(例如,不需要扩展名),并且该命令使用 >sqlite 作为提示符打开 SQLite 客户端实用程序

Enter ".help" for usage hints.
sqlite> 

使用以下命令在 snowfall 数据库中创建 snowfall 表。表名(如数据库名称)是任意的

sqlite> CREATE TABLE snowfall (id INTEGER PRIMARY KEY AUTOINCREMENT,
                               region TEXT NOT NULL,
                               device TEXT NOT NULL,
                               amount DECIMAL NOT NULL,
                               tstamp DECIMAL NOT NULL);

SQLite 命令不区分大小写,但传统上对 SQL 术语使用大写,对用户术语使用小写。检查表是否已创建

sqlite> .schema

该命令会回显 CREATE TABLE 语句。

数据库现在已准备就绪,可以投入使用,尽管单表 snowfall 是空的。您可以交互式地向表中添加行,但现在空表就可以了。

首次了解总体架构

回想一下,uwsgi 可以通过两种方式使用:既可以用作轻量级 Web 服务器,也可以用作连接到生产级 Web 服务器(例如 Nginx)的应用程序服务器。第二个用途是目标,但第一个用途适用于开发和测试程序员的请求处理代码。以下是以 Nginx 作为 Web 服务器的架构

       HTTP       uwsgi
client<---->Nginx<----->appServer<--->request-handling code<--->SQLite

客户端可以是浏览器、curl 等实用程序或精通 HTTP 的手工程序。客户端和 Nginx 之间的通信通过 HTTP 进行,但随后 uwsgi 接管作为 Nginx 和应用程序服务器之间的二进制传输协议,该应用程序服务器与请求处理代码(例如 requestHandler.py(如下所述))交互。此架构提供了清晰的职责分工。Nginx 单独管理客户端,只有请求处理代码与数据库交互。反过来,应用程序服务器将 Web 服务器与程序员编写的代码分开,后者具有高级 API 来读取和写入通过 uwsgi 传递的 HTTP 消息。

我将在接下来的部分中检查这些架构组件,并介绍安装、配置和使用 uwsgi 和 Nginx 的步骤。

降雪量应用程序代码

以下是降雪量应用程序的源代码文件 requestHandler.py。(它也可以在我的 网站上找到。)此代码中的不同函数有助于阐明连接 SQLite、Nginx 和 uwsgi 的软件架构。

请求处理程序

import sqlite3
import cgi

PATH_2_DB = '/home/marty/wsgi/snowfall.db'

## Dispatches HTTP requests to the appropriate handler.
def application(env, start_line):
    if env['REQUEST_METHOD'] == 'POST':   ## add new DB record
        return handle_post(env, start_line)
    elif env['REQUEST_METHOD'] == 'GET':  ## create HTML-fragment report 
        return handle_get(start_line)
    else:                                 ## no other option for now
        start_line('405 METHOD NOT ALLOWED', [('Content-Type', 'text/plain')])
        response_body = 'Only POST and GET verbs supported.'
        return [response_body.encode()]                            

def handle_post(env, start_line):    
    form = get_field_storage(env)  ## body of an HTTP POST request
    
    ## Extract fields from POST form.
    region = form.getvalue('region')
    device = form.getvalue('device')
    amount = form.getvalue('amount')
    tstamp = form.getvalue('tstamp')

    ## Missing info?
    if (region is not None and
        device is not None and
        amount is not None and
        tstamp is not None):
        add_record(region, device, amount, tstamp)
        response_body = "POST request handled.\n"
        start_line('201 OK', [('Content-Type', 'text/plain')])
    else:
        response_body = "Missing info in POST request.\n"
        start_line('400 Bad Request', [('Content-Type', 'text/plain')])
 
    return [response_body.encode()]

def handle_get(start_line):
    conn = sqlite3.connect(PATH_2_DB)        ## connect to DB
    cursor = conn.cursor()                   ## get a cursor
    cursor.execute("select * from snowfall")

    response_body = "<h3>Snowfall report</h3><ul>"
    rows = cursor.fetchall()
    for row in rows:
        response_body += "<li>" + str(row[0]) + '|'  ## primary key
        response_body += row[1] + '|'                ## region
        response_body += row[2] + '|'                ## device
        response_body += str(row[3]) + '|'           ## amount
        response_body += str(row[4]) + "</li>"       ## timestamp
    response_body += "</ul>"

    conn.commit()  ## commit
    conn.close()   ## cleanup
    
    start_line('200 OK', [('Content-Type', 'text/html')])
    return [response_body.encode()]

## Add a record from a device to the DB.
def add_record(reg, dev, amt, tstamp):
    conn = sqlite3.connect(PATH_2_DB)      ## connect to DB
    cursor = conn.cursor()                 ## get a cursor

    sql = "INSERT INTO snowfall(region,device,amount,tstamp) values (?,?,?,?)"
    cursor.execute(sql, (reg, dev, amt, tstamp)) ## execute INSERT 

    conn.commit()  ## commit
    conn.close()   ## cleanup

def get_field_storage(env):
    input = env['wsgi.input']
    form = env.get('wsgi.post_form')
    if (form is not None and form[0] is input):
        return form[2]

    fs = cgi.FieldStorage(fp = input,
                          environ = env,
                          keep_blank_values = 1)
    return fs

源文件开头的常量定义了数据库文件的路径

PATH_2_DB = '/home/marty/wsgi/snowfall.db'

请务必更新您的树莓派的路径。

如前所述,uwsgi 包括一个可以托管此请求处理应用程序的轻量级 Web 服务器。首先,使用以下两个命令安装 uwsgi(## 引入我的注释)

% sudo apt-get install build-essential python-dev ## C header files, etc.
% pip install uwsgi                               ## pip = Python package manager

接下来,使用 uwsgi 作为 Web 服务器启动一个最基本的降雪量应用程序

% uwsgi --http 127.0.0.1:9999 --wsgi-file requestHandler.py  

标志 --http 在 Web 服务器模式下运行 uwsgi,其中 9999 是 Web 服务器在 localhost (127.0.0.1) 上的侦听端口。默认情况下,uwsgi 将 HTTP 请求分派给程序员定义的名为 application 的函数。回顾一下,以下是 requestHandler.py 代码顶部的完整函数

def application(env, start_line):
    if env['REQUEST_METHOD'] == 'POST':   ## add new DB record
        return handle_post(env, start_line)
    elif env['REQUEST_METHOD'] == 'GET':  ## create HTML-fragment report 
        return handle_get(start_line)
    else:                                 ## no other option for now
        start_line('405 METHOD NOT ALLOWED', [('Content-Type', 'text/plain')])
        response_body = 'Only POST and GET verbs supported.'
        return [response_body.encode()]

降雪量应用程序仅接受两种请求类型

  • POST 请求,如果符合标准,则在 snowfall 表中创建一个新条目。该请求应包括滑雪区区域、区域中的设备、厘米降雪量和 Unix 样式的 时间戳。POST 请求被分派到 handle_post 函数(我稍后将澄清)。
  • GET 请求返回一个 HTML 片段(无序列表),其中包含 snowfall 表中当前记录。

HTTP 动词不是 POST 和 GET 的请求将生成错误消息。

您可以使用 curl 等实用程序生成 HTTP 请求以进行测试。以下是三个示例 POST 请求,用于开始填充数据库

% curl -X POST -d "region=R1&device=D9&amount=1.42&tstamp=1604722088.0158753" localhost:9999/
% curl -X POST -d "region=R7&device=D4&amount=2.11&tstamp=1604722296.8862638" localhost:9999/
% curl -X POST -d "region=R5&device=D1&amount=1.12&tstamp=1604942236.1013834" localhost:9999/

这些命令向 snowfall 表添加了三条记录。来自 curl 或浏览器的后续 GET 请求会显示一个 HTML 片段,其中列出了 snowfall 表中的行。以下是等效的非 HTML 文本

Snowfall report

    1|R1|D9|1.42|1604722088.0158753
    2|R7|D4|2.11|1604722296.8862638
    3|R5|D1|1.12|1604942236.1013834

专业的报告会将数字时间戳转换为人类可读的时间戳。但目前的重点是降雪量应用程序中的架构组件,而不是用户界面。

uwsgi 实用程序接受各种标志,这些标志可以通过配置文件或启动命令给出。例如,以下是作为 Web 服务器的 uwsgi 的更丰富的启动

% uwsgi --master --processes 2 --http 127.0.0.1:9999 --wsgi-file requestHandler.py

此版本创建一个主(监控)进程和两个工作进程,它们可以并发处理 HTTP 请求。

在降雪量应用程序中,函数 handle_posthandle_get 分别处理 POST 和 GET 请求。以下是完整的 handle_post 函数

def handle_post(env, start_line):    
    form = get_field_storage(env)  ## body of an HTTP POST request
    
    ## Extract fields from POST form.
    region = form.getvalue('region')
    device = form.getvalue('device')
    amount = form.getvalue('amount')
    tstamp = form.getvalue('tstamp')

    ## Missing info?
    if (region is not None and
        device is not None and
        amount is not None and
        tstamp is not None):
        add_record(region, device, amount, tstamp)
        response_body = "POST request handled.\n"
        start_line('201 OK', [('Content-Type', 'text/plain')])
    else:
        response_body = "Missing info in POST request.\n"
        start_line('400 Bad Request', [('Content-Type', 'text/plain')])
 
    return [response_body.encode()]

handle_post 函数的两个参数(envstart_line)分别表示系统环境和通信通道。start_line 通道发送 HTTP 启动行(在本例中为 400 Bad Request201 OK)和 HTTP 响应的任何 HTTP 标头(在本例中仅为 Content-Type: text/plain)。

handle_post 函数尝试从 HTTP POST 请求中提取相关数据,如果成功,则调用函数 add_record 以向 snowfall 表添加另一行

def add_record(reg, dev, amt, tstamp):
    conn = sqlite3.connect(PATH_2_DB)      ## connect to DB
    cursor = conn.cursor()                 ## get a cursor

    sql = "INSERT INTO snowfall(region,device,amount,tstamp) VALUES (?,?,?,?)"
    cursor.execute(sql, (reg, dev, amt, tstamp)) ## execute INSERT 

    conn.commit()  ## commit
    conn.close()   ## cleanup

SQLite 自动将单个 SQL 语句(例如上面的 INSERT)包装在事务中,这解释了代码中对 conn.commit() 的调用。SQLite 还支持多语句事务。调用 add_record 后,handle_post 函数通过向请求者发送 HTTP 响应确认消息来完成其工作。

handle_get 函数也访问数据库,但仅读取 snowfall 表中的记录

def handle_get(start_line):
    conn = sqlite3.connect(PATH_2_DB)        ## connect to DB
    cursor = conn.cursor()                   ## get a cursor
    cursor.execute("SELECT * FROM snowfall")

    response_body = "<h3>Snowfall report</h3><ul>"
    rows = cursor.fetchall()
    for row in rows:
        response_body += "<li>" + str(row[0]) + '|'  ## primary key
        response_body += row[1] + '|'                ## region
        response_body += row[2] + '|'                ## device
        response_body += str(row[3]) + '|'           ## amount
        response_body += str(row[4]) + "</li>"       ## timestamp
    response_body += "</ul>"

    conn.commit()  ## commit
    conn.close()   ## cleanup
    
    start_line('200 OK', [('Content-Type', 'text/html')])
    return [response_body.encode()]

用户友好的降雪量应用程序将支持其他(和更高级的)报告,但即使是这个版本的 handle_get 也强调了 Python 和 SQLite 之间的清晰接口。顺便说一句,uwsgi 期望响应正文是字节列表。在 return 语句中,对方括号内的 response_body.encode() 的调用从 response_body 字符串生成字节列表。

升级到 Nginx

可以使用一个命令在基于 Debian 的系统上安装 Nginx Web 服务器

% sudo apt-get install nginx

作为 Web 服务器,Nginx 提供预期的服务,例如线级安全性、HTTPS、用户身份验证、负载平衡、媒体流、响应压缩、文件上传等。Nginx 引擎具有高性能和稳定性,并且此服务器可以通过各种编程语言支持动态内容。使用 uwsgi 作为非常轻量级的 Web 服务器是一个有吸引力的选择,但切换到 Nginx 是升级到具有大容量能力的工业强度 Web 托管。Nginx 和 uwsgi 都是用 C 实现的。

在 Nginx 的作用下,uwsgi 承担了通信协议的受限角色和应用程序服务器;它不再充当 HTTP Web 服务器。以下是修改后的架构

          HTTP       uwsgi                   
requester<---->Nginx<----->app server<--->requestHandler.py

如前所述,Nginx 包括 uwsgi 支持,现在充当反向代理服务器,将指定的 HTTP 请求转发到 uwsgi 应用程序服务器,后者又与 Python 脚本 requestHandler.py 交互。来自 Python 脚本的响应以相反方向移动,以便 Nginx 将 HTTP 响应发送回请求客户端。

两个更改使这种新架构成为现实。第一个是将 uwsgi 作为应用程序服务器启动

% uwsgi --socket 127.0.0.1:8001 --wsgi-file requestHandler.py

套接字 8001 是 Nginx 用于 uwsgi 通信的默认值。为了提高稳健性,您可以使用 Python 脚本的完整路径,以便不必在包含 Python 脚本的目录中执行上述命令。在生产环境中,uwsgi 将自动启动和停止;但是,目前重点仍然是架构组件如何组合在一起。

第二个更改涉及 Nginx 配置,这在基于 Debian 的系统上可能很棘手。Nginx 的主配置文件是 /etc/nginx/nginx.conf,但此文件可能具有针对其他文件的 include 指令,特别是三个 /etc/nginx 子目录之一中的文件:nginx.dsites-availablesites-enabled。可以消除 include 指令以简化问题;在这种情况下,配置仅发生在 nginx.conf 中。我推荐简单的方法。

无论配置如何分布,让 Nginx 与 uwsgi 应用程序服务器对话的关键部分都以 http 开头,并且有一个或多个 server 子节,这些子节又具有 location 子节。以下是 Nginx 文档中的示例

...
http {
    # Configuration specific to HTTP and affecting all virtual servers  
    ...
    server { # simple reverse-proxy
       listen       80;
       server_name  domain2.com www.domain2.com;
       access_log   logs/domain2.access.log  main;

       # serve static files
       location ~ ^/(images|javascript|js|css|flash|media|static)/  {
         root    /var/www/virtual/big.server.com/htdocs;
         expires 30d;
       }

       # pass requests for dynamic content to rails/turbogears/zope, et al
       location / {
         proxy_pass      http://127.0.0.1:8080;
       }
     }
     ...
}

location 子节是感兴趣的子节。对于降雪量应用程序,以下是添加的 location 条目及其两条配置行

...
server {
   listen 80 default_server;
   listen [::]:80 default_server;

   root /var/www/html;
   index index.html index.htm index.nginx-debian.html;

   server_name _;

   ### key addition for uwsgi communication
   location /snowfall {
      include uwsgi_params;       ## comes with Nginx
      uwsgi_pass 127.0.0.1:8001;  ## 8001 is the default for uwsgi
   }
   ...
}
...

为了现在保持简单,将 /snowfall 作为配置中唯一的 location。在此配置到位后,Nginx 侦听端口 80 并将以 /snowfall 路径结尾的 HTTP 请求分派到 uwsgi 应用程序服务器

% curl -X POST -d "..." localhost/snowfall ## new POST 
% curl -X GET localhost/snowfall           ## new GET

可以从请求中删除端口号 80,因为 80 是 HTTP 请求的默认服务器端口。

如果配置的位置只是 / 而不是 /snowfall,则任何以 / 开头的 HTTP 请求都将分派到 uwsgi 应用程序服务器。因此,/snowfall 路径为其他位置留出了空间,因此也为响应 HTTP 请求的进一步操作留出了空间。

使用添加的 location 子节更改 Nginx 配置后,您可以启动 Web 服务器

% sudo systemctl start nginx

还有其他类似于 stoprestart Nginx 的命令。在生产环境中,您可以自动化这些操作,以便 Nginx 在系统启动时启动,并在系统关闭时停止。

在 uwsgi 和 Nginx 都运行的情况下,您可以使用浏览器测试架构组件是否按预期协同工作。例如,如果您在浏览器的输入窗口中输入 URL localhost/,则应出现 Nginx 欢迎页面,其(HTML)内容类似于以下内容

Welcome to nginx!
...
Thank you for using nginx.

相比之下,URL localhost/snowfall 应显示 snowfall 表中当前的行

Snowfall report

    1|R1|D9|1.42|1604722088.0158753
    2|R7|D4|2.11|1604722296.8862638
    3|R5|D1|1.12|1604942236.1013834

总结

降雪量应用程序展示了免费软件组件——高性能 Web 服务器、符合 ACID 标准的数据库系统以及用于动态内容的脚本——如何在树莓派 4 平台上支持真实的 Web 应用程序。这款轻量级机器超出了其重量级别,而 Debian 简化了提升过程。

Web 应用程序中的软件组件协同工作良好,并且只需要很少的配置。对于针对关系数据库的更高容量的访问,请记住,SQLite 的免费且功能丰富的替代方案是 PostgreSQL。如果您渴望在树莓派 4 上玩耍——特别是探索此平台上的服务器端 Web 编程——那么值得考虑 Nginx、SQLite 或 PostgreSQL、uwsgi 和 Python。

接下来阅读什么
标签
User profile image.
我是计算机科学领域的学者(德保罗大学计算与数字媒体学院),在软件开发方面拥有丰富的经验,主要是在生产计划和调度(钢铁行业)以及产品配置(卡车和公共汽车制造)方面。有关书籍和其他出版物的详细信息,请访问

1 条评论

我一直在我们的家庭/办公室中使用 MariaDB、Apache 和 PHP(我用它编写了很多商业代码)在 Pi Zero W 上托管内容,它可以轻松处理十几个左右的客户端来处理简单的内容。虽然没有尝试“正确地”进行压力测试。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.