阿里雲函數計算 使用Python開發一個基於WSGI的HTTP觸發器 (實戰)

廢話不多說,直接上乾貨

 

源碼下載:

鏈接:https://pan.baidu.com/s/1OFMCtNglHHQnBlfedpdPmw 
提取碼:w465

 

文章大綱:

1. 需求背景

2. 技術選型

3. 代碼開發

4. 部署項目到阿里雲

1. 需求背景

大致想實現一個 端口-手機號 配置的功能,並且可以接收到指定手機發來的短信,將消息過濾後轉發至釘釘羣。

ps:該項目僅是服務端,另一端是Android,Android配置硬件實現端口切換【一個端口對應一張手機卡,共16個端口】,使手機使用某張卡,隨後將收到的短信發送至服務端,服務端過濾消息後,將消息轉發到釘釘羣。這樣就可以方便整個公司的員工接收短信驗證碼,提高效率。

原型大致如下 :

(1)端口配置:

此處包含 增刪改查 方法,相關數據存在redis,後文會介紹爲什麼選用redis。

(2)號碼切換

切換到指定手機號,就可以收到該手機號發來的短信。

2. 技術選型

(1)Python 3.7,想挑戰一下自己,並且可以學習一門新的語言,所以選擇了Python

(2)WSGI,沒有選用基於WSGI的框架(flask或者django),因爲時間有限

(3)Redis,沒有選用數據庫,是因爲該項目與業務關係不大,只是爲了方便接收驗證碼

(4)阿里雲函數計算 HTTP觸發器

3. 代碼開發

將項目部署到阿里雲之前,需要讓項目跑起來,並單測完成。

本地選用的是wsgiref.simple_server,來模擬一個服務。

實現simple_server有三種方式(一個函數,一個類,或者一個重載了__call__的類的實例),我選用的是最後一種方式。

ps:由於沒有使用web框架,整個項目實現思想有點類似於Java Web

項目結構:

下面直接貼代碼

(1)fcIndex.py

from sms_log import SMSLog
from urllib.parse import unquote
from redis_util import RedisUtil
from port_phone_conf import PortPhoneConf
from wsgiref.simple_server import make_server


"""路由配置"""
URL_PATTERNS = (
    ('/', 'list'),
    ('/list', 'list'),
    ('/insert', 'insert'),
    ('/update', 'update'),
    ('/delete', 'delete'),
    ('/sms', 'sms'),
    ('/set_current_conf', 'set_current_conf')
)


class AppClass:

    @staticmethod
    def _match(path):
        """解析應該調用哪個app"""
        path = path.split('/')[1]
        # path = path[path.rfind('/'):]
        for url, app in URL_PATTERNS:
            if path in url:
                return app

    @staticmethod
    def parse_params(params):
        """解析請求參數"""
        if not params:
            return None

        result = {}
        items = params.split('&')
        for item in items:
            result[item.split("=")[0]] = item.split('=')[1]

        return result

    def __call__(self, environ, start_response):
        """入口"""
        path = environ.get('PATH_INFO', '/')
        app = self._match(path)

        if app:
            if app in ['list']:
                """get"""
                params = self.parse_params(environ['QUERY_STRING'])
            else:
                """post"""
                request_body_size = int(environ.get("CONTENT_LENGTH", 0))
                request_body_str = unquote(environ["wsgi.input"].read(request_body_size).decode('utf-8'))
                params = self.parse_params(request_body_str)

            app = globals()[app]
            result = app(params)
            start_response("200 OK", [('Content-type', 'text/plain; charset=utf-8')])
            return result
        else:
            start_response("404 NOT FOUND", [('Content-type', 'text/plain')])
            return [b"Page dose not exists!"]


def list(params):
    """端口配置列表展示 """

    items = PortPhoneConf().list().encode()
    if not items:
        items = []

    return [items, ]


def insert(params):
    """新增配置"""

    try:
        port_no = params['port_no']
        phone_no = params['phone_no']
        key_words = params['key_words']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    PortPhoneConf().insert(port_no, phone_no, key_words)

    return [b"success"]


def update(params):
    """修改配置"""

    try:
        port_no = params['port_no']
        phone_no = params['phone_no']
        key_words = params['key_words']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    PortPhoneConf().update(port_no, phone_no, key_words)

    return [b"success"]


