本偏文章是 imouto project 的开发日志,想到哪里写到哪里,会比较凌乱
写在前面#
#俺は妹が大好きだあああああ!!!
Web 框架应该做什么#
- HTTP 数据包的解析与构建
- 控制反转 依赖注入
- 适配各种 driver
- 转义,单独拿出来说,XSS, SQL Injection, Shell Injection等都可以通过正确的转义来防止(若是 PHP,或许可以通过畸形数据(进制)来钻空子,所以最好进行预编译)
- 提供各种功能的抽象(类似 redirect, set_cookie)
- 异常处理与 logging
- 模板渲染,可以通过插件的方式载入,不是重点任务
- 易于与中间件进行联动,或许不是很重要
- PEP 333/ PEP 3333
基本流程#
应用服务器 -> 应用主体 -> 路由匹配 -> 用户逻辑代码 -> 响应/模板渲染
以 Tornado 为例
首先是 Application
的 __init__
注册了所有的路由(通过调用 add_handlers
方法)。当有请求来时触发 Application
的 __call__
方法
def __call__(self, request):
# Legacy HTTPServer interface
dispatcher = _RequestDispatcher(self, None)
dispatcher.set_request(request)
return dispatcher.execute()
其中 _RequestDispatcher
类用来解耦 Application
和 路由分发。 app
自身被注入进去,所以能够获得所有经过注册的路由。set_request
中调用了 _find_handler
进行了路由匹配;execute
实例化了对应路由的 RequestHandler。
通过 RequestHandler
的 __init__
方法,app
对象和 request
对象(包含请求信息)被注入。__init__
方法会调用 initialize
方法,即用户自定义的初始化方法。RequestHandler
创建完成后,其 _execute
方法被调用。这里会做一些 XSRF 检查之类的工作,然后调用 prepare()
进行准备处理。最后便是根据请求方法来调用 get/post
等方法了
method = getattr(self, self.request.method.lower())
result = method(*self.path_args, **self.path_kwargs)
MMP 的兼容性#
兼容性要考虑 unicode 和 bytes 的类型统一,lib 的更改(如 thread
) win 和 linux等
这点可以参考 Bottle 源码,已经封装成了工具函数
当然是选择放弃 2.x 了
一些辅助工具#
test_client 这个很重要,如果你不想一次次运行然后打开浏览器或者使用 curl 之类的人工判断是否合理的话
HTTP Header 是一个一对多值的映射,需要一个辅助数据结构 MultiDcit 可以参考 Bottle 源代码
autoload 模式,可以方便调试代码,当用户修改代码时能够监测到,并重启 server,可以参见蠢作者之前的文章,或者 Tornado 的源码(分离成单独的一个模块)
secure_cookie 经过签名的 cookie 可以参见蠢作者之间的文章
注意事项#
做之前请充分的考虑,虽然避免不了写的时候也许会灵光一闪,然后推了重造
bytesting 没有 format。或许统一使用 C 风格的格式化比较好呢?
思考问题的本源,有可能会出现发现 bug 然后引入解决方法,然后又遇到 bug,又去尝试解决。这时应该思考问题的本质是什么,不能去缝缝补补
EAFP 和 LBYL
# LBYL
def tob(s, enc='utf8'):
"""convert to bytes
"""
- return s.encode(enc) if isinstance(s, str) else bytes(s)
# EAFP
def tob(s, enc='utf8'):
"""convert to bytes
"""
try:
return s.encode(enc)
except AttributeError:
return str(s).encode(enc)
在 Python 中推荐使用 EAFP,重点放在对象的行为上而不是对象的类型上。只要实现对应的 encode 方法便可以进行 tob
OK, 上面代码 EAFP 的示例代码是有瑕疵的,因为他没有实现幂等
In [11]: tob('test')
Out[11]: b'test'
In [12]: tob(tob('test'))
Out[12]: b"b'test'"
为什么要实现幂等呢?因为 tob 的存在意义是:给予任何一个数据类型,我想让他返回 bytes 表示。那么已经是 bytes 的应当直接返回才对(这段代码来自于 sanic)
应当设计成什么形态,感觉这个事情不好描述,直接上个例子好了。比如是选择支持用户代码中多次进行数据的 write,然后框架给一个 chunk 来收集(Tornado 风格);还是让用户自己去组合这些数据,之后返回一个 HTTPResponse 实例(Sanic 风格)。两种那种比较好?可能会遇到很多这样的问题,能达到同样目的的设计有很多很多,应当选择那种。以此为例,平常的 Web 开发一般是 1) API 2) MVC。在 API 中显然是由用户构建数据然后 json 后返回。在 MVC 中也是构建好数据然后去渲染模板。所以 Tornado 风格显然做了多余的事情,但是这种风格易于 set_cookie
set_header
等操作,不必像这样冗长 return HttpResponse(header=header, cookie=cookie)
注重优化,比如
def _find_handler(self, path: str):
"""Find the corresponding handler for the path
if nothing mathed but having default handler, use default
otherwise 404 Not Found
"""
for route, handler_class in self._handlers:
match = route.match(path)
if match:
return handler_class, match.groupdict()
if self.settings.get('default_handler'):
handler_class = self.settings['default_handler']
return handler_class, {}
# attenton !!! TODO
return None, None
_find_handler
每次请求都会执行,用于路由匹配。当未匹配到任何路由时,返回 default_handler
。其用来自定义 404 页面。显然上面的代码不够好,可以修改为
def __init__(self, request, resposne, settings):
self.default_handler = settings.get('default_handler', None)
def _find_handler(self, path: str):
"""Find the corresponding handler for the path
if nothing mathed but having default handler, use default
otherwise 404 Not Found
"""
for route, handler_class in self._handlers:
match = route.match(path)
if match:
return handler_class, match.groupdict()
return self.default_handler, None
持续集成#
imouto 的构造#
大致画了一下,应该是这样的
原型设计图,后面应该会改
Tips#
Tips 有可能又解,有可能无解
#####Cookie 过期
# Tornado
def clear_cookie(self, name, path="/", domain=None):
"""Deletes the cookie with the given name.
Due to limitations of the cookie protocol, you must pass the same
path and domain to clear a cookie as were used when that cookie
was set (but there is no way to find out on the server side
which values were used for a given cookie).
"""
expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
self.set_cookie(name, value="", path=path, expires=expires,
domain=domain)
# Bottle
def delete_cookie(self, key, **kwargs):
''' Delete a cookie. Be sure to use the same `domain` and `path`
settings as used to create the cookie. '''
kwargs['max_age'] = -1
kwargs['expires'] = 0
self.set_cookie(key, '', **kwargs)
使 Cookie 过期可以通过 expired
或者 max-age
来控制。推荐使用 max-age
,expired
在 HTTP 1.1 开启弃用。新浏览器一定支持 max-age
,旧浏览器不一定支持。所以为了兼容 Bottle 采取了同时设置这两个域
HTTP 状态码 3xx#
301 Move Permanently 302 Found 问题来了,在 HTTP 1.0 中规定,如果不是一个 GET 请求或者 HEAD 请求,浏览器是禁止自动重定向的,除非得到用户的确认。但是这个浏览器并没有遵守,允许了 POST -> 302 -> GET 的重定向。所以在 HTTP 1.1 中加入了 303 303 See Other POST 可以直接重定向 304 Not Modified 305 Use Proxy 会在 Location 域中给出代理 306 此状态码被弃用 307 Temporary Redirect 重定向是请求方法和请求体不会改变,就是说 POST -> 307 -> POST
更详细的请参照 MDN
迭代一个 OrderDict 比 list 慢很多#
WSGI 还原 url#
python from urllib import quote
url = environ['wsgi.url_scheme']+'://'
if environ.get('HTTP_HOST'):
url += environ['HTTP_HOST']
else:
url += environ['SERVER_NAME']
if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
url += ':' + environ['SERVER_PORT']
else:
if environ['SERVER_PORT'] != '80':
url += ':' + environ['SERVER_PORT']
url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
url += '?' + environ['QUERY_STRING']
压力测试#
wrk#
需要 LuaJIT
git clone https://github.com/wg/wrk.git
cd wrk
make
sudo cp wrk /usr/local/bin
imouto 目前的测试,性能很不理想,如果使用 uvloop
效果会好一些(测试于 2017.7.12)
➜ ~ wrk -t12 -c400 -d30s http://127.0.0.1:8080
Running 30s test @ http://127.0.0.1:8080
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 306.72ms 93.61ms 1.82s 61.95%
Req/Sec 53.61 42.64 323.00 69.78%
17351 requests in 30.04s, 1.24MB read
Socket errors: connect 0, read 17350, write 0, timeout 0
Requests/sec: 577.58
Transfer/sec: 42.30KB
Tornado 测试结果(未使用 async/await)
Running 30s test @ http://127.0.0.1:8888/
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 214.20ms 34.26ms 305.17ms 86.52%
Req/Sec 149.42 108.79 595.00 54.22%
53007 requests in 30.04s, 9.10MB read
Requests/sec: 1764.54
Transfer/sec: 310.17KB
flask 测试
Running 30s test @ http://127.0.0.1:9001
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 97.22ms 107.17ms 1.81s 94.54%
Req/Sec 134.64 140.48 515.00 77.59%
19664 requests in 30.04s, 3.09MB read
Socket errors: connect 0, read 169, write 0, timeout 531
Requests/sec: 654.54
Transfer/sec: 105.47KB
感觉这个结果需要研究一下