├── .gitignore ├── Makefile ├── README.md ├── benchmark.md ├── demo └── main.py ├── requirements.txt ├── setup.py └── storm ├── __init__.py ├── app.pyx ├── exceptions.pyx ├── handler.pyx ├── request.pyx ├── response.pyx └── server.pyx /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.so 3 | **/__pycache/ 4 | *.c 5 | build/ 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | python3 setup.py build_ext -i 3 | 4 | .PHONY: clean test 5 | 6 | clean: 7 | rm storm/*.so storm/*.c 8 | 9 | test: 10 | PYTHONPATH=. python demo/main.py 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Storm: A super fast web framework based on uvloop 2 | 3 | Storm是基于uvloop和Cython的一个web框架,其特性为响应巨快,主要得益于libuv和 4 | Cython的速度和框架的精简。 5 | 6 | ```bash 7 | $ git clone git@github.com:jiajunhuang/storm 8 | $ cd storm 9 | $ make 10 | $ PYTHONPATH=. python3 demo/main.py 11 | ``` 12 | 13 | Demo: 14 | 15 | ```python 16 | import logging 17 | 18 | from storm.app import Application 19 | from storm.handler import RequestHandler 20 | 21 | 22 | class RootHandler(RequestHandler): 23 | def initialize(self): 24 | logging.debug("initialize been called") 25 | 26 | async def get(self): 27 | self.write({ 28 | "hello": "world" 29 | }) 30 | 31 | 32 | handler_classes = [ 33 | (r"/", RootHandler), 34 | ] 35 | 36 | Application( 37 | handler_clses=handler_classes, 38 | debug=False, 39 | ).run() 40 | ``` 41 | 42 | ```bash 43 | $ PYTHONPATH=. python3 demo/main.py 44 | ``` 45 | 46 | [Benchmark](./benchmark.md) 47 | 48 | storm的IOPS约为13106.53,远超Tornado(约2015.07 ),略低于Sanic(约17353.71 ) 49 | storm将在后续优化对象的消耗上,目标是超过Sanic。 50 | -------------------------------------------------------------------------------- /benchmark.md: -------------------------------------------------------------------------------- 1 | benchmark for storm: 2 | 3 | ```bash 4 | $ ab -n 90000 -c 1000 http://127.0.0.1:8080/ 5 | ``` 6 | 7 | ``` 8 | Server Software: 9 | Server Hostname: 127.0.0.1 10 | Server Port: 8080 11 | 12 | Document Path: / 13 | Document Length: 0 bytes 14 | 15 | Concurrency Level: 1000 16 | Time taken for tests: 6.867 seconds 17 | Complete requests: 90000 18 | Failed requests: 0 19 | Non-2xx responses: 90000 20 | Total transferred: 12060000 bytes 21 | HTML transferred: 0 bytes 22 | Requests per second: 13106.53 [#/sec] (mean) 23 | Time per request: 76.298 [ms] (mean) 24 | Time per request: 0.076 [ms] (mean, across all concurrent requests) 25 | Transfer rate: 1715.11 [Kbytes/sec] received 26 | 27 | Connection Times (ms) 28 | min mean[+/-sd] median max 29 | Connect: 0 40 241.9 1 3040 30 | Processing: 2 26 58.0 21 2130 31 | Waiting: 2 21 58.0 16 2125 32 | Total: 9 66 265.9 22 3267 33 | 34 | Percentage of the requests served within a certain time (ms) 35 | 50% 22 36 | 66% 23 37 | 75% 23 38 | 80% 24 39 | 90% 30 40 | 95% 41 41 | 98% 1043 42 | 99% 1248 43 | 100% 3267 (longest request) 44 | ``` 45 | 46 | benchmark for Tornado: 47 | 48 | ```bash 49 | $ ab -n 90000 -c 1000 http://127.0.0.1:8080/ 50 | ``` 51 | 52 | ``` 53 | Server Software: TornadoServer/4.5.1 54 | Server Hostname: 127.0.0.1 55 | Server Port: 8080 56 | 57 | Document Path: / 58 | Document Length: 12 bytes 59 | 60 | Concurrency Level: 1000 61 | Time taken for tests: 8.307 seconds 62 | Complete requests: 16740 63 | Failed requests: 0 64 | Total transferred: 3465180 bytes 65 | HTML transferred: 200880 bytes 66 | Requests per second: 2015.07 [#/sec] (mean) 67 | Time per request: 496.261 [ms] (mean) 68 | Time per request: 0.496 [ms] (mean, across all concurrent requests) 69 | Transfer rate: 407.34 [Kbytes/sec] received 70 | 71 | Connection Times (ms) 72 | min mean[+/-sd] median max 73 | Connect: 0 16 158.3 0 3048 74 | Processing: 1 74 177.3 63 6816 75 | Waiting: 1 74 177.3 63 6816 76 | Total: 11 90 283.0 63 7819 77 | 78 | Percentage of the requests served within a certain time (ms) 79 | 50% 63 80 | 66% 64 81 | 75% 64 82 | 80% 65 83 | 90% 69 84 | 95% 70 85 | 98% 99 86 | 99% 1083 87 | 100% 7819 (longest request) 88 | ``` 89 | 90 | benchmark for sanic: 91 | 92 | ```bash 93 | $ ab -n 90000 -c 1000 http://127.0.0.1:8080/ 94 | ``` 95 | 96 | ``` 97 | Server Software: 98 | Server Hostname: 127.0.0.1 99 | Server Port: 8080 100 | 101 | Document Path: / 102 | Document Length: 17 bytes 103 | 104 | Concurrency Level: 1000 105 | Time taken for tests: 5.186 seconds 106 | Complete requests: 90000 107 | Failed requests: 0 108 | Total transferred: 9630000 bytes 109 | HTML transferred: 1530000 bytes 110 | Requests per second: 17353.71 [#/sec] (mean) 111 | Time per request: 57.625 [ms] (mean) 112 | Time per request: 0.058 [ms] (mean, across all concurrent requests) 113 | Transfer rate: 1813.33 [Kbytes/sec] received 114 | 115 | Connection Times (ms) 116 | min mean[+/-sd] median max 117 | Connect: 0 34 181.1 1 1042 118 | Processing: 1 16 43.0 13 1673 119 | Waiting: 1 12 42.7 10 1669 120 | Total: 2 50 196.3 15 2711 121 | 122 | Percentage of the requests served within a certain time (ms) 123 | 50% 15 124 | 66% 15 125 | 75% 16 126 | 80% 16 127 | 90% 20 128 | 95% 47 129 | 98% 1036 130 | 99% 1047 131 | 100% 2711 (longest request) 132 | ``` 133 | -------------------------------------------------------------------------------- /demo/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from storm.app import Application 4 | from storm.handler import RequestHandler 5 | 6 | 7 | class RootHandler(RequestHandler): 8 | def initialize(self): 9 | logging.debug("initialize been called") 10 | 11 | async def get(self): 12 | self.write({ 13 | "hello": "world" 14 | }) 15 | 16 | 17 | class NotFoundHandler(RequestHandler): 18 | pass 19 | 20 | 21 | class ExceptionHandler(RequestHandler): 22 | def get(self): 23 | raise 24 | 25 | 26 | class BitchHandler(RequestHandler): 27 | def get(self, num): 28 | self.write({ 29 | "got": num 30 | }) 31 | 32 | 33 | handler_classes = [ 34 | (r"/", RootHandler), 35 | (r"/notfound", NotFoundHandler), 36 | (r"/exc", ExceptionHandler), 37 | (r"/bad/(\d+)", ExceptionHandler), 38 | (r"/num/(\d+)", BitchHandler), 39 | ] 40 | 41 | Application( 42 | handler_clses=handler_classes, 43 | debug=False, 44 | ).run() 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Cython==0.25.2 2 | httptools==0.0.9 3 | uvloop==0.8.0 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from Cython.Build import cythonize 3 | 4 | setup( 5 | ext_modules=cythonize("storm/*.pyx") 6 | ) 7 | -------------------------------------------------------------------------------- /storm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiajunhuang/storm/091e2f35a9baae4ee29ef7e2c59d7eb3f93400ae/storm/__init__.py -------------------------------------------------------------------------------- /storm/app.pyx: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import asyncio 4 | import re 5 | 6 | import uvloop 7 | 8 | from storm.server import HTTPProtocol 9 | from storm.exceptions import NotFoundError 10 | 11 | 12 | class Application: 13 | def __init__( 14 | self, 15 | log_level=logging.INFO, 16 | handler_clses=None, 17 | ignore_slash=True, 18 | debug=False, 19 | ): 20 | self.debug = debug 21 | if self.debug: 22 | log_level = logging.DEBUG 23 | 24 | logging.basicConfig(level=log_level) 25 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 26 | self._loop = asyncio.get_event_loop() 27 | 28 | self.ignore_slash = ignore_slash 29 | # handler classes should be a list of tuple like: (url_regex, Handler) 30 | self._handler_classes = [] 31 | for regex, handler_cls in handler_clses: 32 | if not regex.startswith(r"^"): 33 | regex = r"^" + regex 34 | if not regex.endswith(r"$"): 35 | if self.ignore_slash: 36 | append = r"/?$" 37 | else: 38 | append = r"$" 39 | 40 | regex = regex + append 41 | 42 | regex = re.compile(regex) 43 | self._handler_classes.append( 44 | (regex, handler_cls) # a tuple 45 | ) 46 | 47 | def run(self, str host="127.0.0.1", int port=8080): 48 | try: 49 | self._loop.run_until_complete( 50 | self._loop.create_server( 51 | functools.partial(HTTPProtocol, self, self._loop), 52 | host, 53 | port, 54 | ) 55 | ) 56 | logging.warn("Serving on {}:{}".format(host, port)) 57 | self._loop.run_forever() 58 | except KeyboardInterrupt: 59 | self._loop.stop() 60 | 61 | def get_handler_cls(self, str url): 62 | logging.debug("request url is {}".format(url)) 63 | 64 | for regex, handler in self._handler_classes: 65 | if regex.match(url): 66 | return handler, regex.findall(url) 67 | 68 | raise NotFoundError("no handlers found for {}".format(url)) 69 | -------------------------------------------------------------------------------- /storm/exceptions.pyx: -------------------------------------------------------------------------------- 1 | class NotFoundError(Exception): 2 | """404 Not Found""" 3 | pass 4 | -------------------------------------------------------------------------------- /storm/handler.pyx: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class RequestHandler: 5 | def __init__(self, app, request): 6 | cdef: 7 | int status 8 | str content_type 9 | list headers 10 | list body 11 | 12 | self._app = app 13 | self.request = request 14 | self.status = 200 15 | self.content_type = "text/json" 16 | self.headers = [] 17 | self.body = [] 18 | 19 | # initial hook 20 | self.initialize() 21 | 22 | def write(self, body): 23 | """accept string or dict""" 24 | if isinstance(body, dict): 25 | body = json.dumps(body) 26 | self.body.append(body) 27 | 28 | # Hooks 29 | def initialize(self): 30 | pass 31 | 32 | def before_handle(self): 33 | pass 34 | 35 | def after_handle(self): 36 | pass 37 | 38 | # HTTP methods 39 | async def head(self): 40 | raise NotImplementedError() 41 | 42 | async def patch(self): 43 | raise NotImplementedError() 44 | 45 | async def get(self): 46 | raise NotImplementedError() 47 | 48 | async def post(self): 49 | raise NotImplementedError() 50 | 51 | async def put(self): 52 | raise NotImplementedError() 53 | 54 | async def delete(self): 55 | raise NotImplementedError() 56 | 57 | async def options(self): 58 | raise NotImplementedError() 59 | -------------------------------------------------------------------------------- /storm/request.pyx: -------------------------------------------------------------------------------- 1 | class Request: 2 | def __init__(self): 3 | self.url = "" 4 | self.http_version = "1.1" 5 | self.http_method = "GET" 6 | self.headers = {} 7 | self.body = [] 8 | -------------------------------------------------------------------------------- /storm/response.pyx: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | import logging 4 | import traceback 5 | import datetime 6 | 7 | from storm.exceptions import NotFoundError 8 | 9 | HTTP_STATUS_CODE_MAP = { 10 | 100: 'Continue', 11 | 101: 'Switching Protocols', 12 | 102: 'Processing', 13 | 200: 'OK', 14 | 201: 'Created', 15 | 202: 'Accepted', 16 | 203: 'Non-Authoritative Information', 17 | 204: 'No Content', 18 | 205: 'Reset Content', 19 | 206: 'Partial Content', 20 | 207: 'Multi-Status', 21 | 208: 'Already Reported', 22 | 226: 'IM Used', 23 | 300: 'Multiple Choices', 24 | 301: 'Moved Permanently', 25 | 302: 'Found', 26 | 303: 'See Other', 27 | 304: 'Not Modified', 28 | 305: 'Use Proxy', 29 | 307: 'Temporary Redirect', 30 | 308: 'Permanent Redirect', 31 | 400: 'Bad Request', 32 | 401: 'Unauthorized', 33 | 402: 'Payment Required', 34 | 403: 'Forbidden', 35 | 404: 'Not Found', 36 | 405: 'Method Not Allowed', 37 | 406: 'Not Acceptable', 38 | 407: 'Proxy Authentication Required', 39 | 408: 'Request Timeout', 40 | 409: 'Conflict', 41 | 410: 'Gone', 42 | 411: 'Length Required', 43 | 412: 'Precondition Failed', 44 | 413: 'Request Entity Too Large', 45 | 414: 'Request-URI Too Long', 46 | 415: 'Unsupported Media Type', 47 | 416: 'Requested Range Not Satisfiable', 48 | 417: 'Expectation Failed', 49 | 422: 'Unprocessable Entity', 50 | 423: 'Locked', 51 | 424: 'Failed Dependency', 52 | 426: 'Upgrade Required', 53 | 428: 'Precondition Required', 54 | 429: 'Too Many Requests', 55 | 431: 'Request Header Fields Too Large', 56 | 500: 'Internal Server Error', 57 | 501: 'Not Implemented', 58 | 502: 'Bad Gateway', 59 | 503: 'Service Unavailable', 60 | 504: 'Gateway Timeout', 61 | 505: 'HTTP Version Not Supported', 62 | 506: 'Variant Also Negotiates', 63 | 507: 'Insufficient Storage', 64 | 508: 'Loop Detected', 65 | 510: 'Not Extended', 66 | 511: 'Network Authentication Required' 67 | } 68 | 69 | 70 | class HTTPResponse: 71 | def __init__( 72 | self, 73 | int status=200, 74 | str content_type="text/json", 75 | str headers="", 76 | str body="", 77 | ): 78 | self.status = status 79 | self.content_type = content_type 80 | self.headers = headers 81 | self.body = body 82 | 83 | def output(self, str http_version="1.1"): 84 | return ( 85 | "HTTP/{http_version} {http_status} {http_status_code}\r\n" 86 | "Content-Length: {content_length}\r\n" 87 | "Content-Type: {content_type}; charset=UTF-8\r\n" 88 | "Date: {date}\r\n" 89 | "{headers}\r\n" 90 | "{body}" 91 | ).format( 92 | http_version=http_version, 93 | http_status=self.status, 94 | http_status_code=HTTP_STATUS_CODE_MAP[self.status], 95 | content_length=len(self.body), 96 | content_type=self.content_type, 97 | date=datetime.datetime.utcnow().strftime( 98 | '%a, %d %b %Y %H:%M:%S GMT' 99 | ), 100 | headers=self.headers, 101 | body=self.body, 102 | ).encode() 103 | 104 | 105 | async def start_response(app, transport, request): 106 | cdef: 107 | str http_method 108 | list params 109 | object handler_cls 110 | 111 | logging.debug("start to response") 112 | try: 113 | handler_cls, params = app.get_handler_cls(request.url) 114 | except NotFoundError: 115 | response = HTTPResponse(404) 116 | else: 117 | try: 118 | handler = handler_cls(app, request) 119 | handler.before_handle() 120 | # call get/post/... 121 | http_method = request.http_method.lower() 122 | if http_method in ( 123 | "get", "post", "put", "delete", "patch", "options" 124 | ): 125 | http_handler = getattr(handler, request.http_method.lower()) 126 | logging.debug( 127 | "handler {} are calling with params: {}".format( 128 | http_handler, 129 | params, 130 | ) 131 | ) 132 | if params: 133 | may_awaitable = http_handler(*params) 134 | else: 135 | may_awaitable = http_handler() 136 | if inspect.isawaitable(may_awaitable): 137 | await may_awaitable 138 | else: 139 | raise NotImplementedError() 140 | except NotImplementedError: 141 | handler.status = 405 142 | except: 143 | if app.debug: 144 | body = traceback.format_exc() 145 | else: 146 | body = "" 147 | 148 | response = HTTPResponse( 149 | status=500, 150 | body=body, 151 | ) 152 | else: 153 | response = HTTPResponse( 154 | handler.status, 155 | handler.content_type, 156 | "\r\n".join(handler.headers), 157 | "".join(handler.body) 158 | ) 159 | handler.after_handle() 160 | 161 | transport.write(response.output()) 162 | transport.close() 163 | -------------------------------------------------------------------------------- /storm/server.pyx: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | import httptools 5 | 6 | from storm.request import Request 7 | from storm.response import start_response 8 | 9 | 10 | class HTTPProtocol(asyncio.Protocol): 11 | def __init__(self, app, loop=None): 12 | logging.debug("HTTPProtocol initialized with loop {}".format(loop)) 13 | self._app = app 14 | self._loop = loop or asyncio.get_event_loop() 15 | self._transport = None 16 | self._request = Request() 17 | self._parser = httptools.HttpRequestParser(self) 18 | 19 | def connection_made(self, transport): 20 | logging.debug("connection made with transport {}".format(transport)) 21 | self._transport = transport 22 | 23 | def data_received(self, bytes data): 24 | logging.debug("received data {}".format(data)) 25 | try: 26 | self._parser.feed_data(data) 27 | except httptools.HttpParserError: 28 | logging.exception("Bad Request") 29 | 30 | def on_url(self, bytes url): 31 | logging.debug("on url {}".format(url)) 32 | self._request.url = url.decode() 33 | 34 | def on_header(self, bytes name, bytes value): 35 | logging.debug("on header {}: {}".format(name, value)) 36 | self._request.headers[name.decode()] = value.decode() 37 | 38 | def on_headers_complete(self): 39 | logging.debug("headers complete") 40 | self._request.http_version = self._parser.get_http_version() 41 | self._request.http_method = self._parser.get_method().decode() 42 | 43 | def on_body(self, bytes body): 44 | logging.debug("on body {}".format(body)) 45 | self._request.body.append(body.decode()) 46 | 47 | def on_message_complete(self): 48 | logging.debug("message complete") 49 | if self._request.body: 50 | self._request.body = b"".join(self._request.body) 51 | 52 | self._loop.create_task( 53 | start_response(self._app, self._transport, self._request) 54 | ) 55 | --------------------------------------------------------------------------------