def delete(params):
    """刪除配置"""

    try:
        port_no = params['port_no']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    PortPhoneConf().delete(port_no)

    return [b"success"]


def sms(params):
    """接收短信,並轉發到釘釘羣"""

    try:
        phone_no = params['phone_no']
        msg = params['msg']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    current_phone_no = RedisUtil.comm_get(RedisUtil.CURRENT_PORT_KEY)
    if phone_no == current_phone_no:
        raise Exception(phone_no + "對應端口未開啓")

    SMSLog().redirect_to_ding_talk(msg)

    return [b"success"]


def set_current_conf(params):
    """設置當前開啓的端口"""
    try:
        phone_no = params['phone_no']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    RedisUtil.comm_set(RedisUtil.CURRENT_PORT_KEY, phone_no)

    return [b"success"]


if __name__ == "__main__":
    handler = AppClass()

    httpd = make_server('', 8080, handler)
    print("Serving on port 8080...")
    httpd.serve_forever()

(2)port_phone_conf.py

import json
import time
from redis_util import RedisUtil


class PortPhoneConf:
    """端口-手機號操作類"""

    def insert(self, port_no, phone_no, key_words):
        """新增端口配置"""

        conf = {'port_no': port_no,
                           'phone_no': phone_no,
                           'key_words': key_words,
                           'gmt_create': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                           'gmt_modified': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}

        items = self.list()
        if not items:
            items = []
        else:
            items = json.loads(items)

        items.append(conf)

        RedisUtil.comm_set(RedisUtil.CONF_KEY, items)

    def update(self, port_no, phone_no, key_words):
        """修改端口配置"""

        items = json.loads(self.list())
        for item in items:
            if item["port_no"] == port_no:
                item["phone_no"] = phone_no
                item["key_words"] = key_words
                item["gmt_modified"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())

        RedisUtil.comm_set(RedisUtil.CONF_KEY, items)

    def delete(self, port_no):
        """刪除端口配置"""

        global items
        if port_no:
            items = json.loads(self.list())
            for item in items:
                if item["port_no"] == port_no:
                    items.remove(item)
                    break

        RedisUtil.comm_set(RedisUtil.CONF_KEY, items)

    def list(self):
        """獲取端口配置列表"""

        return RedisUtil.comm_get(RedisUtil.CONF_KEY)

(3)redis_util.py

import json
import redis


class RedisUtil:
    """獲取redis連接,並且操作redis"""

    CONF_KEY = "select::port_phone_conf"

    CURRENT_PORT_KEY = "select::port_phone_conf_current"

    @staticmethod
    def get_conn():
        """返回連接"""
        conn = redis.Redis(host="106.15.176.xxx", port=6379)
        return conn

    @staticmethod
    def comm_set(key, values):
        """通用set方法"""
        global conn
        try:
            conn = RedisUtil.get_conn()
            conn.set(key, json.dumps(values, ensure_ascii=False).encode("utf-8"))
        finally:
            conn.close()

    @staticmethod
    def comm_get(key):
        """通用get方法"""
        global conn
        try:
            conn = RedisUtil.get_conn()
            items = conn.get(key)
        finally:
            conn.close()

        if items:
            items = items.decode("utf-8")

        return items

(4)sms_log.py

import json
import requests


class SMSLog:
    @staticmethod
    def redirect_to_ding_talk(msg):
        """過濾消息並轉發到釘釘羣"""

        if ("阿里雲" in msg) or ("釘釘" in msg):
            url = 'https://oapi.dingtalk.com/robot/send?access_token=xxx'
            data = {"msgtype": "text", "text": {"content": msg}}
            headers = {'Content-Type': 'application/json'}

            requests.request("post", url, json=data, headers=headers)

(5)調試

接下來,就可以運行 fcIndex.py#__main__ 來測試功能了!

4. 部署項目到阿里雲

本地單測完成,就可以部署到阿里雲了,不要沾沾自喜,後面還有好多坑等着呢。

(1)創建函數和服務

進入阿里雲函數計算控制檯#函數_服務 #新建服務

新增函數

特別注意:函數入口一點要寫對,不然會找不到入口(重要)

(2)下載自定義模塊代碼包

