Flask結(jié)合tornado和Nginx部署為Windows Service服務(wù)并開機(jī)自啟

Flask部署為服務(wù)需要三步:

  1. Flask結(jié)合tornado部署項目;
  2. 利用win32模塊包裝第一步的項目啟動代碼;
  3. 在前兩部的基礎(chǔ)上配置Nginx轉(zhuǎn)發(fā);

Flask結(jié)合tornado部署項目

經(jīng)過嘗試,我發(fā)現(xiàn)直接使用Flask原始的app.run()或結(jié)合了flask_script插件后的manage.run()都不能成功的設(shè)置為Windows的服務(wù),并且那兩種方式也不適合作為項目的部署方式。
Linxu中可以使用gunicornuwsgi作為WSGI服務(wù)器,但在Windows中都不能用,最后發(fā)現(xiàn)結(jié)合tornado充當(dāng)WSGI服務(wù)器可以完美設(shè)置為Windows的服務(wù)。
我們用一個非常簡單的Flask工程來進(jìn)行測試,Flask工程僅僅包含2個文件:

  1. app.py:作為Flask程序;
  2. server.py:結(jié)合Tornado作為WSGI服務(wù)器啟動Flask項目;

app.py代碼如下:

import time
from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():
    return 'Hello World!'

@app.route("/sleep")  # 為了測試請求是否只是異步
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)方式,因為這種方式在windows上不支持相應(yīng)的add_reader APIs,就會拋出NotImplementedError錯誤。
# 因此在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)前目錄下通過運行service.py文件來啟動Flask程序:

python service.py

瀏覽器訪問:127.0.0.1:9900,返回Hello World!表示Flask結(jié)合Tornado部署成功。
[圖片上傳失敗...(image-8fe8a-1617348857245)]
如果以上成功了,就可以進(jìn)行第二部啦,否則看下面的步驟也沒用,因為后面都是基于這個步驟的。

利用Win32模塊將Flask項目制作成Windows服務(wù)

如果想用Python開發(fā)Windows程序,并讓其開機(jī)啟動等,就必須寫成Windows的服務(wù)程序Windows Service,用Python來做這個事情必須要借助第三方模塊pywin32,下面有一個簡單模板,先來將下模板各部分的作用:

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停止這個過程 
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 
        # 設(shè)置事件 
        win32event.SetEvent(self.hWaitStop) 
  
if __name__=='__main__': 
    win32serviceutil.HandleCommandLine(PythonService) 
    # 括號里參數(shù)可以改成其他名字,但是必須與class類名一致

上面模板的執(zhí)行流程:

  1. 在類PythonService__init__函數(shù)執(zhí)行完后,系統(tǒng)服務(wù)開始啟動,Windows系統(tǒng)會自動調(diào)用SvcDoRun函數(shù);
  2. SvcDoRun這個函數(shù)的執(zhí)行不可以結(jié)束,因為結(jié)束就代表服務(wù)停止。所以當(dāng)我們放自己的代碼在SvcDoRun函數(shù)中執(zhí)行的時候,必須確保該函數(shù)不退出,如果退出或者該函數(shù)沒有正常運行就表示服務(wù)停止;
  3. 當(dāng)停止服務(wù)的時候,系統(tǒng)會調(diào)用SvcStop函數(shù),該函數(shù)通過設(shè)置標(biāo)志位等方式讓SvcDoRun函數(shù)退出,就是正常的停止服務(wù)。例子中是通過event事件讓SvcDoRun函數(shù)停止等待,從而退出該函數(shù),從而使服務(wù)停止。

提示:系統(tǒng)關(guān)機(jī)時不會調(diào)用SvcStop函數(shù),所以服務(wù)可以設(shè)置為開機(jī)自啟的。
類中的方法名、類屬性名稱都是固定的,不可以隨意改變。其中類屬性的值對應(yīng)服務(wù)的展示位置如下圖所示:

在這里插入圖片描述

