基于asyncio的web框架aiohttp

本来想扯一通标准库里添加了asyncio的意义, 什么统一了异步框架, 什么方便代码前移之类的了. 然后发现原来大家也都是用装饰器+生成器来写的, 好像也没啥区别…

迁移的主要阻力也不是各个框架实现异步的方式不同, 而是用到了框架的某些特性, 在其他框架里可能没有, asyncio成为标准库也改变不了这一点.

不过python3.4 3.5 3.6添加了很多新功能,语言层面的异步支持越来越好了

在3.4 就是上面说的, 引入了asyncio的标准库.

3.5有了一系列的bug fix ,可以见why-is-python-3-5-3-the-lowest-supported-version, 还支持了async/await语法.

不过3.5的时候async/await还不是关键字, 还可以给async赋值, 所以到了python3.7的时候挂了一堆库, 因为他们用了async当变量…

import asyncio
import sys

print(sys.version_info) # 3.5.4


async def hello():
print("Hello world!")
# 异步调用asyncio.sleep(1):
r = await asyncio.sleep(1)
print("Hello again!")


async = 1
print(async)

# 获取EventLoop:
loop = asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()

会输出

1
Hello world!
Hello again!

但是如果到了python3.7, 会报错SyntaxError, 也因为这个原因挂了一堆库.

  File "app.py", line 14
async = 1
^
SyntaxError: invalid syntax

比如说twisted, python3.7.0是6月27号发布的, 但是直到到前两天(2018-10-05), pypi上的最新版本还不能正常运行…

说了半天没用的…

最近在折腾的python框架aiohttp其实没有这个问题. 因为aiohttp3.x.x的最低版本要求就是3.5.4, 从一开始就用到了async/await, 自然也不会有某个开发者把async再当作函数参数或者变量来赋值.

出于好奇, 还是了解了一下aiohttp这个框架, 写了几个小玩具.

from aiohttp import web
import asyncio

route = web.RouteTableDef()


async def index(request):
await asyncio.sleep(1)
return web.Response(text='hello world')


@route.get('/about')
async def about(request):
return web.Response(text=request.app.version + ' author Trim21<trim21me@gmail.com>')


@web.middleware
async def middleware(request, handler):
# before handle request
resp = await handler(request)
return resp


def create_app():
app = web.Application(middlewares=[middleware, ])
app.version = '0.0.1'
app.add_routes([
web.get('/', index, name='index'),
])
app.add_routes(route)
return app


web.run_app(create_app())

一个简单的例子, (如果需要数据库的话, 官方的例子是把mongo的连接池绑在了app.mongo上.)

前面提到了, 语言层面的异步支持是越来越好了, 但是类库的支持还是有些匮乏.

mongodb和redis的支持还算可以, mongodb的官方自己写了motor, aiohttp的开发者写了aioredis.

但是如果想找一个异步的关系型数据库的ORM就非常难了. SQLAlchemy的作者曾经写过一篇文章, 说因为python本身就很慢, 所以异步也没有意义, 反而比同步还要慢.

但我还是相信python, 会有那么一天变快的(

异步的SQL ORM主要有这么几个

  1. peewee-async
  2. gino

gino直接pass, 因为目前只支持postgreSQL.

peewee-async的问题是, 他其实是基于peewee的, 你要通过peewee来定义你的模块, 然后用peewee-async给的一个Manager来异步调用.

举个例子, 他的异步代码是长这样的

import peewee
from peewee_async import Manager, PostgresqlDatabase


class User(peewee.Model):
username = peewee.CharField(max_length=40, unique=True)


class Twitter(peewee.Model):
user = peewee.ForeignKeyField(model=User, backref='tweets')


objects = Manager(PostgresqlDatabase('test'))
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call


async def my_async_func():
user0 = await objects.create(User, username='test')
user1 = await objects.get(User, id=user0.id)
user2 = await objects.get(User, username='test')
# All should be the same
print(user0.id, user1.id, user2.id)
print(user0.tweets) # raise exception here

像这个例子比较简单, 就只有一个外键, 还添加了backref, 就容易出问题了.

因为本身是异步框架, 所以同步的代码都会阻塞整个事件循环, 在使用orm的时候会先设置禁止同步链接数据库, 只允许异步链接. 但是如果用了外键, peewee在你试图获取对应的属性的时候就会链接数据库, 取回对应的数据. 所以如果用了外键, 在查询和使用实例的时候总要小心翼翼的, 以避免触发同步查询.

也许不用外键, 直接存id进去, 不依赖数据库的约束, 而在web层面约束可能好一些, 不会出现类似的问题.

ORM扯完了, 几个经常会用到的东西.

模板: aiohttp-jinja2

session: aiohttp-session

辅助的开发服务器, 支持livereload和hotreloadaiohttp-devtools

devtools有一些坑, 主要是项目的readme不是很全, 主要还是要靠cli的help信息和源码…

比如, 我的项目结构是这样的

project
├─ Dockerfile
├─ README.md
├─ requirements.txt
└──app
├─ main.py
├─static
│ └─css
│ └── 1.css
└──templates
└── index.html

这里有一个潜在的坑, 如果你要在project目录下直接启动服务器的话, 是不能提供app-path的, 而是用通过--root来启动, 比如说adev runserver --root app

但是如果你的pwdapp, 此时不需要提供root-path, 只需要提供app-apth, 启动命令变为adev runserver main.py

贴一下runserver的help信息

Usage: adev runserver [OPTIONS] [APP_PATH]

Run a development server for an aiohttp apps.

Takes one argument "app-path" which should be a path to either a directory
containing a recognized default file ("app.py" or "main.py") or to a
specific file. Defaults to the environment variable "AIO_APP_PATH" or ".".

The app path is run directly, see the "--app-factory" option for details
on how an app is loaded from a python module.

Options:
-s, --static DIRECTORY Path of static files to serve, if excluded
static files aren't served. env variable:
AIO_STATIC_STATIC
--root DIRECTORY Root directory project used to qualify other
paths. env variable: AIO_ROOT
--static-url TEXT URL path to serve static files from, default
"/static/". env variable: AIO_STATIC_URL
--livereload / --no-livereload Whether to inject livereload.js into html
page footers to autoreload on changes. env
variable AIO_LIVERELOAD
--host TEXT host used when referencing livereload and
static files, if blank host is taken from
the request header with default of
localhost. env variable AIO_HOST
--debug-toolbar / --no-debug-toolbar
Whether to enable debug toolbar. env
variable: AIO_DEBUG_TOOLBAR
--app-factory TEXT name of the app factory to create an
aiohttp.web.Application with, if missing
default app-factory names are tried. This
can be either a function with signature "def
create_app(loop): -> Application" or "def
create_app(): -> Application" or just an
instance of aiohttp.Application. env
variable AIO_APP_FACTORY
-p, --port INTEGER Port to serve app from, default 8000. env
variable: AIO_PORT
--aux-port INTEGER Port to serve auxiliary app (reload and
static) on, default port + 1. env variable:
AIO_AUX_PORT
-v, --verbose Enable verbose output.
--help Show this message and exit.

其中, -s, --static因为实际上静态文件要通过--aux-port去访问, 所以感觉有些鸡肋.

也就是说, 如果正常的服务器启动在6001端口, 而aux-server启动在6002端口, 我们要使用这个参数代理的静态文件的话, 要访问http://localhost:6002/static/1.css, 而正常我们会把静态文件放在同一个域名下, 也就是http://localhost:6001/static/1.css, 所以我的选择是直接添加一个web.static的路由, 而不是使用的这个功能.