tcpから、Pythonでwebフレーム1を書きます.

44907 ワード

https://blog.51cto.com/artcommend/66
Webフレームワークを書いてみたいのは、Django、Flask、Sanic、tornadoなどのWebフレームワークが香ばしくないからではなく、ホイールを作ってみるとフレームワークに対する認識が深まり、認識が深まるために第三者ライブラリに依存するべきではない(内蔵ライブラリのみ使用).
Webフレームワークを書く文章の多くは、wsgiインタフェースに基づいてWebフレームワークを実現するなど、アプリケーション層の実現に専念しています.もちろん問題ありませんが、requestがどのように来たのか分かりませんが、httpリクエストを解析するのはあまり面白い内容ではありません.
本文は主にtcp伝送から始め,tcp伝送,httpプロトコルの解析,ルーティング解析,フレームワークの実現を順に紹介する.テンプレートエンジンも実装されません.これは単独で文章を話すことができるからです.
フレームワークの実装は、単一スレッド、マルチスレッド、非同期IOの3つの段階に分けられます.
最終的な目標はflask,sanicのようなフレームワークを使用することです.
httpの内容が多いため,本稿ではhttpプロトコルのすべての内容を実現することは当然ではない.
記事ディレクトリの構造は次のとおりです.
  • TCP伝送
  • HTTP解析
  • ルーティング
  • WEBフレーム
  • 環境の説明
    Python:3.6.8サードパーティ製ライブラリに依存しない
    このバージョン以上であれば可能です
    HTTPプロトコル
    HTTPは最も広く利用されているアプリケーション層プロトコルの1つではないはずです.
    HTTPプロトコルは一般的に2つの部分に分けられ、クライアント、サービス側である.クライアントは一般的にブラウザを指します.クライアントはHTTP要求をサービス側に送信し、サービス側はクライアントの要求に応じて応答する.
    では、これらのリクエストと応答は何でしょうか.以下、tcpレベルでhttpリクエストおよび応答をシミュレートする.
    TCP転送
    HTTPはアプリケーション層のプロトコルであり、プロトコルとは当然、最初の行の内容をどのように書くべきか、どのように内容のフォーマットを組織するかなどの約束である.
    TCPは、これらのコンテンツを伝送層として担持する伝送タスクとして、httpライブラリを一切使用せずに、tcpでhttpリクエストをシミュレートしたり、httpリクエストを送信したりすることができるのは当然である.伝送とは送信(send)受信(recv)にほかならない.
    #socket_http_client.py
    
    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    CRLF = b"\r
    "
    req = b"GET / HTTP/1.1" + (CRLF * 3) client.connect(("www.baidu.com", 80)) client.send(req) resp = b"" while True: data = client.recv(1024) if data: resp += data else: break client.close() # 1024 bytes print(resp[:1024]) # 1024 print() print(resp.decode("utf8")[:1024])

    出力は次のとおりです.
    b'HTTP/1.1 200 OK\r
    Accept-Ranges: bytes\r
    Cache-Control: no-cache\r
    Connection: keep-alive\r
    Content-Length: 14615\r
    Content-Type: text/html\r
    Date: Wed, 10 Jun 2020 10:14:37 GMT\r
    P3p: CP=" OTI DSP COR IVA OUR IND COM "\r
    P3p: CP=" OTI DSP COR IVA OUR IND COM "\r
    Pragma: no-cache\r
    Server: BWS/1.1\r
    Set-Cookie: BAIDUID=32C6E7B012F4DBAAB40756844698B7DF:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r
    Set-Cookie: BIDUPSID=32C6E7B012F4DBAAB40756844698B7DF; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r
    Set-Cookie: PSTM=1591784077; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r
    Set-Cookie: BAIDUID=32C6E7B012F4DBAA3C9883ABA2DD201E:FG=1; max-age=31536000; expires=Thu, 10-Jun-21 10:14:37 GMT; domain=.baidu.com; path=/; version=1; comment=bd\r
    Traceid: 159178407703725358186803341565479700940\r
    Vary: Accept-Encoding\r
    X-Ua-Compatible: IE=Edge,chrome=1\r
    \r
    --STATUS OK-->\r
    \r
    \r
    \t HTTP/1.1 200 OK Accept-Ranges: bytes Cache-Control: no-cache Connection: keep-alive Content-Length: 14615 Content-Type: text/html Date: Wed, 10 Jun 2020 10:14:37 GMT P3p: CP=" OTI DSP COR IVA OUR IND COM " P3p: CP=" OTI DSP COR IVA OUR IND COM " Pragma: no-cache Server: BWS/1.1 Set-Cookie: BAIDUID=32C6E7B012F4DBAAB40756844698B7DF:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com Set-Cookie: BIDUPSID=32C6E7B012F4DBAAB40756844698B7DF; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com Set-Cookie: PSTM=1591784077; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com Set-Cookie: BAIDUID=32C6E7B012F4DBAA3C9883ABA2DD201E:FG=1; max-age=31536000; expires=Thu, 10-Jun-21 10:14:37 GMT; domain=.baidu.com; path=/; version=1; comment=bd Traceid: 159178407703725358186803341565479700940 Vary: Accept-Encoding X-Ua-Compatible: IE=Edge,chrome=1 --STATUS OK--> <head> http-equi

    tcpでhttpのクライアントからのリクエストが完了する以上、サービス側の実装が完了するのは当然ではないでしょうか.
    #socket_http_server.py
    
    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #   socket    ,    socket    ,              
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    CRLF = b"\r
    "
    host = "127.0.0.1" port = 6666 server.bind((host, port)) server.listen() print(" : http://{}:{}".format(host, port)) resp = b"HTTP/1.1 200 OK" + (CRLF * 2) + b"Hello world" while True: peer, addr = server.accept() print(" : {}".format(str(addr))) data = peer.recv(1024) print(" :") print(" ") print(data) print() print(" ") print(data.decode("utf8")) peer.send(resp) peer.close() # windows ctrl+c , break

    起動後、requestsでテストできます.
    In [1]: import requests
    In [2]: resp = requests.get("http://127.0.0.1:6666")
    In [3]: resp.ok
    Out[3]: True
    In [4]: resp.text
    Out[4]: 'Hello world'

    その後、サービス側はいくつかの情報を出力して終了します.
          :
           
    b'GET / HTTP/1.1\r
    Host: 127.0.0.1:6666\r
    User-Agent: python-requests/2.18.4\r
    Accept-Encoding: gzip, deflate\r
    Accept: */*\r
    Connection: keep-alive\r
    \r
    ' GET / HTTP/1.1 Host: 127.0.0.1:6666 User-Agent: python-requests/2.18.4 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive

    ここでこつこつとbytesもstrタイプのデータも出力し、主にその中のrに気づくためには、この2つの不可視文字が重要です.
    誰が見えない文字が見えないと言って、私はバイトコードフォーマットのデータフォーマットのデータの中で見ませんでしたか?これはおもしろい問題ですね.
    これで、http(ハイパーテキスト転送プロトコル)は、その名前のように、クライアント側がどのようなフォーマットのテキストを使用してリクエストを送信すべきか、サービス側がどのようなフォーマットのテキスト応答リクエストを使用すべきかを知っています.
    httpクライアント、サービス側のシミュレーションが完了しました.ここでは、サービス側の応答内容をさらにカプセル化し、Responseクラスを抽象化することができます.
    クライアントのRequestクラスも抽象化しないのはなぜですか?本文はwebサービス側のフレームワークを書くつもりなので:).
    # response.py
    
    from collections import namedtuple
    
    RESP_STATUS = namedtuple("RESP_STATUS", ["code", "phrase"])
    CRLF = "\r
    "
    status_ok = RESP_STATUS(200, "ok") status_bad_request = RESP_STATUS(400, "Bad Request") statue_server_error = RESP_STATUS(500, "Internal Server Error") default_header = {"Server": "youerning", "Content-Type": "text/html"} class Response(object): http_version = "HTTP/1.1" def __init__(self, resp_status=status_ok, headers=None, body=None): self.resp_status = resp_status if not headers: headers = default_header if not body: body = "hello world" self.headers = headers self.body = body def to_bytes(self): status_line = "{} {} {}".format(self.http_version, self.resp_status.code, self.resp_status.phrase) header_lines = ["{}: {}".format(k, v) for k,v in self.headers.items()] headers_text = CRLF.join(header_lines) if self.body: headers_text += CRLF message_body = self.body data = CRLF.join([status_line, headers_text, message_body]) return data.encode("utf8")

    だから前の応答はこのように書くことができます.
    # socket_http_server2.py
    
    import socket
    from response import Response
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #   socket    ,    socket    ,              
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    CRLF = b"\r
    "
    host = "127.0.0.1" port = 6666 server.bind((host, port)) server.listen() print(" : http://{}:{}".format(host, port)) resp = Response() while True: peer, addr = server.accept() print(" : {}".format(str(addr))) data = peer.recv(1024) print(" :") print(" ") print(data) print() print(" ") print(data.decode("utf8")) peer.send(resp.to_bytes()) peer.close() # windows ctrl+c , break

    最終的な結果は大きく異なり,唯一の違いは後者の応答にhttpヘッダ情報があることである.
    HTTPリクエスト(Request)およびレスポンス(Response)の具体的な定義については、次のリンクを参照してください.
    https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5
    https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6
    HTTP解析
    前述の内容は,HTTPインタラクションのシミュレーションが完了したものの,要求に応じて指定された応答を返す要求に達していないのは,クライアントから送信された要求を解析していないため,自然と要求の違いを判断するからである.
    以下に、一般的な2つのリクエストを示します.
    GETリクエスト
    # Bytes  
    b'GET / HTTP/1.1\r
    Host: 127.0.0.1:6666\r
    User-Agent: python-requests/2.18.4\r
    Accept-Encoding: gzip, deflate\r
    Accept: */*\r
    Connection: keep-alive\r
    \r
    ' # string GET / HTTP/1.1 Host: 127.0.0.1:6666 User-Agent: python-requests/2.18.4 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive

    POSTリクエスト
    # Bytes  
    b'POST / HTTP/1.1\r
    Host: 127.0.0.1:6666\r
    User-Agent: python-requests/2.18.4\r
    Accept-Encoding: gzip, deflate\r
    Accept: */*\r
    Connection: keep-alive\r
    Content-Length: 29\r
    Content-Type: application/x-www-form-urlencoded\r
    \r
    username=admin&password=admin' # string POST / HTTP/1.1 Host: 127.0.0.1:6666 User-Agent: python-requests/2.18.4 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive Content-Length: 29 Content-Type: application/x-www-form-urlencoded username=admin&password=admin

    ここでは、文字列が印刷されると表示されない文字をフォーマットします.例えば改行文字ですが、HTTPプロトコルが1行1行のデータであるのは、印刷時にフォーマットしたからです.この意識がなければ、Request Line(リクエストライン)、Request Header Fields(リクエストヘッダフィールド)、message-body(メッセージトピック)を特定できません.
    中国語と英語が混ざっているのは曖昧さを避けるためだ.
    クライアントから送信された情報を抽象化するために、すべてのリクエストのすべての情報を格納するRequestクラスを書きます.
    # request.py
    
    class Request(object):
        def __init__(self):
            self.method = None
            self.path = None
            self.raw_path = None
            self.query_params = {}
            self.path_params = {}
            self.headers = {}
            self.raw_body = None
            self.data = None

    では解析してみましょう
    # http_parser.py
    
    import re
    import json
    from urllib import parse
    from request import Request
    from http_exceptions import BadRequestException, InternalServerErrorException
    
    CRLF = b"\r
    "
    SEPARATOR = CRLF + CRLF HTTP_VERSION = b"1.1" REQUEST_LINE_REGEXP = re.compile(br"[a-z]+ [a-z0-9.?_\[\]=&-\\]+ http/%s" % HTTP_VERSION, flags=re.IGNORECASE) SUPPORTED_METHODS = {"GET", "POST"} def http_parse(buffer): print(type(buffer[:])) request = Request() def remove_buffer(buffer, stop_index): buffer = buffer[stop_index:] return buffer def parse_request_line(line): method, raw_path = line.split()[:2] method = method.upper() if method not in SUPPORTED_METHODS: raise BadRequestException("{} method noy supported".format(method)) request.method = method request.raw_path = raw_path # , /a/b/c?username=admin&password=admin # /a/b/c # ? # , /a/b/c?filter=name&filter=id # , {"filter": ["name", "id"]} url_obj = parse.urlparse(raw_path) path = url_obj.path query_params = parse.parse_qs(url_obj.query) request.path = path request.query_params = query_params def parse_headers(header_lines): # bytes , parse_request_line header_iter = (line for line in header_lines.split(CRLF.decode("utf8")) if line) headers = {} for line in header_iter: header, value = [i.strip() for i in line.strip().split(":")][:2] header = header.lower() headers[header] = value request.headers = headers def parse_body(body): # data = body_parser(raw_body) request.raw_body = raw_body request.data = data # request line if REQUEST_LINE_REGEXP.match(buffer): line = buffer.split(CRLF, maxsplit=1)[0].decode("utf8") parse_request_line(line) # request line , first_line_end = buffer.index(CRLF) # , \r
    # 2 , \r
    http header http header 。
    # del buffer[:first_line_end + 2] buffer = remove_buffer(buffer, first_line_end + 2) # \r
    \r
    http header
    if SEPARATOR in buffer: header_end = buffer.index(SEPARATOR) header_lines = buffer[:header_end].decode("utf8") parse_headers(header_lines) # # del buffer[:header_end + 4] buffer = remove_buffer(buffer, header_end + 4) headers = request.headers if headers and "content-length" in headers: # application/x-www-form-urlencoded application/json content-type # application/x-www-form-urlencoded , :username=admin&password=admin url query_params # application/json , json content_type = headers.get("content-type") # content_length = headers.get("content-length", "0") body_parser = parse.parse_qs if content_type == "application/json": # body_parser = json.loads # raw_body = buffer.decode("utf8") parse_body(raw_body) return request

    そしてテストして
    #      
    python socket_http_server3.py

    リクエストを使用してhttpリクエストを送信
    In [115]: requests.post("http://127.0.0.1:6666/test/path?asd=aas", data={"username": "admin", "password": "admin"})
    Out[115]: <Response [200]>

    サービス側の出力は次のとおりです.
          : ('127.0.0.1', 1853)
    <class 'bytes'>
          :
        : POST
        : /test/path
        : {'asd': ['aas']}
       : {'host': '127.0.0.1', 'user-agent': 'python-requests/2.18.4', 'accept-encoding': 'gzip, deflate', 'accept': '*/*', 'connection': 'keep-alive', 'content-length': '29', 'content-type': 'application/x-www-form-urlencoded'}
        : {'username': ['admin'], 'password': ['admin']}

    これでクライアントからのリクエストを解析することで、Requestオブジェクトが得られ、このRequestオブジェクトで必要なすべての情報を得ることができます.
    ルート
    ルーティング解析
    経験によれば,異なるウェブページパスが異なるコンテンツに対応し,パスの異なる応答によって異なるコンテンツに応答することを知っており,この部分をルーティング解析と呼ぶのが一般的である.
    したがって,要求が得られた後,クライアントがアクセスする経路に基づいてどのようなコンテンツを返すかを判断する必要があり,これらの対応関係を格納するオブジェクトをルーティングと呼ぶのが一般的である.
    ルーティングは少なくとも2つのインタフェースを提供し、1つはこのような対応関係を追加する方法であり、2つは経路に基づいて要求に応答可能な実行可能な関数を返すことであり、この関数は一般的にhandlerと呼ぶ.
    経路とは一般的に2種類あり、静的、動的である.
    スタティツクルーティング
    静的で簡単で,1つの辞書で解決でき,要求方法と経路を1つの二元グループとして辞書のkeyとし,対応する処理方法をvalueとすればよい.次のように
    # router1.py
    
    import re
    from collections import namedtuple
    from functools import partial
    
    def home():
        return "home"
    
    def info():
        return "info"
    
    def not_found():
        return "not found"
    
    class Router(object):
        def __init__(self):
            self._routes = {}
    
        def add(self, path, handler, methods=None):
            if methods is None:
                methods = ["GET"]
    
            if not isinstance(methods, list):
                raise Exception("methods      ")
    
            for method in methods:
                key = (method, path)
                if key in self._routes:
                    raise Exception("     : {}".format(path))
                self._routes[key] = handler
    
        def get_handler(self, method, path):
            method_path = (method, path)
            return self._routes.get(method_path, not_found)
    
    route = Router()
    route.add("/home", home)
    route.add("/info", info, methods=["GET", "POST"])
    print(route.get_handler("GET", "/home")())
    print(route.get_handler("POST", "/home")())
    print(route.get_handler("GET", "/info")())
    print(route.get_handler("POST", "/info")())
    print(route.get_handler("GET", "/xxxxxx")())

    実行結果は次のとおりです.
    home
    not found
    info
    info
    not found

    どうてきルーティング
    ダイナミックは少し複雑で、正規表現を使用する必要があります.しかし、簡単にするために、ここでは、/user/{id:int}のような動的パスタイプをフィルタするインタフェースは提供されません.
    コードは次のとおりです.
    # router2.py
    
    import re
    from collections import namedtuple
    from functools import partial
    
    Route = namedtuple("Route", ["methods", "pattern", "handler"])
    
    def home():
        return "home"
    
    def item(name):
        return name
    
    def not_found():
        return "not found"
    
    class Router(object):
        def __init__(self):
            self._routes = []
    
        @classmethod
        def build_route_regex(self, regexp_str):
            #           
            # 1. /home           ,   ^/home$        
            # 2. /item/{name}          ,       ^/item/(?P[a-zA-Z0-9_-]+)$    
            def named_groups(matchobj):
                return '(?P[a-zA-Z0-9_-]+)'.format(matchobj.group(1))
    
            re_str = re.sub(r'{([a-zA-Z0-9_-]+)}', named_groups, regexp_str)
            re_str = ''.join(('^', re_str, '$',))
            return re.compile(re_str)
    
        @classmethod
        def match_path(self, pattern, path):
            match = pattern.match(path)
            try:
                return match.groupdict()
            except AttributeError:
                return None
    
        def add(self, path, handler, methods=None):
            if methods is None:
                methods = {"GET"}
            else:
                methods = set(methods)
            pattern = self.__class__.build_route_regex(path)
            route = Route(methods, pattern, handler)
    
            if route in self._routes:
                raise Exception("     : {}".format(path))
            self._routes.append(route)
    
        def get_handler(self, method, path):
            for route in self._routes:
                if method in route.methods:
                    params = self.match_path(route.pattern, path)
    
                    if params is not None:
                        return partial(route.handler, **params)
    
            return not_found
    
    route = Router()
    route.add("/home", home)
    route.add("/item/{name}", item, methods=["GET", "POST"])
    print(route.get_handler("GET", "/home")())
    print(route.get_handler("POST", "/home")())
    print(route.get_handler("GET", "/item/item1")())
    print(route.get_handler("POST", "/item/item1")())
    print(route.get_handler("GET", "/xxxxxx")())

    実行結果は次のとおりです.
    home
    not found
    item1
    item1
    not found

    アクセラレータによるルーティングの追加
    ルーティングの追加について単独で述べるのは,明示的な呼び出しが派手ではない(甘さが足りない):).だからflaskのように装飾器(文法糖)でルートを追加するのは素晴らしい(甘い)選択肢です.
    # router3.py
    
    import re
    from collections import namedtuple
    from functools import partial
    from functools import wraps
    
    SUPPORTED_METHODS = {"GET", "POST"}
    Route = namedtuple("Route", ["methods", "pattern", "handler"])
    
    class View:
        pass
    
    class Router(object):
        def __init__(self):
            self._routes = []
    
        @classmethod
        def build_route_regex(self, regexp_str):
            #           
            # 1. /home           ,   ^/home$        
            # 2. /item/{name}          ,       ^/item/(?P[a-zA-Z0-9_-]+)$    
            def named_groups(matchobj):
                return '(?P[a-zA-Z0-9_-]+)'.format(matchobj.group(1))
    
            re_str = re.sub(r'{([a-zA-Z0-9_-]+)}', named_groups, regexp_str)
            re_str = ''.join(('^', re_str, '$',))
            return re.compile(re_str)
    
        @classmethod
        def match_path(self, pattern, path):
            match = pattern.match(path)
            try:
                return match.groupdict()
            except AttributeError:
                return None
    
        def add_route(self, path, handler, methods=None):
            if methods is None:
                methods = {"GET"}
            else:
                methods = set(methods)
            pattern = self.__class__.build_route_regex(path)
            route = Route(methods, pattern, handler)
    
            if route in self._routes:
                raise Exception("     : {}".format(path))
            self._routes.append(route)
    
        def get_handler(self, method, path):
            for route in self._routes:
                if method in route.methods:
                    params = self.match_path(route.pattern, path)
    
                    if params is not None:
                        return partial(route.handler, **params)
    
            return not_found
    
        def route(self, path, methods=None):
            def wrapper(handler):
                #                 ,        ,        
                nonlocal methods
                if callable(handler):
                    if methods is None:
                        methods = {"GET"}
                    else:
                        methods = set(methods)
                    self.add_route(path, handler, methods)
    
                return handler
            return wrapper
    
    route = Router()
    
    @route.route("/home")
    def home():
        return "home"
    
    @route.route("/item/{name}", methods=["GET", "POST"])
    def item(name):
        return name
    
    def not_found():
        return "not found"
    
    print(route.get_handler("GET", "/home")())
    print(route.get_handler("POST", "/home")())
    print(route.get_handler("GET", "/item/item1")())
    print(route.get_handler("POST", "/item/item1")())
    print(route.get_handler("GET", "/xxxxxx")())

    出力結果は以下の通りで、上に装飾器を使用していない場合と同じです.
    home
    not found
    item1
    item1
    not found

    これで、Webがサポートすべき大部分の作業を完了しました.では、次はこれらの部分を有機的に組み合わせる方法です.
    WEBフレームワーク
    単一スレッドまたはマルチスレッドバージョンrequestに関する処理はflaskに似ています.この2つのバージョンのrequestは、flaskのように必要に応じてインポートできますが、非同期バージョンはsanicを模倣しています.
    しかし、どのバージョンも、最も基本的なニーズを満たすことを追求しているだけで、多くのコア概念やコードが読みやすさを損なわないことを理解した上でできるだけ少ないコードを追求しているのは、「500 Lines or Less」の真似だ.
    続きます...
    ところで『500 Lines or Less』は素晴らしいプロジェクトで、強い安利です.
    ソースコード
    https://github.com/youerning/blog/tree/master/web_framework
    後続の文章を期待すれば、私の微信公衆番号(また耳ノート)、トップ番号(また耳ノート)、githubに注目することができます.
    リファレンスリンク
    https://www.w3.org/Protocols/rfc2616/rfc2616.html
    https://github.com/sirMackk/diy_framework
    https://github.com/hzlmn/diy-async-web-framework
    https://github.com/huge-success/sanic
    https://www.cnblogs.com/Zzbj/p/10207128.html