建立HTTP服务

aiohttp功能库的另一个重要功能就是建立HTTP服务。使用aiohttp功能库建立HTTP服务一般要经历三个步骤:

  1. 定义相关的请求处理方法。
  2. 定义路由来将URL与处理方法绑定。
  3. 启动HTTP应用。

以下示例演示了一个最简答的HTTP服务的搭建。

from aiohttp import web

async def hello(request):
	return web.Response(text='Hello world')

app = web.Application()
app.add_routes([web.get('/', hello)])
web.run_app(app)

或者还可以使用修饰器来定义路由:

from aiohttp impoer web

routes = web.RouteTableDef()

@routes.get('/')
async def hello(request):
	return web.Response(text='Hello world')

app = web.Application()
app.add_routes(routes)
web.run_app(app)

请求处理

在Web应用中,最核心的内容是请求处理函数的编写。请求处理包含两部分内容,一是编写处理函数,二是将处理函数与要响应的URL以及请求方法绑定。

编写处理函数比较简单,需要接收一个Request类实例,并且返回一个StreamResponse或其子类的实例即可,但是由于aiohttp是异步的,所以需要使用async/await关键字来标注。

async def handler(request):
	return await web.Response()

在编写处理函数之后,就是将处理函数与要响应的URL以及请求方法绑定。URL以及请求方法可以形成一个组合,相同的URL但不同的请求方法可以绑定不同的处理函数。这种绑定有两种实现方法,第一种是使用web模块提供的HTTP谓词方法来形成路由实例,其次则是前面示例中出现的路由修饰器。路由修饰器也是使用HTTP谓词来进行路由的绑定。

默认情况下绑定到GET的处理函数将可以处理HEAD请求,如果不允许这种请求,可以使用web.get(url, hanler, allow_head=False)来关闭这项特性。

请求处理的函数不仅可以独立放置在包里,还可以使用类来组织。aiohttp不关心任何处理函数的实现细节。如果使用类来组织请求处理,可以参考以下示例。

class Handler:
	def __init__(self):
		pass
	
	async def handle_greeting(self, request):
		name = request.match_info.get('name', 'Anonymous')
		test = "Hello, {}".format(name)
		return web.Response(text=test)

handler = Handler()
app.add_routes([web.get('/greet/{name}', handler.handle_greeting)])

使用类来组织处理函数,还有一种方法,就是继承web.View。继承web.View的类可以直接定义HTTP谓词方法,并将类整个绑定在路由上,来响应指定路由的不同谓词访问。例如:

@routes.view('path/to')
class MyView(web.View):
	async def get(self):
		return await get_resp(self.request)
	
	async def post(self):
		return await post_resp(self.request)
		
# 如果不使用修饰器,则可以使用以下语句进行绑定
web.view('/path/to', MyView)

路由参数传递

在Restful API的设计理念中,追求URL的语义化表达方式,这就使要传递给API的参数混入了URL中,而不是像传统API使用Query参数来传递。这样往往会出现/a/123/b/c格式的URL,其中的123就是要传递给API的参数。

解析Restful URL中的参数是一个支持Restful设计理念的框架的基本能力。aiohttp可以在路由映射中定义参数位置,并在传入处理函数的Request对象中提供了.match_info()方法来访问这些参数。例如:

@routes.get('/a/{name}')
async def hello_handler(request):
	return web.Response(text="Hello {}".format(request.match_info['name']))

此外,路由参数可以使用正则表达式来限定接收什么格式的参数,格式为{标识符:正则表达式}

命名路由

路由在定义的时候可以附加一个name属性,用来为其命名。命名的路由可以基由request.app.router对象访问。例如:

@routes.get('/root', name='root')
async def handler(request)
	pass

async def other_handler(request)
	url = request.app.router['root']

返回JSON响应

以上几节的示例中,返回的都是文本类型的响应。但是在实际项目中,常常会返回JSON类型、XML类型、HTML类型等多种类型的响应。

其中最常用的是返回JSON类型的数据,要返回JSON类型的数据,可以使用web.json_response(data)。这个函数接受一个字典类型的参数,可以将其编码成一个JSON字符串返回给客户端。

Session控制

