1. 前言
最近 Github 上一个项目 “狗屁不通文章生成器”,把我给看乐了,点此前往[1]。
趁着阿里云双十一的新用户 1 折优惠活动,买了一台云主机,包年 86 软妹币,配置也是最基础的,毕竟也不是部署什么大项目,主要想试试公众号开发、云主机试试手,大致流程记录一下。在这先感谢 @deBroglie 的文章 《云主机配置微信公众号后台全记录 --【 阿里云ECS CentOS7 + Python3 + Flask + Nginx + Gunicorn + WeRoBot + Systemd 】》,本文流程大部分参考此篇文章,并以此基础上进行一些拓展以及异常的补充,正如文章中作者提到帮助你少了弯路,我亦认同。
2. 配置
我的云主机ECS配置如下:
操作系统 | CPU | 内存 | 带宽 |
---|---|---|---|
CentOS 7.6 64位 | 1 核 | 2 GiB | 1Mbps |
2.1 阿里云安全组规则(端口)配置
由于我们需要把微信公众号后台部署到阿里云上,为了保证微信能发送请求到我们的阿里云上,我们需要配置下阿里云的安全组规则,换句话说就是开个后门,太君,这边请~
我们找到云服务实例的安全组规则,选择添加安全组规则,端口填 80
(因为微信只支持 80
和 443
端口),授权对象 0.0.0.0/0
即可,允许任何 IP 进行访问(保险起见可以指定 IP,不过微信发送请求的 IP 好像不是固定的,这里就不深入了,觉得比较敏感的可以尝试解决)
好了,端口已经设置好,太君里面请(由于没有配置安全组规则,微信后台请求时老是提示请求 URL 超时,整整花掉 2 天空闲时间来解决这个问题,网上的教程几乎都没有提到阿里云设置安全组规则这一块)
2.2 linux 环境配置
2.2.1 安装依赖包
首先查看是否已安装 gcc,输入 gcc --version
查看 gcc 版本,如下图则已安装。若没安装,运行 yum -y install gcc
。
接下来安装 python3 所需的其他依赖包(缺少可能导致 python3 安装失败)
yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel
2.2.2 安装 python3
阿里云一般自带 python2,就下来我们来安装 python3 [2]。
1) 安装 python 3.7.4 (具体版本可修改命令指定)
# 下载 python 3.7.4 的安装包
wget https://www.python.org/ftp/python/3.7.4/Python-3.7.4.tgz
# 解压 Python-3.7.4.tgz
tar -zxvf Python-3.7.4.tgz
# 创建一个新文件夹用于存放安装文件
mkdir /usr/local/python3
# 执行配置文件,编译安装
cd Python-3.7.4
./configure --prefix=/usr/local/python3
make && make install
# 建立软连接(相当于 Windows 系统的环境变量)
ln -s /usr/local/python3/bin/python3.7 /usr/bin/python3
ln -s /usr/local/python3/bin/pip3.7 /usr/bin/pip3
运行命令 python3
,若显示版本号即为安装成功,如图。输入 exit()
或 Ctrl+D
退出 python。
2) 添加新用户
adduser wechat # 创建新用户
passwd wechat # 给新用户设置密码
usermod -aG wheel wechat # 给用户获取root权限
su wechat # 切换至新用户
Note:
-
wechat
是我创建的用户名。 - 此时
/home
路径下会生成以用户名命名的文件夹
3) 微信公众平台请求测试
python 已经安装好了,接下来测试一下微信公众平台是否能成功向服务器发送请求。登录微信公众平台,找到服务器配置。URL 可填域名或者 IP 地址,这里的 IP 地址必须是公网 IP ,由于我们使用 80
端口,头部用 http://
即可,这里我们用阿里云的公网 IP ,公网 IP 可在阿里云控制台示例找到。Token 相当于密码,自填。EncodingAESKey 随机生成即可。消息加密选择安全模式。填好后先不用提交。
编写测试脚本 test.py
。
cd /home/wechat # 进入用户目录
pip3 install werobot # 安装 werobot
pip3 install cryptography # 安装 cryptography
vim test.py # 创建名为 test 的 py 文件
利用 werobot
框架编写一个简单的机器人 demo [3]。
# -*- coding: utf-8 -*-
# filename: test.py
import werobot
# 输入微信公众平台请求凭证
robot = werobot.WeRoBot(token='$$$$$$') # 写入服务器配置填写的 Token
robot.config["APP_ID"] = "$$$$$$" # 写入开发者ID
robot.config["ENCODING_AES_KEY"] = "$$$$$$" # 写入服务器配置填写的 EncodingAESKey
# 建立一个消息处理装饰器,当 handler 无论收到何种信息时都运行 hello 函数
@robot.handler
def hello(message):
msg = message.content
reply = "Hello! You said {}".format(msg)
return reply
# 让服务器监听在 0.0.0.0:80
robot.config['HOST'] = '0.0.0.0'
robot.config['PORT'] = '80'
robot.run()
python3 test.py
命令运行 demo 脚本。
回到微信公众平台,将服务器配置填好的信息提交,没有返回异常则正常。点击启用,启用该配置。此时在公众号里发送I love u
,会自动回复 Hello! You said I love u
。测试完成。
Note:
-
cryptography
包需要手动安装,否则会报错ImportError
导入失败,该包用于安全模型下,如加密模式使用非安全模式,可不安装。
其实到这一步,按我之前的构想,只要不断地添加功能到代码,后台运行挂着进程监听就行了。然而我发现很多人还会加上 Gunicorn + Nginx 的方案,我不太了解这两东西,为什么有一个 Linux 服务器,为什么还需要套一个 Web 服务器(小白无知还请大佬理解)。翻阅一些参考资料,我的初步看法是,这两方案主要是为了提高并发能力、进程管理,加上其他的一些机制,组成一个完成度高的、稳定的服务器。这里我借用下别人的图[4],很精辟。那现在就尝试搭建 Web 服务器吧。
yum install uwsgi nginx # 安装 uwsgi, Nginx
3. 搭建服务器
3.1 创建虚拟环境[5]
为了保证项目的独立性,避免造成各项目的依赖混乱。
mkdir weixin_public # 新建项目文件夹
python3 -m venv env_wp # 新建虚拟环境
接下来的命令将会在 weixin_public
目录下进行,安装必要工具。
Note:
- 运行
ll
命令确认env_wp
目录权限所有者为用户,而非root
。若env_wp
目录权限所有者为root
却以用户的身份进入虚拟环境,pip
安装依赖包或其他操作可能会引起权限报错Permission denied
,请务必注意。 - 进入虚拟环境,运行
source ./env_wp/bin/activate
。 - 退出虚拟环境,运行
deactivate
。 - 停用环境后需要完全删除环境,则可以运行
rm -rf env_wp
。
3.2 激活虚拟环境
source ./env_wp/bin/activate # 激活 weixin_public 项目下的虚拟环境 env_wp
pip3 list # 查看 pip 列表
# 可看出虚拟环境的 pip 列表很干净
==================
Package Version
---------- -------
pip 19.0.3
setuptools 40.8.0
==================
pip3 install flask gunicorn werobot cryptography # 虚拟环境中 安装 gunicorn werobot cryptography
pip3 list # 再次查看 pip 列表
# 安装后虚拟环境中 pip list 发生变化
======================
Package Version
------------ ---------
bottle 0.12.17
certifi 2019.9.11
cffi 1.13.2
chardet 3.0.4
Click 7.0
cryptography 2.8
Flask 1.1.1
gunicorn 20.0.0
idna 2.8
itsdangerous 1.1.0
Jinja2 2.10.3
MarkupSafe 1.1.1
pip 19.0.3
pycparser 2.19
requests 2.22.0
setuptools 40.8.0
six 1.13.0
urllib3 1.25.7
Werkzeug 0.16.0
WeRoBot 1.10.1
xmltodict 0.12.0
======================
3.3 Nginx 反向代理
3.3.1 创建 Nginx 配置文件
sudo vim /etc/nginx/conf.d/weixin_public.conf # 创建项目所需的 nginx 配置文件
Note:
sudo
命令将获取root
权限进行运行。-
此处用户若第一次使用
sudo
命令,会弹出一则警告。并要求输入用户密码。We trust you have received the usual lecture from the local System Administrator. It usually boils down to these three things: #1) Respect the privacy of others. #2) Think before you type. #3) With great power comes great responsibility. [sudo] password for wechat: ---- 翻译 我们确信你已经收到了本地系统管理员的指导。通常可归为: #1) 尊重别人的隐私。 #2) 打字前要多想。 #3) 权利越大,责任越大。
Nginx 配置文件编写内容如下。保存退出。
server {
listen 80;
server_name 阿里云服务器公网IP; # 域名亦可
access_log /var/log/nginx/access.log; # 设置正常通信日志
error_log /var/log/nginx/error.log; # 设置报错日志
location /项目名/ {
proxy_pass http://127.0.0.1:8000; # 反向代理 Gunicorn 本地的服务地址
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Note:
- 注意配置文件内的 阿里云服务器公网IP 以及 项目名 需要自行修改。
- location /项目名/ 的意义是 不论项目名前面有多少父目录,只要请求路径中含有"项目名"即可访问,如
http://阿里云服务器公网IP/weixin_public/
。
3.3.2 启动 Nginx 服务
sudo nginx -t # 检查 nginx 的 .conf 配置文件的有效性
# 返回如下信息显示无异常
###########################
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
###########################
sudo systemctl start nginx # 运行 nginx 服务
sudo systemctl enable nginx # 启动 nginx 服务
sudo systemctl status nginx # 查看 nginx 服务状态
# 若返回如下信息则 nginx 无异常
###########################
● nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)
Drop-In: /etc/systemd/system/nginx.service.d
└─override.conf
Active: active (running) since Thu 2019-11-21 11:54:11 CST; 21s ago
Process: 10679 ExecStartPost=/bin/sleep 0.1 (code=exited, status=0/SUCCESS)
Process: 10676 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
Process: 10672 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 10671 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
Main PID: 10678 (nginx)
CGroup: /system.slice/nginx.service
├─10678 nginx: master process /usr/sbin/nginx
└─10680 nginx: worker process
Nov 21 11:54:11 simple systemd[1]: Stopped The nginx HTTP and reverse proxy server.
Nov 21 11:54:11 simple systemd[1]: Starting The nginx HTTP and reverse proxy server...
Nov 21 11:54:11 simple nginx[10672]: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
Nov 21 11:54:11 simple nginx[10672]: nginx: configuration file /etc/nginx/nginx.conf test is successful
Nov 21 11:54:11 simple systemd[1]: Started The nginx HTTP and reverse proxy server.
###########################
Note:
- 若停止 Nginx 服务,可使用
sudo service nginx stop
。 - 即使在
/etc/nginx/conf.d
中,也不要重复设置 server ,尤其是同一个 ip ,否则会造成地址冲突,导致服务启动失败。 - 每次修改 nginx 配置文件后,无需 先
stop
后restart
再enable
,可直接sudo nginx -s reload
热启动,重新加载新配置文件。 - 若 nginx 服务状态信息倒数第二行出现报错提示
Failed to read PID from file /run/nginx.pid: Invalid argument
,在网上找到了比较靠谱的解释[6]。产生的原因是 nginx 启动需要一点点时间,而 systemd 在 nginx 完成启动前就去读取 pid file,导致获取失败。解决方法如下。
# 先退出虚拟环境,切换成 root 管理员
deactivate
su root
# 在该路径新建名为 nginx.service.d 的文件夹
mkdir -p /etc/systemd/system/nginx.service.d
# 在刚刚新建的文件夹中新建名为 override.conf 的配置文件
vim /etc/systemd/system/nginx.service.d/override.conf
# 文件中键入如下内容后,:wq 保存退出
[Service]
ExecStartPost=/bin/sleep 0.1
# 使服务文件生效
systemctl daemon-reload
# 重启 nginx 服务
systemctl restart nginx.service
# 重新查看 nginx 状态, successful
sudo systemctl status nginx
##################
nginx: configuration file /etc/nginx/nginx.conf test is successful
##################
4. 部署狗屁不通文章生成器
4.1 Flask 应用 + 主业务脚本
创建一个 app.py
脚本,实现机器人收到文本消息后调用狗屁文章的接口生成结果并返回给用户。
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 18 10:15:45 2019
@filename: app.py
@author: Simple
"""
import werobot
import 自动狗屁不通文章生成器 as gp
from flask import Flask
from werobot.contrib.flask import make_view
robot = werobot.WeRoBot(token='$$$$') # 微信公众平台配置设置的Token
robot.config["APP_ID"] = "$$$$" # 微信公众平台的APP_ID
robot.config["ENCODING_AES_KEY"] = "$$$$" #微信公众平台配置的ENCODING_AES_KEY
# 被关注
@robot.subscribe
def subscribe(message):
return '''咸鱼,你来辣~'''
# 收到文本消息
@robot.text
def hello(text):
xx = text.content
for x in xx:
tmp = str()
while (len(tmp) < 100) :
分支 = gp.random.randint(0,100)
if 分支 < 5:
tmp += gp.另起一段()
elif 分支 < 20 :
tmp += gp.来点名人名言()
else:
tmp += next(gp.下一句废话)
tmp = tmp.replace("x",xx)
return tmp
app = Flask(__name__)
app.add_url_rule(rule='/weixin_public/', # WeRoBot 挂载地址,即项目目录名
endpoint='werobot', # Flask 的 endpoint
view_func=make_view(robot),
methods=['GET', 'POST'])
if __name__ == '__main__':
app.run()
Note:
-
rule='/weixin_public/'
中 weixin_public 项目名后必须带/
号,且微信公众平台的配置URL
也要以/weixin_public/
结尾。
4.2 启动 Gunicorn 服务器
gunicorn -w 1 -b 127.0.0.1:8000 app:app
# 参数解释
# -w 1: -w 等价于 --workers, 后接 INT,用于处理工作进程的数量,为正整数,默认为 1
# -b 127.0.0.1:8000: -b 等价于 --bind, 后接 ADDRESS, IP 加端口,绑定运行的主机
# first app: 指前文的 app.py
# second app: flask 应用
正常运行则会显示监听状态
[2019-11-20 16:25:51 +0800] [7366] [INFO] Starting gunicorn 20.0.0
[2019-11-20 16:25:51 +0800] [7366] [INFO] Listening at: http://127.0.0.1:8000 (7366)
[2019-11-20 16:25:51 +0800] [7366] [INFO] Using worker: sync
[2019-11-20 16:25:51 +0800] [7370] [INFO] Booting worker with pid: 7370
浏览器打开 http://你的阿里云公网IP/weixin_public/
, 网页显示 “这是一个 WeRoBot 应用”,无异常。这时我们可以去微信后台将 URL 修改为 http://你的阿里云公网IP/weixin_public/
,提交成功。公众号发送消息,返回狗屁文章成功。
异常纠错心得:
若网页显示 404、503,去查看运行日志以及报错日志的具体报错原因,路径在 nginx 配置中写明,以我为例,/var/log/nginx/access.log
以及/var/log/nginx/error.log
。仔细查看 nginx 配置文件是否有疏漏。
若网页显示 502 Bad Gateway,检查 gunicorn 是否启动成功,是否有报错堆栈信息。
4.3 Systemd 进程管理
当退出终端或终端意外重启时为了保证进程正常运行或重启,使用 Systemd 进行管理[7]。
sudo vim /etc/systemd/system/weixin_public.service # 创建一个服务文件
文件内容如下
[Unit]
Description=Gunicorn instance to serve weixin_public
After=network.target
Wants=nginx.service
[Service]
User=wechat
Group=nginx
WorkingDirectory=/home/wechat/weixin_public
Environment="PATH=/home/wechat/weixin_public/env_wp/bin"
ExecStart=/home/wechat/weixin_public/env_wp/bin/gunicorn -w 1 -b 127.0.0.1:8000 --log-file gunicorn_error.logapp:app
[Install]
WantedBy=multi-user.target
Note: 服务配置文件汇总不能含有中文,备注也不行,会导致报错
- Description: 描述
- After: network 运行后才启动
- Wants: 弱依赖
- User: 以 wechat 用户权限启动
- Group: 当前控制组里面的所有子进程,都会被杀掉
- WorkingDirectory: 工作目录
- Environment: 环境
- ExecStart: 定义启动进程时执行的命令
- WantedBy: 表示该服务所在的 Target。
重启服务
sudo systemctl daemon-reload # 使服务文件生效
sudo systemctl start weixin_public.service # 启动 weixin_public 服务
sudo systemctl enable weixin_public.service # 使 weixin_public 服务可用
浏览器打开 http://你的阿里云公网IP/weixin_public/
, 网页显示 “这是一个 WeRoBot 应用”,无异常。
打开公众号回复消息,机器人没有自动回复狗屁文章。
运行 sudo systemctl status weixin_public
,有堆栈报错信息。
Nov 22 14:56:57 simple gunicorn[12889]: Catch an exception
Nov 22 14:56:57 simple gunicorn[12889]: Traceback (most recent call last):
Nov 22 14:56:57 simple gunicorn[12889]: File "/home/wechat/weixin_public/env_wp/lib/python3.7/site-packages/werobot/robot.py", line 594, in get_reply
Nov 22 14:56:57 simple gunicorn[12889]: session_storage[id] = session
Nov 22 14:56:57 simple gunicorn[12889]: File "/home/wechat/weixin_public/env_wp/lib/python3.7/site-packages/werobot/session/__init__.py", line 15, in __setitem__
Nov 22 14:56:57 simple gunicorn[12889]: self.set(id, session)
Nov 22 14:56:57 simple gunicorn[12889]: File "/home/wechat/weixin_public/env_wp/lib/python3.7/site-packages/werobot/session/sqlitestorage.py", line 55, in set
Nov 22 14:56:57 simple gunicorn[12889]: (id, json_dumps(value))
Nov 22 14:56:57 simple gunicorn[12889]: sqlite3.OperationalError: attempt to write a readonly database
Nov 22 14:56:57 simple gunicorn[12889]: No handler responded message <werobot.messages.messages.TextMessage object at 0x7f97a9ad7fd0>
从报错信息来看,是 sqlite3 的某个文件没有写的权限。我们追溯到 /home/wechat/weixin_public/env_wp/lib/python3.7/site-packages/werobot/session/sqlitestorage.py
中,部分内容如下。
class SQLiteStorage(SessionStorage):
"""
SQLiteStorge 会把 Session 数据储存在一个 SQLite 数据库文件中 ::
import werobot
from werobot.session.sqlitestorage import SQLiteStorage
session_storage = SQLiteStorage
robot = werobot.WeRoBot(token="token", enable_session=True,
session_storage=session_storage)
:param filename: SQLite数据库的文件名, 默认是 ``werobot_session.sqlite3``
"""
def __init__(self, filename='werobot_session.sqlite3'):
self.db = sqlite3.connect(filename, check_same_thread=False)
self.db.text_factory = str
self.db.execute(__CREATE_TABLE_SQL__)
......
类 SQLiteStorage
的 docstring 来看, werobot_session.sqlite3
的嫌疑很大。这文件好像在哪见过,没错他就在你的项目文件夹下,由 WeRobot 自动生成。有两种解决方法:
- 运行
chmod 777 werobot_session.sqlite3
,更改werobot_session.sqlite3
文件的权限为任何人都可读、写、运行的权限。 - WeRobot 官方文档中提到 Session 是用于实现记录用户状态的,可以选择关闭,需要修改Config,代码中可添加
robot.config['SESSION_STORAGE'] = False
即可解决。
重新 reload/start/enabel 服务后,发送消息,自动回复成功。部署完成。
如果文章对你有所帮助,请不要吝啬手中的赞哦。心情会美美哒。
WX: xianyu_splash, 本公众号用于记录本人学习历程、基础技术,分享日常灵感、优质工具,欢迎关注!共同进步!