將Flask
部署為服務(wù)需要三步:
-
Flask
結(jié)合tornado
部署項(xiàng)目襟士; - 利用
win32
模塊包裝第一步的項(xiàng)目啟動(dòng)代碼; - 在前兩部的基礎(chǔ)上配置
Nginx
轉(zhuǎn)發(fā);
Flask結(jié)合tornado部署項(xiàng)目
經(jīng)過嘗試,我發(fā)現(xiàn)直接使用Flask原始的app.run()
或結(jié)合了flask_script
插件后的manage.run()
都不能成功的設(shè)置為Windows
的服務(wù)初狰,并且那兩種方式也不適合作為項(xiàng)目的部署方式。
在Linxu
中可以使用gunicorn
或uwsgi
作為WSGI
服務(wù)器互例,但在Windows
中都不能用奢入,最后發(fā)現(xiàn)結(jié)合tornado
充當(dāng)WSGI
服務(wù)器可以完美設(shè)置為Windows
的服務(wù)。
我們用一個(gè)非常簡(jiǎn)單的Flask
工程來進(jìn)行測(cè)試媳叨,Flask
工程僅僅包含2個(gè)文件:
-
app.py
:作為Flask
程序腥光; -
server.py
:結(jié)合Tornado
作為WSGI
服務(wù)器啟動(dòng)Flask
項(xiàng)目;
app.py
代碼如下:
import time
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
@app.route("/sleep") # 為了測(cè)試請(qǐng)求是否只是異步
def sleep():
time.sleep(15)
return "Sleep 15's"
service.py
代碼如下:
import sys
import asyncio
from tornado.ioloop import IOLoop
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from app import app
# Python3.8的asyncio改變了循環(huán)方式糊秆,因?yàn)檫@種方式在windows上不支持相應(yīng)的add_reader APIs武福,就會(huì)拋出NotImplementedError錯(cuò)誤。
# 因此在python3.8及更高版本需要加入下面兩行代碼痘番,其他版本不需要
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
if __name__ == '__main__':
http_server = HTTPServer(WSGIContainer(app))
http_server.listen(9900) # 監(jiān)聽9900端口
IOLoop.current().start()
在當(dāng)前目錄下通過運(yùn)行service.py
文件來啟動(dòng)Flask
程序:
python service.py
瀏覽器訪問:127.0.0.1:9900
捉片,返回Hello World!
表示Flask
結(jié)合Tornado
部署成功。
[圖片上傳失敗...(image-8fe8a-1617348857245)]
如果以上成功了汞舱,就可以進(jìn)行第二部啦伍纫,否則看下面的步驟也沒用,因?yàn)楹竺娑际腔谶@個(gè)步驟的昂芜。
利用Win32
模塊將Flask
項(xiàng)目制作成Windows
服務(wù)
如果想用Python
開發(fā)Windows
程序莹规,并讓其開機(jī)啟動(dòng)等,就必須寫成Windows
的服務(wù)程序Windows Service
泌神,用Python
來做這個(gè)事情必須要借助第三方模塊pywin32
良漱,下面有一個(gè)簡(jiǎn)單模板舞虱,先來將下模板各部分的作用:
import win32event
import win32service
import win32serviceutil
class PythonService(win32serviceutil.ServiceFramework):
_svc_name_ = "PythonService" # 服務(wù)名
_svc_display_name_ = "Python Service Test" # 服務(wù)在windows系統(tǒng)中顯示的名稱
_svc_description_ = "This code is a Python service Test" # 服務(wù)的描述
def __init__(self, args):
# __init__的寫法基本固定,可以參考幫助文檔中的任意一種
# https://www.programcreek.com/python/example/99659/win32serviceutil.ServiceFramework
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
def SvcDoRun(self):
# 把自己的代碼放到這里债热,就OK
# 等待服務(wù)被停止
win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
def SvcStop(self):
# 先告訴SCM停止這個(gè)過程
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# 設(shè)置事件
win32event.SetEvent(self.hWaitStop)
if __name__=='__main__':
win32serviceutil.HandleCommandLine(PythonService)
# 括號(hào)里參數(shù)可以改成其他名字砾嫉,但是必須與class類名一致
上面模板的執(zhí)行流程:
- 在類
PythonService
的__init__
函數(shù)執(zhí)行完后幼苛,系統(tǒng)服務(wù)開始啟動(dòng)窒篱,Windows
系統(tǒng)會(huì)自動(dòng)調(diào)用SvcDoRun
函數(shù); -
SvcDoRun
這個(gè)函數(shù)的執(zhí)行不可以結(jié)束,因?yàn)榻Y(jié)束就代表服務(wù)停止舶沿。所以當(dāng)我們放自己的代碼在SvcDoRun
函數(shù)中執(zhí)行的時(shí)候墙杯,必須確保該函數(shù)不退出,如果退出或者該函數(shù)沒有正常運(yùn)行就表示服務(wù)停止; - 當(dāng)停止服務(wù)的時(shí)候括荡,系統(tǒng)會(huì)調(diào)用
SvcStop
函數(shù)高镐,該函數(shù)通過設(shè)置標(biāo)志位等方式讓SvcDoRun
函數(shù)退出,就是正常的停止服務(wù)畸冲。例子中是通過event
事件讓SvcDoRun
函數(shù)停止等待嫉髓,從而退出該函數(shù),從而使服務(wù)停止邑闲。
提示:系統(tǒng)關(guān)機(jī)時(shí)不會(huì)調(diào)用SvcStop
函數(shù)算行,所以服務(wù)可以設(shè)置為開機(jī)自啟的。
類中的方法名苫耸、類屬性名稱都是固定的州邢,不可以隨意改變。其中類屬性的值對(duì)應(yīng)服務(wù)的展示位置如下圖所示:
現(xiàn)在把第一步
service.py
中的代碼融合到模板中(簡(jiǎn)單來將就是把原來的代碼都塞到SvcDoRun
方法中):
# -*- coding:utf-8 -*-
import os
import sys
import time
import socket
import asyncio
import logging
import inspect
import winerror
import win32event
import win32service
import servicemanager
import win32serviceutil
from tornado.ioloop import IOLoop
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from app import app
class PythonService(win32serviceutil.ServiceFramework):
_svc_name_ = 'Flask_Web' # 屬性中的服務(wù)名
_svc_display_name_ = 'FLASK_WEB' # 服務(wù)在windows系統(tǒng)中顯示的名稱
_svc_description_ = 'Python的Flask程序褪子,用于驗(yàn)證設(shè)置Windows服務(wù)量淌,且開機(jī)自啟' # 服務(wù)的描述
def __init__(self, args):
"""
init的內(nèi)容可以參考以下網(wǎng)址:
https://www.programcreek.com/python/example/99659/win32serviceutil.ServiceFramework
:param args:
"""
win32serviceutil.ServiceFramework.__init__(self, args)
self.stop_event = win32event.CreateEvent(None, 0, 0, None)
socket.setdefaulttimeout(60) # 套接字設(shè)置默認(rèn)超時(shí)時(shí)間
self.logger = self._getLogger() # 獲取日志對(duì)象
self.isAlive = True
def _getLogger(self):
# 設(shè)置日志功能
logger = logging.getLogger('[PythonService]')
this_file = inspect.getfile(inspect.currentframe())
dirpath = os.path.abspath(os.path.dirname(this_file))
handler = logging.FileHandler(os.path.join(dirpath, "service.log"))
formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
def SvcDoRun(self):
"""
實(shí)例化win32serviceutil.ServiceFramework的時(shí)候,windows系統(tǒng)會(huì)自動(dòng)調(diào)用SvcDoRun方法嫌褪,
這個(gè)函數(shù)的執(zhí)行不可以結(jié)束呀枢,因?yàn)榻Y(jié)束就代表服務(wù)停止。所以當(dāng)我們放自己的代碼在SvcDoRun函數(shù)中執(zhí)行的時(shí)候笼痛,必須確保該函數(shù)不退出裙秋,就需要用死循環(huán)
:return: None
"""
self.logger.info("服務(wù)即將啟動(dòng)...")
while self.isAlive:
self.logger.info("服務(wù)正在運(yùn)行...")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('127.0.0.1', 9900)) # 嗅探網(wǎng)址是否可以訪問,成功返回0晃痴,出錯(cuò)返回錯(cuò)誤碼
if result != 0:
# Python3.8的asyncio改變了循環(huán)方式残吩,因?yàn)檫@種方式在windows上不支持相應(yīng)的add_reader APIs,就會(huì)拋出NotImplementedError錯(cuò)誤倘核。
# 因此加入下面兩行代碼
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
s = HTTPServer(WSGIContainer(app))
s.listen(9900)
IOLoop.current().start()
time.sleep(8)
sock.close()
time.sleep(20)
def SvcStop(self):
"""
當(dāng)停止服務(wù)的時(shí)候泣侮,系統(tǒng)會(huì)調(diào)用SvcStop函數(shù),該函數(shù)通過設(shè)置標(biāo)志位等方式讓SvcDoRun函數(shù)退出紧唱,就是正常的停止服務(wù)活尊。
win32event.SetEvent(self.hWaitStop) 通過事件退出
:return: None
"""
self.logger.info("服務(wù)即將關(guān)閉...")
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) # 先告訴SCM停止這個(gè)過程
win32event.SetEvent(self.stop_event) # 設(shè)置事件
self.ReportServiceStatus(win32service.SERVICE_STOPPED) # 確保停止隶校,也可不加
self.isAlive = False
if __name__ == '__main__':
print("接收到的參數(shù)為", sys.argv)
if len(sys.argv) == 1:
print("輸入?yún)?shù)不正確!")
try:
evtsrc_dll = os.path.abspath(servicemanager.__file__)
servicemanager.PrepareToHostSingle(PythonService)
servicemanager.Initialize('PythonService', evtsrc_dll)
servicemanager.StartServiceCtrlDispatcher()
except Exception as details:
print("發(fā)生異常蛹锰,信息如下:", details)
# 如果錯(cuò)誤的狀態(tài)碼為1063深胳,則輸出使用信息
if details[0] == winerror.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
win32serviceutil.usage()
else:
win32serviceutil.HandleCommandLine(PythonService) # 括號(hào)里必須與class類名一致
提示:監(jiān)聽端口盡量不要設(shè)置為5000,因?yàn)镕lask默認(rèn)端口是5000铜犬,此項(xiàng)目設(shè)置為Windows
服務(wù)后舞终,我們可能會(huì)忘記自己在后臺(tái)一直占用著5000端口,在編寫其他Flask項(xiàng)目時(shí)啟動(dòng)不起來癣猾,把自己繞進(jìn)去敛劝。
問題:在if __name__ == '__main__'
代碼快中,except
部分的代碼是有問題的纷宇,但是我也不知道是什么意思夸盟,而且一般也走不到這個(gè)代碼塊中。其實(shí)if __name__ == '__main__'
中只寫win32serviceutil.HandleCommandLine(PythonService)
也是完全沒有問題的像捶。
服務(wù)操作命令
常用操作命令如下:
# 1.安裝服務(wù)
python PythonService.py install
# 2.以開機(jī)自啟的方式安裝服務(wù)
python PythonService.py --startup auto install
# 3.啟動(dòng)服務(wù)
python PythonService.py start
# 4.重啟服務(wù)
python PythonService.py restart
# 5.停止服務(wù)
python PythonService.py stop
# 6.刪除/卸載服務(wù)
python PythonService.py remove
先執(zhí)行安裝服務(wù)命令上陕,再執(zhí)行啟動(dòng)服務(wù)命令(剛開始還以為install
好它自己就啟來呢,這一頓找Bug
拓春,最后發(fā)現(xiàn)是沒啟動(dòng)服務(wù)释簿,坑死),如下圖所示:
[圖片上傳失敗...(image-f21f55-1617348857245)]
瀏覽器輸入127.0.0.1:9900
痘儡,能正常訪問說明服務(wù)制作成功辕万。
[圖片上傳失敗...(image-1696e7-1617348857245)]
將服務(wù)設(shè)置成開機(jī)自啟
其實(shí)開機(jī)自啟,只需要更改安裝命令即可沉删。
# 1.先把剛才的服務(wù)停止
python PythonService.py stop
# 2.刪除剛才的服務(wù)
python PythonService.py remove
# 3.以開機(jī)自啟的方式安裝服務(wù)
python PythonService.py --startup auto install
# 4.手動(dòng)啟動(dòng)服務(wù)
python PythonService.py start
重啟電腦后后直接訪問127.0.0.1:9900
渐尿,此時(shí)應(yīng)該可以照常訪問,成功矾瑰。
結(jié)合Nginx
實(shí)現(xiàn)請(qǐng)求轉(zhuǎn)發(fā)
結(jié)合Nginx
完全按實(shí)際需求砖茸,如果用不到可以不用。
結(jié)合Nginx
的話需要做兩件事:
- 設(shè)置
Nginx
為開機(jī)自啟 - 轉(zhuǎn)發(fā)請(qǐng)求到9900端口
安裝Nginx
并設(shè)置開機(jī)自啟
具體查看“Nginx
學(xué)習(xí)筆記”中的“Windows
安裝Nginx
”:https://blog.csdn.net/u013487601/article/details/115392254
配置Nginx
轉(zhuǎn)發(fā)請(qǐng)求
當(dāng)用戶在瀏覽器中輸入http://localhost
殴穴,Nginx
自動(dòng)轉(zhuǎn)發(fā)到9900端口凉夯,這樣就可以關(guān)聯(lián)到Tornado
充當(dāng)?shù)?code>WSGI服務(wù)器。
Nginx
的配置文件nginx.conf
在安裝目錄下conf
子文件加下采幌,打開該文件劲够,進(jìn)行如下配置:
http {
server {
listen 80;
server_name localhost;
server_name 127.0.0.1;
charset utf-8;
location / {
root html;
index index.html index.htm;
proxy_pass http://localhost:9900; # 加上這句
}
# other configurations
}
配置完成后,重新啟動(dòng)nginx
休傍,當(dāng)用戶在瀏覽器中輸入http://localhost
征绎,nginx
將請(qǐng)求轉(zhuǎn)發(fā)到9900端口,從而關(guān)聯(lián)到我們的Flask
程序磨取。