查看函數計算的文檔,我們會發現,Python只提供了標準模塊和一些常用模塊。如果需要使用自定義的模塊,則需要將它們與代碼一起打包,在這個項目中,自定義模塊是requests和redis。官方推薦使用Fun,個人覺得代價有點高,放棄。文檔中有這樣一段話:如果沒有 docker 環境,且不涉及動態鏈接庫(.so)、編譯二進制程序等,只是安裝語言依賴,那麼可以直接使用 pip install -t . PyMySQL 的方式進行安裝。這種方式不管函數計算的執行環境中是否安裝了這些 python 庫,都會下載下來,會增加代碼包的大小。

進入項目目錄 ,執行以下命令,下載代碼包。

pip install -t . redis;
pip install -t . requests;

再來看看我們的項目目錄:

(3)修改 fcIndex.py 文件

查看文檔發現,不支持最後一種方式【ps:最後一種方式只是爲了單測方便】,所以我們的代碼需要變動一下,改變後的代碼如下:

from sms_log import SMSLog
from urllib.parse import unquote
from redis_util import RedisUtil
from port_phone_conf import PortPhoneConf
from wsgiref.simple_server import make_server

def handler(environ, start_response):
    """函數計算調用入口"""
    return AppClass(environ, start_response)

"""路由配置"""
URL_PATTERNS = (
    ('/', 'list'),
    ('/list', 'list'),
    ('/insert', 'insert'),
    ('/update', 'update'),
    ('/delete', 'delete'),
    ('/sms', 'sms'),
    ('/set_current_conf', 'set_current_conf')
)


class AppClass:

    @staticmethod
    def _match(path):
        """解析應該調用哪個app"""
        path = path.split('/')[1]
        # path = path[path.rfind('/'):]
        for url, app in URL_PATTERNS:
            if path in url:
                return app

    @staticmethod
    def parse_params(params):
        """解析請求參數"""
        if not params:
            return None

        result = {}
        items = params.split('&')
        for item in items:
            result[item.split("=")[0]] = item.split('=')[1]

        return result

    def __init__(self, environ, start_response):
        self.environ = environ
        self.start_response = start_response

    def __iter__(self):
        path = self.environ.get('PATH_INFO', '/')
        app = self._match(path)

        if app:
            if app in ['list']:
                """get"""
                params = {}
                # params = self.parse_params(self.environ['QUERY_STRING'])
            else:
                """post"""
                request_body_size = int(self.environ.get("CONTENT_LENGTH", 0))
                request_body_str = unquote(self.environ["wsgi.input"].read(request_body_size).decode('utf-8'))
                params = self.parse_params(request_body_str)

            app = globals()[app]
            result = app(params)
            self.start_response("200 OK", [('Content-type', 'text/plain; charset=utf-8')])
            yield result
        else:
            self.start_response("404 NOT FOUND", [('Content-type', 'text/plain')])
            yield b"Page dose not exists!"


def list(params):
    """端口配置列表展示 """

    items = PortPhoneConf().list().encode()
    if not items:
        items = []

    return items


def insert(params):
    """新增配置"""

    try:
        port_no = params['port_no']
        phone_no = params['phone_no']
        key_words = params['key_words']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    PortPhoneConf().insert(port_no, phone_no, key_words)

    return b"success"


def update(params):
    """修改配置"""

    try:
        port_no = params['port_no']
        phone_no = params['phone_no']
        key_words = params['key_words']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    PortPhoneConf().update(port_no, phone_no, key_words)

    return b"success"


def delete(params):
    """刪除配置"""

    try:
        port_no = params['port_no']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    PortPhoneConf().delete(port_no)

    return b"success"


def sms(params):
    """接收短信,並轉發到釘釘羣"""

    try:
        phone_no = params['phone_no']
        msg = params['msg']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    current_phone_no = RedisUtil.comm_get(RedisUtil.CURRENT_PORT_KEY)
    if phone_no == current_phone_no:
        raise Exception(phone_no + "對應端口未開啓")

    SMSLog().redirect_to_ding_talk(msg)

    return b"success"


def set_current_conf(params):
    """設置當前開啓的端口"""
    try:
        phone_no = params['phone_no']
    except Exception:
        raise Exception('參數錯誤,請刷新頁面後重試')

    RedisUtil.comm_set(RedisUtil.CURRENT_PORT_KEY, phone_no)

    return b"success"

(4)上傳代碼

快成功了。。。

(5)測試

保存之後,選擇在線編輯,進行測試,nice

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章