Примеры использования asyncio: HTTPServer?!
- четверг, 27 марта 2014 г. в 03:10:08
asyncio
, и рискну устранить «фатальный недостаток» этого модуля, а именно отсутствие реализации асинхронного HTTP сервера.import asyncio
import logging
import concurrent.futures
@asyncio.coroutine
def handle_connection(reader, writer):
peername = writer.get_extra_info('peername')
logging.info('Accepted connection from {}'.format(peername))
while True:
try:
data = yield from asyncio.wait_for(reader.readline(), timeout=10.0)
if data:
writer.write(data)
else:
logging.info('Connection from {} closed by peer'.format(peername))
break
except concurrent.futures.TimeoutError:
logging.info('Connection from {} closed by timeout'.format(peername))
break
writer.close()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
logging.basicConfig(level=logging.INFO)
server_gen = asyncio.start_server(handle_connection, port=2007)
server = loop.run_until_complete(server_gen)
logging.info('Listening established on {0}'.format(server.sockets[0].getsockname()))
try:
loop.run_forever()
except KeyboardInterrupt:
pass # Press Ctrl+C to stop
finally:
server.close()
loop.close()
server_gen = asyncio.start_server(handle_connection, port=2007)
server = loop.run_until_complete(server_gen)
asyncio
создает и инициализирует по заданным параметрам TCP сервер. Вторая строка и есть пример такого обращения. try:
data = yield from asyncio.wait_for(reader.readline(), timeout=10.0)
if data:
writer.write(data)
else:
logging.info('Connection from {} closed by peer'.format(peername))
break
except concurrent.futures.TimeoutError:
logging.info('Connection from {} closed by timeout'.format(peername))
break
reader.readline()
производит асинхронное чтение данных из входного потока. Но ожидание данных для чтения не ограниченно по времени, если нужно его прекратить по таймауту необходимо обернуть вызов функции-coroutine в asyncio.wait_for()
. В этом случае по истечению заданного в секундах интервала времени будет поднято исключение concurrent.futures.TimeoutError
, которое можно обработать необходимым образом.reader.readline()
возвращает не пустое значение в данном примере обязательна. Иначе после разрыва соединения клиентом (connection reset by peer), попытки чтения и возврат пустого значения будут продолжаться до бесконечности.import asyncio
import logging
import concurrent.futures
class EchoServer(object):
"""Echo server class"""
def __init__(self, host, port, loop=None):
self._loop = loop or asyncio.get_event_loop()
self._server = asyncio.start_server(self.handle_connection, port=2007)
def start(self, and_loop=True):
self._server = self._loop.run_until_complete(self._server)
logging.info('Listening established on {0}'.format(self._server.sockets[0].getsockname()))
if and_loop:
self._loop.run_forever()
def stop(self, and_loop=True):
self._server.close()
if and_loop:
self._loop.close()
@asyncio.coroutine
def handle_connection(self, reader, writer):
peername = writer.get_extra_info('peername')
logging.info('Accepted connection from {}'.format(peername))
while not reader.at_eof():
try:
data = yield from asyncio.wait_for(reader.readline(), timeout=10.0)
writer.write(data)
except concurrent.futures.TimeoutError:
break
writer.close()
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
server = EchoServer('127.0.0.1', 2007)
try:
server.start()
except KeyboardInterrupt:
pass # Press Ctrl+C to stop
finally:
server.stop()
asyncio
предоставляет нам и такую возможность. В нем в отличии например от tornado
не реализован HTTP сервер. Как говориться грех не попробовать исправить это упущение :) asyncio
.import bottle
import os.path
from os import listdir
from bottle import route, template, static_file
root = os.path.abspath(os.path.dirname(__file__))
@route('/')
def index():
tmpl = """<!DOCTYPE html>
<html>
<head><title>Bottle of Aqua</title></head>
</body>
<h3>List of files:</h3>
<ul>
% for item in files:
<li><a href="/files/{{item}}">{{item}}</a></li>
% end
</ul>
</body>
</html>
"""
files = [file_name for file_name in listdir(os.path.join(root, 'files'))
if os.path.isfile(os.path.join(root, 'files', file_name))]
return template(tmpl, files=files)
@route('/files/<filename>')
def server_static(filename):
return static_file(filename, root=os.path.join(root,'files'))
class AquaServer(bottle.ServerAdapter):
"""Bottle server adapter"""
def run(self, handler):
import asyncio
import logging
from aqua.wsgiserver import WSGIServer
logging.basicConfig(level=logging.ERROR)
loop = asyncio.get_event_loop()
server = WSGIServer(handler, loop=loop)
server.bind(self.host, self.port)
try:
loop.run_forever()
except KeyboardInterrupt:
pass # Press Ctrl+C to stop
finally:
server.unbindAll()
loop.close()
if __name__ == '__main__':
bottle.run(server=AquaServer, port=5000)
asyncio
. Единственный момент, это особенность браузеров (например хрома), сбрасывать запрос если он видит что начинает получать большой файл. Очевидно это сделано с целью переключения на более оптимизированный способ загрузки больших файлов, ибо следом запрос повторяется и файл начинает приниматься штатно. Но первый сброшенный запрос вызывает исключение ConnectionResetError
, если отдача файла по нему уже началась с помощь вызова функции StreamWriter.write()
. Этот случай надо обрабатывать и закрывать соединение с помощью StreamWriter.close()
.bottle
, достаточно популярный Waitress WSGI сервер тоже в связке с bottle
и конечно же Tornado. В качестве приложения был минимально возможный helloword. Тесты проводил со следующими параметрами: 100 и 1000 одновременных подключений; длительность теста 10 секунд для 13 байт и килобайт; длительность теста 60 секунд для 13 мегабайт; три варианта размера отдаваемых данных соответственно 13 байт, 13 килобайт и 13 мегабайт. Ниже результат: 100 concurent users |
13 b (10 sec) |
13 Kb (10 sec) |
13 Mb (60 sec) |
|||
---|---|---|---|---|---|---|
Avail. |
Trans/sec |
Avail. |
Trans/sec |
Avail. |
Trans/sec |
|
aqua+bottle |
100,0% |
835,24 |
100,0% |
804,49 |
99,9% |
26,28 |
waitress+bootle |
100,0% |
707,24 |
100,0% |
642,03 |
100,0% |
8,67 |
tornado |
100,0% |
2282,45 |
100,0% |
2071,27 |
100,0% |
15,78 |
1000 concurent users |
13 b (10 sec) |
13 Kb (10 sec) |
13 Mb (60 sec) |
|||
---|---|---|---|---|---|---|
Avail. |
Trans/sec |
Avail. |
Trans/sec |
Avail. |
Trans/sec |
|
aqua+bottle |
99,9% |
800,41 |
99,9% |
777,15 |
60,2% |
26,24 |
waitress+bootle |
94,9% |
689,23 |
99,9% |
621,03 |
37,5% |
8,89 |
tornado |
100,0% |
1239,88 |
100,0% |
978,73 |
55,7% |
14,51 |
asyncio
имеет право на жизнь. Возможно говорить об использовании таких серверов в серьезных проектах пока рано, но после тестирования, обкатки и с появлением асинхронных драйверов asyncio
к базам данных и key-value хранилищам — это вполне может быть возможно.