Fixed keep-alive header and broken connection handling

This commit is contained in:
Channel Cat 2016-10-02 02:18:41 +00:00
parent 9dd87a62ad
commit 8cc028764d
12 changed files with 347 additions and 290 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
settings.py settings.py
*.pyc *.pyc

View File

@ -1,23 +1,23 @@
# Sanic # Sanic
Python 3.5+ web server that's written to go fast Python 3.5+ web server that's written to go fast
▄▄▄▄▄ ▄▄▄▄▄
▀▀▀██████▄▄▄ _______________ ▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \ ▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/ ▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄ ▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ ▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌ ▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ ▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀ ▀▀█████████▀
▄▄██▀██████▀█ ▄▄██▀██████▀█
▄██▀ ▀▀▀ █ ▄██▀ ▀▀▀ █
▄█ ▐▌ ▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀ ▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀ ▀▀▄▄▀

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
uvloop
httptools
ujson

View File

@ -1,2 +1 @@
from .sanic import Sanic from .sanic import Sanic
from .server import Response

View File

@ -1,21 +1,21 @@
LOGO = """ LOGO = """
_______________ _______________
/ \\ / \\
| Gotta go fast! | | Gotta go fast! |
| _________________/ | _________________/
|/ |/
""" """

View File

@ -1,4 +1,4 @@
import logging import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -1,6 +1,47 @@
import ujson import ujson
from .server import Response STATUS_CODES = {
200: 'OK',
def json(input): 404: 'Not Found'
return Response(ujson.dumps(input), content_type="application/json") }
class HTTPResponse:
__slots__ = ('body', 'status', 'content_type')
def __init__(self, body='', status=200, content_type='text/plain'):
self.content_type = 'text/plain'
self.body = body
self.status = status
@property
def body_bytes(self):
body_type = type(self.body)
if body_type is str:
body = self.body.encode('utf-8')
elif body_type is bytes:
body = self.body
else:
body = b'Unable to interpret body'
return body
def output(self, version="1.1", keep_alive=False):
body = self.body_bytes
return b''.join([
'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode('latin-1'),
'Content-Type: {}\r\n'.format(self.content_type).encode('latin-1'),
'Content-Length: {}\r\n'.format(len(body)).encode('latin-1'),
'Connection: {}\r\n'.format('keep-alive' if keep_alive else 'close').encode('latin-1'),
b'\r\n',
body,
#b'\r\n'
])
def error_404(request, *args):
return HTTPResponse("404!", status=404)
error_404.is_async = False
def json(input):
return HTTPResponse(ujson.dumps(input), content_type="application/json")
def text(input):
return HTTPResponse(input, content_type="text/plain")

View File

@ -1,15 +1,19 @@
from .log import log from .log import log
class Router(): class Router():
routes = None routes = None
default = None default = None
def __init__(self, default=None): def __init__(self, default=None):
self.routes = {} self.routes = {}
self.default=default self.default=default
def add(self, route, handler): def add(self, route, handler):
self.routes[route] = handler self.routes[route] = handler
def get(self, uri): def get(self, uri):
return self.routes.get(uri.decode('utf-8'), self.default) handler = self.routes.get(uri.decode('utf-8'), self.default)
if handler:
return handler
else:
return self.default

View File

@ -1,26 +1,24 @@
import inspect import inspect
from .router import Router from .router import Router
from .server import Response, serve from .response import HTTPResponse, error_404
from .log import log from .server import serve
from .log import log
class Sanic:
name = None class Sanic:
routes = [] name = None
routes = []
def __init__(self, name):
self.name = name def __init__(self, name, router=None):
self.router = Router(default=self.handler_default) self.name = name
self.router = router or Router(default=error_404)
def route(self, *args, **kwargs):
def response(handler): def route(self, *args, **kwargs):
handler.is_async = inspect.iscoroutinefunction(handler) def response(handler):
self.router.add(*args, **kwargs, handler=handler) handler.is_async = inspect.iscoroutinefunction(handler)
return handler self.router.add(*args, **kwargs, handler=handler)
return handler
return response
return response
def run(self, host="127.0.0.1", port=8000, debug=False):
return serve(router=self.router, host=host, port=port, debug=debug) def run(self, host="127.0.0.1", port=8000, debug=False):
return serve(router=self.router, host=host, port=port, debug=debug)
def handler_default(self, request, *args):
return Response("404!", status=404)