aiohttp功能库并不带任何Session功能支持,其Session功能支持是由aiohttp_session这个第三方中间件支持的。具体使用可参考以下示例。

import asyncio
import time
import base64
from cryptography import fernet
from aiohttp import web
from aiohttp_session import setup, get_session, session_middleware
from aiohttp_session.cookie_storage import EncryptedCookiesStorage

async def handler(request):
	session = await get_session(request)
	last_visit = session['last_visit'] if 'last_visit' in session else None
	text = 'last visited: {}'.format(last_visit)
	return web.Response(text=text)

async def make_app():
	app = web.Application()
	fernet_key = fernet.Fernet.generate_key()
	secret_key = base64.urlsafe_b64decode(fernet_key)
	setup(app, EncryptedCookieStorage(secret_key))
	app.add_routes([web.get('/', handler)])
	return app

web.run_app(make_app())

Warning

这里的last_visit = session['last_visit'] if 'last_visit' in session else None,也是一个Pythonic用法,可以用来在目标值不存在或者不满足条件时返回一个默认值,也可以当做其他语言中的三元操作符来使用。

表单及文件上传

使用GET的Query参数接收数据是存在限制的,大量数据传输一般都是由表单完成的。从POST请求中获取表单数据并不是一件十分困难的事情。Request类型实例可以使用.post()方法解析表单数据,它可以接受并解析application/x-www-form-urlencodedmultipart/form-data类型的表单数据。表单数据可以像以下示例中所示解析表单数据。

async def do_login(request):
	data = await request.post()
	login = data['login']
	password = data['password']

Request.post()也可以完成文件上传功能,但是不建议处理较大的文件,因为.post()方法是将表单内的所有内容都读入内存,容易产生Out of memory错误。所以以下给出两种文件上传的处理。

# 直接使用.post()进行解析的示例
async def store_file(request):
	data = await request.post()
	file = data['upfile']
	filename = file.filename
	file_handle = data['upfile'].file
	content = file_handle.read()
	return web.Response(body=content, header=MultiDict({'CONTENT-DISPOSITION': file_handle))
# 推荐用于解析文件的示例
async def store_file(request):
	reader = await request.multipart()
	field = await reader.next()
	name = await field.read(decode=True)
	
	field = await reader.next
	filename = field.filename
	size = 0
	with open(os.path.join(path, filename), 'wb') as f
		while True:
			chunk = await field.read_chunk()
			if not chunk:
				break
			size += len(chunk)
			f.write(chunk)
	return web.Response(text="{} size of {} ".format(filename, size))

URL重定向以及HTTP响应状态

重定向是采用aiohttp.web.HTTPFound(url)来完成的。对于要返回不同的HTTP响应码,aiohttp针对不同的状态码提供了不同的异常类。以下给出一个常用的简表。

状态码对应方法
200HTTPOk
201HTTPCreated
202HTTPAccepted
204HTTPNoContent
205HTTPResetContent
301HTTPMovedPermanently
302HTTPFound
304HTTPNotModified
305HTTPUseProxy
307HTTPTemporaryRedirect
308HTTPPermanentRedirect
400HTTPBadRequest
401HTTPUnauthorized
403HTTPForbidden
404HTTPNotFound
405HTTPMethodNotAllowed
406HTTPNotAcceptable
408HTTPRequestTimeout
500HTTPInternelServerError
501HTTPNotImplemented
502HTTPBadGateway
503HTTPServiceUnavailable
504HTTPGatewayTimeout

所有的响应异常都有相同的初始化格式,以HTTPNotFound为例,aiohttp.web.HTTPNotFound(*, headers=None, reason=None, body=None, text=None, content_type=None)。其中301、302、305、307采用以下格式aiohttp.web.HTTPFound(location, *, headers=None, reason=None, body=None, text=None, content_type=None),而405则使用aiohttp.web.HTTPMethodNotAllowed(method, allowed_methods, *, headers=None, reason=None, body=None, text=None, content_type=None)的格式。

WebSocket

aiohttp的HTTP服务同样支持WebSocket。要支持WebSocket服务,只需要建立WebSocketResponse类型实例并在请求处理函数中返回即可。以下给出一个简单的示例。

async def websocket_handler(request):
	ws = web.WebSocketResponse()
	await ws.prepare(request)
	
	async for msg in ws:
		if msg.type = aiohttp.WSMsgType.TEXT:
			if msg.data == 'close':
				await ws.close()
			else:
				await ws.send_str(msg.data + '/answer')
		elif msg.type == aiohttp.WSMsgType.ERROR:
			print(ws.excwption())
	print('websocket connection closed')
	return ws

# WebSocket的处理函数需要绑定到GET请求上。
app.add_routes([web.get('/ws', websocket_handler)])

静态文件映射

对于静态文件的访问,一般HTTP框架的做法都是将一个URL目录映射至自身的静态文件目录中。aiohttp中使用aiohttp.web.static(url, path_to_static_folder)来完成静态文件目录映射。

aiohttp.web.static()除了接受url和被映射路径以外,还接受以下几个参数用于对映射方式进行配置:

  • show_index,对静态目录进行列表。
  • follow_symlinks,允许访问符号链接。
  • append_version,自动追加版本号,用于使客户端的JS和CSS缓存失效。

模板输出

aiohttp支持模板渲染输出,常用的渲染引擎是前面提到的Jinja2,这是由第三方中间件aiohttp_jinja2提供支持的。

在使用模板引擎之前需要先配置Jinja2的环境。

app = web.Application()
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(templateFolder))