現(xiàn)在把第一步service.py中的代碼融合到模板中(簡單來將就是把原來的代碼都塞到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程序,用于驗證設(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)超時時間
        self.logger = self._getLogger()  # 獲取日志對象
        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):
        """
        實例化win32serviceutil.ServiceFramework的時候,windows系統(tǒng)會自動調(diào)用SvcDoRun方法,
        這個函數(shù)的執(zhí)行不可以結(jié)束,因為結(jié)束就代表服務(wù)停止。所以當(dāng)我們放自己的代碼在SvcDoRun函數(shù)中執(zhí)行的時候,必須確保該函數(shù)不退出,就需要用死循環(huán)

        :return: None
        """
        self.logger.info("服務(wù)即將啟動...")
        while self.isAlive:
            self.logger.info("服務(wù)正在運行...")
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            result = sock.connect_ex(('127.0.0.1', 9900))  # 嗅探網(wǎng)址是否可以訪問,成功返回0,出錯返回錯誤碼
            if result != 0:
                # Python3.8的asyncio改變了循環(huán)方式,因為這種方式在windows上不支持相應(yīng)的add_reader APIs,就會拋出NotImplementedError錯誤。
                # 因此加入下面兩行代碼
                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ù)的時候,系統(tǒng)會調(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停止這個過程
        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)
            # 如果錯誤的狀態(tài)碼為1063,則輸出使用信息
            if details[0] == winerror.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
                win32serviceutil.usage()
    else:
        win32serviceutil.HandleCommandLine(PythonService)  # 括號里必須與class類名一致

提示:監(jiān)聽端口盡量不要設(shè)置為5000,因為Flask默認(rèn)端口是5000,此項目設(shè)置為Windows服務(wù)后,我們可能會忘記自己在后臺一直占用著5000端口,在編寫其他Flask項目時啟動不起來,把自己繞進(jìn)去。
問題:在if __name__ == '__main__'代碼快中,except部分的代碼是有問題的,但是我也不知道是什么意思,而且一般也走不到這個代碼塊中。其實if __name__ == '__main__'中只寫win32serviceutil.HandleCommandLine(PythonService)也是完全沒有問題的。

服務(wù)操作命令

常用操作命令如下:

# 1.安裝服務(wù)
python PythonService.py install
# 2.以開機(jī)自啟的方式安裝服務(wù)
python PythonService.py --startup auto install
# 3.啟動服務(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í)行啟動服務(wù)命令(剛開始還以為install好它自己就啟來呢,這一頓找Bug,最后發(fā)現(xiàn)是沒啟動服務(wù),坑死),如下圖所示:
[圖片上傳失敗...(image-f21f55-1617348857245)]
瀏覽器輸入127.0.0.1:9900,能正常訪問說明服務(wù)制作成功。
[圖片上傳失敗...(image-1696e7-1617348857245)]

將服務(wù)設(shè)置成開機(jī)自啟

其實開機(jī)自啟,只需要更改安裝命令即可。

# 1.先把剛才的服務(wù)停止
python PythonService.py stop
# 2.刪除剛才的服務(wù)
python PythonService.py remove

# 3.以開機(jī)自啟的方式安裝服務(wù)
python PythonService.py --startup auto install
# 4.手動啟動服務(wù)
python PythonService.py start

重啟電腦后后直接訪問127.0.0.1:9900,此時應(yīng)該可以照常訪問,成功。

結(jié)合Nginx實現(xiàn)請求轉(zhuǎn)發(fā)

結(jié)合Nginx完全按實際需求,如果用不到可以不用。
結(jié)合Nginx的話需要做兩件事:

  1. 設(shè)置Nginx為開機(jī)自啟
  2. 轉(zhuǎn)發(fā)請求到9900端口
安裝Nginx并設(shè)置開機(jī)自啟

具體查看“Nginx學(xué)習(xí)筆記”中的“Windows安裝Nginx”:https://blog.csdn.net/u013487601/article/details/115392254

配置Nginx轉(zhuǎn)發(fā)請求

當(dāng)用戶在瀏覽器中輸入http://localhostNginx自動轉(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
  }

配置完成后,重新啟動nginx,當(dāng)用戶在瀏覽器中輸入http://localhost,nginx將請求轉(zhuǎn)發(fā)到9900端口,從而關(guān)聯(lián)到我們的Flask程序。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容