View File

@ -1,196 +1,180 @@
import argparse import argparse
import sys import sys
import asyncio import asyncio
import signal import signal
import functools import functools
import httptools import httptools
import logging import logging
import httptools import httptools
try: try:
import uvloop as async_loop import uvloop as async_loop
except: except:
async_loop = asyncio async_loop = asyncio
from socket import * from socket import *
from .log import log from .log import log
from .config import LOGO from .config import LOGO
from .response import HTTPResponse
PRINT = 0
PRINT = 0
class Request:
__slots__ = ('protocol', 'url', 'headers', 'version', 'method') class Request:
__slots__ = ('protocol', 'url', 'headers', 'version', 'method')
def __init__(self, protocol, url, headers, version, method):
self.protocol = protocol def __init__(self, protocol, url, headers, version, method):
self.url = url self.protocol = protocol
self.headers = headers self.url = url
self.version = version self.headers = headers
self.method = method self.version = version
self.method = method
STATUS_CODES = {
200: 'OK', class HttpProtocol(asyncio.Protocol):
404: 'Not Found'
} __slots__ = ('loop',
class Response: 'transport', 'request', 'parser',
__slots__ = ('body', 'status', 'content_type') 'url', 'headers', 'router')
def __init__(self, body='', status=200, content_type='text/plain'): def __init__(self, *, router, loop):
self.content_type = 'text/plain' self.loop = loop
self.body = body self.transport = None
self.status = status self.request = None
self.parser = None
@property self.url = None
def body_bytes(self): self.headers = None
body_type = type(self.body) self.router = router
if body_type is str:
body = self.body.encode('utf-8') # -------------------------------------------- #
elif body_type is bytes: # Connection
body = self.body # -------------------------------------------- #
else:
body = b'Unable to interpret body' def connection_made(self, transport):
self.transport = transport
return body #TODO: handle keep-alive/connection timeout
def output(self, version): # TCP Nodelay
body = self.body_bytes # I have no evidence to support this makes anything faster
return b''.join([ # So I'll leave it commented out for now
'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode('latin-1'),
'Content-Type: {}\r\n'.format(self.content_type).encode('latin-1'), # sock = transport.get_extra_info('socket')
'Content-Length: {}\r\n'.format(len(body)).encode('latin-1'), # try:
b'\r\n', # sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
body # except (OSError, NameError):
]) # pass
def connection_lost(self, exc):
class HttpProtocol(asyncio.Protocol): self.request = self.parser = None
__slots__ = ('loop', # -------------------------------------------- #
'transport', 'request', 'parser', # Parsing
'url', 'headers', 'router') # -------------------------------------------- #
def __init__(self, *, router, loop): def data_received(self, data):
self.loop = loop if self.parser is None:
self.transport = None assert self.request is None
self.request = None self.headers = []
self.parser = None self.parser = httptools.HttpRequestParser(self)
self.url = None
self.headers = None try:
self.router = router #print(data)
self.parser.feed_data(data)
# -------------------------------------------- # except httptools.parser.errors.HttpParserError:
# Connection #log.error("Invalid request data, connection closed")
# -------------------------------------------- # self.transport.close()
def connection_made(self, transport): def on_url(self, url):
self.transport = transport self.url = url
# TCP Nodelay
# I have no evidence to support this makes anything faster def on_header(self, name, value):
# So I'll leave it commented out for now self.headers.append((name, value))
# sock = transport.get_extra_info('socket') def on_headers_complete(self):
# try: self.request = Request(
# sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) protocol=self,
# except (OSError, NameError): url=self.url,
# pass headers=dict(self.headers),
version=self.parser.get_http_version(),
def connection_lost(self, exc): method=self.parser.get_method()
self.request = self.parser = None )
global n
# -------------------------------------------- # n += 1
# Parsing self.n = n
# -------------------------------------------- # #print("res {} - {}".format(n, self.request))
self.loop.call_soon(self.handle, self.request)
def data_received(self, data):
if self.parser is None: # -------------------------------------------- #
assert self.request is None # Responding
self.headers = [] # -------------------------------------------- #
self.parser = httptools.HttpRequestParser(self)
def handle(self, request):
self.parser.feed_data(data) handler = self.router.get(request.url)
if handler.is_async:
def on_url(self, url): future = asyncio.Future()
self.url = url self.loop.create_task(self.handle_response(future, handler, request))
future.add_done_callback(self.handle_result)
def on_header(self, name, value): else:
self.headers.append((name, value)) response = handler(request)
self.write_response(request, response)
def on_headers_complete(self):
self.request = Request( def write_response(self, request, response):
protocol=self, #print("response - {} - {}".format(self.n, self.request))
url=self.url, try:
headers=dict(self.headers), keep_alive = self.parser.should_keep_alive()
version=self.parser.get_http_version(), self.transport.write(response.output(request.version, keep_alive))
method=self.parser.get_method() #print("KA - {}".format(self.parser.should_keep_alive()))
) if not keep_alive:
self.loop.call_soon(self.handle, self.request) self.transport.close()
except:
# -------------------------------------------- # log.error("Writing request failed, connection closed")
# Responding self.transport.close()
# -------------------------------------------- #
self.parser = None
def handle(self, request): self.request = None
handler = self.router.get(request.url)
if handler.is_async: # -------------------------------------------- #
future = asyncio.Future() # Async
self.loop.create_task(self.handle_response(future, handler, request)) # -------------------------------------------- #
future.add_done_callback(self.handle_result)
else: async def handle_response(self, future, handler, request):
response = handler(request) response = await handler(request)
self.write_response(response) future.set_result((request, response))
def write_response(self, response): def handle_result(self, future):
self.transport.write(response.output(request.version)) request, response = future.result()
self.write_response(request, response)
if not self.parser.should_keep_alive():
self.transport.close()
self.parser = None def abort(msg):
self.request = None log.info(msg, file=sys.stderr)
sys.exit(1)
# -------------------------------------------- #
# Async
# -------------------------------------------- # def serve(router, host, port, debug=False):
# Create Event Loop
async def handle_response(self, future, handler, request): loop = async_loop.new_event_loop()
result = await handler(request) asyncio.set_event_loop(loop)
future.set_result(result) loop.set_debug(debug)
def handle_result(self, future): # Add signal handlers
response = future.result() def ask_exit(signame):
self.write_response(response) log.debug("Exiting, received signal %s" % signame)
loop.stop()
def abort(msg): for signame in ('SIGINT', 'SIGTERM'):
log.info(msg, file=sys.stderr) loop.add_signal_handler(getattr(signal, signame), functools.partial(ask_exit, signame))
sys.exit(1)
if debug:
log.setLevel(logging.DEBUG)
def serve(router, host, port, debug=False): log.debug(LOGO)
# Create Event Loop
loop = async_loop.new_event_loop() # Serve
asyncio.set_event_loop(loop) log.info('Goin\' Fast @ {}:{}'.format(host, port))
loop.set_debug(debug)
server_coroutine = loop.create_server(lambda: HttpProtocol(loop=loop, router=router), host, port)
# Add signal handlers server_loop = loop.run_until_complete(server_coroutine)
def ask_exit(signame): try:
log.debug("Exiting, received signal %s" % signame) loop.run_forever()
loop.stop() finally:
server_loop.close()
for signame in ('SIGINT', 'SIGTERM'):
loop.add_signal_handler(getattr(signal, signame), functools.partial(ask_exit, signame))
if debug:
log.setLevel(logging.DEBUG)
log.debug(LOGO)
# Serve
log.info('Goin\' Fast @ {}:{}'.format(host, port))
server_coroutine = loop.create_server(lambda: HttpProtocol(loop=loop, router=router), host, port)
server_loop = loop.run_until_complete(server_coroutine)
try:
loop.run_forever()
finally:
server_loop.close()
loop.close() loop.close()

15
test.go Normal file
View File

@ -0,0 +1,15 @@
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8000", nil)
}

13
test.py Normal file
View File

@ -0,0 +1,13 @@
from sanic import Sanic
from sanic.response import json, text
app = Sanic("test")
@app.route("/")
async def test(request):
return json({ "test": True })
@app.route("/text")
def test(request):
return text('hi')
app.run(host="0.0.0.0")