之后便可以在处理器函数上使用修饰器@aiohttp_jinja2.template(templateFilename)并在处理器函数中返回字典类型实例作为模板的上下文来渲染输出模板内容。

Warning

这里要注意的是,在使用修饰器定义处理器函数的映射路径时,@aiohttp_jinja2_templete()修饰器需要放在所有修饰器的最末尾,也就是以最后一个修饰器身份出现。因为模板渲染需要在所有修饰器之前执行来输出内容。

共享数据

aiohttp并不建议使用全局变量(如:单例变量)来保存和传递数据,所以aiohttp中的Application和Request类都加入了collections.abc.MutableMapping接口的支持,允许在其中存储一些数据。

要向其中保存数据,可以直接使用对键赋值的格式:app['key']=data。要调用存储的值,可以直接使用request.app['key']来访问。

中间件定义

使用中间件是aiohttp最强大的功能之一,也给aiohttp带来了强大的扩展性。中间件是通过aiohttp.web.middleware修饰器修饰定义的,可以对传入处理函数的Request和从处理函数传出的Response进行修改。以下是一个简单中间件的定义:

from aiohttp.web import middleware

@middleware
async def middleware(request, handler):
	resp = await handler(request)
	resp.text += ' hello'
	return resp

如同示例中所示,中间件函数需要两个参数,第一个是Request实例,第二是处理函数本身。中间件函数中需要调用处理函数来进行下一步的操作。

中间件在定义后,需要在web.Application()中声明使用,格式为app = web.Application(middlewares=[middleware_1, middleware_2]),这里声明中间件的顺序,一般就是中间件的应用顺序。中间件会应用到应用的所有路由处理函数中。

子应用

子应用是为了解决巨型应用中单一问题而存在的,一个巨型应用可以由多个子应用组成。子应用的建立与正常应用相同,但是启动是由父应用负责,需要使用app.add_subapp(url, subapp)方法将子应用绑定在父应用的一个路径下,当父应用启动时,对于指定路径的访问将转由子应用处理。

异步启动

之前的示例中所采用的web.run_app()方法启动应用,是采用阻塞式启动方式。如果需要以异步方式或者指定主机或端口的方式启动,则需要借助AppRunner的辅助。以下示例将应用启动在了8080端口上。

runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, 'localhost', 8080)
await site.start()

如果需要关闭服务,则需要调用await runner.cleanup()

后台任务

很多时候在项目中需要在应用启动后进行一些异步的后台操作,例如对AMQP队列消息的监听等。这些任务可以通过绑定到应用的启动事件和结束事件来执行。

aiohttp提供了app.on_startup_append()来将要执行的任务函数添加到启动事件中,还提供了app.on_clieanup_append()来将任务函数添加到结束事件中以做处理和资源清理工作。