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

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

  1. Flask結(jié)合tornado部署項(xiàng)目襟士;
  2. 利用win32模塊包裝第一步的項(xiàng)目啟動(dòng)代碼;
  3. 在前兩部的基礎(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中可以使用gunicornuwsgi作為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è)文件:

  1. app.py:作為Flask程序腥光;
  2. 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í)行流程:

  1. 在類PythonService__init__函數(shù)執(zhí)行完后幼苛,系統(tǒng)服務(wù)開始啟動(dòng)窒篱,Windows系統(tǒng)會(huì)自動(dòng)調(diào)用SvcDoRun函數(shù);
  2. SvcDoRun這個(gè)函數(shù)的執(zhí)行不可以結(jié)束,因?yàn)榻Y(jié)束就代表服務(wù)停止舶沿。所以當(dāng)我們放自己的代碼在SvcDoRun函數(shù)中執(zhí)行的時(shí)候墙杯,必須確保該函數(shù)不退出,如果退出或者該函數(shù)沒有正常運(yùn)行就表示服務(wù)停止;
  3. 當(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的話需要做兩件事:

  1. 設(shè)置Nginx為開機(jī)自啟
  2. 轉(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程序磨取。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末人柿,一起剝皮案震驚了整個(gè)濱河市柴墩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凫岖,老刑警劉巖江咳,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異哥放,居然都是意外死亡歼指,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門婶芭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來东臀,“玉大人着饥,你說我怎么就攤上這事犀农。” “怎么了宰掉?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵呵哨,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我轨奄,道長(zhǎng)孟害,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任挪拟,我火速辦了婚禮挨务,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘玉组。我一直安慰自己谎柄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布惯雳。 她就那樣靜靜地躺著朝巫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪石景。 梳的紋絲不亂的頭發(fā)上劈猿,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音潮孽,去河邊找鬼揪荣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛往史,可吹牛的內(nèi)容都是我干的仗颈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼怠堪,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼揽乱!你這毒婦竟也來了名眉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤凰棉,失蹤者是張志新(化名)和其女友劉穎损拢,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撒犀,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡福压,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了或舞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荆姆。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖映凳,靈堂內(nèi)的尸體忽然破棺而出胆筒,到底是詐尸還是另有隱情,我是刑警寧澤诈豌,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布仆救,位于F島的核電站,受9級(jí)特大地震影響矫渔,放射性物質(zhì)發(fā)生泄漏彤蔽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一庙洼、第九天 我趴在偏房一處隱蔽的房頂上張望顿痪。 院中可真熱鬧,春花似錦油够、人聲如沸蚁袭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撕阎。三九已至,卻和暖如春碌补,著一層夾襖步出監(jiān)牢的瞬間虏束,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工厦章, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镇匀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓袜啃,卻偏偏與公主長(zhǎng)得像汗侵,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容