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)にほかならない.
出力は次のとおりです.
tcpでhttpのクライアントからのリクエストが完了する以上、サービス側の実装が完了するのは当然ではないでしょうか.
起動後、requestsでテストできます.
その後、サービス側はいくつかの情報を出力して終了します.
ここでこつこつとbytesもstrタイプのデータも出力し、主にその中のrに気づくためには、この2つの不可視文字が重要です.
誰が見えない文字が見えないと言って、私はバイトコードフォーマットのデータフォーマットのデータの中で見ませんでしたか?これはおもしろい問題ですね.
これで、http(ハイパーテキスト転送プロトコル)は、その名前のように、クライアント側がどのようなフォーマットのテキストを使用してリクエストを送信すべきか、サービス側がどのようなフォーマットのテキスト応答リクエストを使用すべきかを知っています.
httpクライアント、サービス側のシミュレーションが完了しました.ここでは、サービス側の応答内容をさらにカプセル化し、Responseクラスを抽象化することができます.
クライアントのRequestクラスも抽象化しないのはなぜですか?本文はwebサービス側のフレームワークを書くつもりなので:).
だから前の応答はこのように書くことができます.
最終的な結果は大きく異なり,唯一の違いは後者の応答に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リクエスト
POSTリクエスト
ここでは、文字列が印刷されると表示されない文字をフォーマットします.例えば改行文字ですが、HTTPプロトコルが1行1行のデータであるのは、印刷時にフォーマットしたからです.この意識がなければ、Request Line(リクエストライン)、Request Header Fields(リクエストヘッダフィールド)、message-body(メッセージトピック)を特定できません.
中国語と英語が混ざっているのは曖昧さを避けるためだ.
クライアントから送信された情報を抽象化するために、すべてのリクエストのすべての情報を格納するRequestクラスを書きます.
では解析してみましょう
そしてテストして
リクエストを使用してhttpリクエストを送信
サービス側の出力は次のとおりです.
これでクライアントからのリクエストを解析することで、Requestオブジェクトが得られ、このRequestオブジェクトで必要なすべての情報を得ることができます.
ルート
ルーティング解析
経験によれば,異なるウェブページパスが異なるコンテンツに対応し,パスの異なる応答によって異なるコンテンツに応答することを知っており,この部分をルーティング解析と呼ぶのが一般的である.
したがって,要求が得られた後,クライアントがアクセスする経路に基づいてどのようなコンテンツを返すかを判断する必要があり,これらの対応関係を格納するオブジェクトをルーティングと呼ぶのが一般的である.
ルーティングは少なくとも2つのインタフェースを提供し、1つはこのような対応関係を追加する方法であり、2つは経路に基づいて要求に応答可能な実行可能な関数を返すことであり、この関数は一般的にhandlerと呼ぶ.
経路とは一般的に2種類あり、静的、動的である.
スタティツクルーティング
静的で簡単で,1つの辞書で解決でき,要求方法と経路を1つの二元グループとして辞書のkeyとし,対応する処理方法をvalueとすればよい.次のように
実行結果は次のとおりです.
どうてきルーティング
ダイナミックは少し複雑で、正規表現を使用する必要があります.しかし、簡単にするために、ここでは、/user/{id:int}のような動的パスタイプをフィルタするインタフェースは提供されません.
コードは次のとおりです.
実行結果は次のとおりです.
アクセラレータによるルーティングの追加
ルーティングの追加について単独で述べるのは,明示的な呼び出しが派手ではない(甘さが足りない):).だからflaskのように装飾器(文法糖)でルートを追加するのは素晴らしい(甘い)選択肢です.
出力結果は以下の通りで、上に装飾器を使用していない場合と同じです.
これで、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
Webフレームワークを書いてみたいのは、Django、Flask、Sanic、tornadoなどのWebフレームワークが香ばしくないからではなく、ホイールを作ってみるとフレームワークに対する認識が深まり、認識が深まるために第三者ライブラリに依存するべきではない(内蔵ライブラリのみ使用).
Webフレームワークを書く文章の多くは、wsgiインタフェースに基づいてWebフレームワークを実現するなど、アプリケーション層の実現に専念しています.もちろん問題ありませんが、requestがどのように来たのか分かりませんが、httpリクエストを解析するのはあまり面白い内容ではありません.
本文は主にtcp伝送から始め,tcp伝送,httpプロトコルの解析,ルーティング解析,フレームワークの実現を順に紹介する.テンプレートエンジンも実装されません.これは単独で文章を話すことができるからです.
フレームワークの実装は、単一スレッド、マルチスレッド、非同期IOの3つの段階に分けられます.
最終的な目標はflask,sanicのようなフレームワークを使用することです.
httpの内容が多いため,本稿ではhttpプロトコルのすべての内容を実現することは当然ではない.
記事ディレクトリの構造は次のとおりです.
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