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

View File

@ -1,23 +1,23 @@
# Sanic
Python 3.5+ web server that's written to go fast
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
# Sanic
Python 3.5+ web server that's written to 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 .server import Response
from .sanic import Sanic

View File

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

View File

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

View File

@ -1,6 +1,47 @@
import ujson
from .server import Response
def json(input):
return Response(ujson.dumps(input), content_type="application/json")
import ujson
STATUS_CODES = {
200: 'OK',
404: 'Not Found'
}
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
class Router():
routes = None
default = None
def __init__(self, default=None):
self.routes = {}
self.default=default
def add(self, route, handler):
self.routes[route] = handler
def get(self, uri):
return self.routes.get(uri.decode('utf-8'), self.default)
from .log import log
class Router():
routes = None
default = None
def __init__(self, default=None):
self.routes = {}
self.default=default
def add(self, route, handler):
self.routes[route] = handler
def get(self, uri):
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
from .router import Router
from .server import Response, serve
from .log import log
class Sanic:
name = None
routes = []
def __init__(self, name):
self.name = name
self.router = Router(default=self.handler_default)
def route(self, *args, **kwargs):
def response(handler):
handler.is_async = inspect.iscoroutinefunction(handler)
self.router.add(*args, **kwargs, handler=handler)
return handler
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 handler_default(self, request, *args):
return Response("404!", status=404)
import inspect
from .router import Router
from .response import HTTPResponse, error_404
from .server import serve
from .log import log
class Sanic:
name = None
routes = []
def __init__(self, name, router=None):
self.name = name
self.router = router or Router(default=error_404)
def route(self, *args, **kwargs):
def response(handler):
handler.is_async = inspect.iscoroutinefunction(handler)
self.router.add(*args, **kwargs, handler=handler)
return handler
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)

View File

@ -1,196 +1,180 @@
import argparse
import sys
import asyncio
import signal
import functools
import httptools
import logging
import httptools
try:
import uvloop as async_loop
except:
async_loop = asyncio
from socket import *
from .log import log
from .config import LOGO
PRINT = 0
class Request:
__slots__ = ('protocol', 'url', 'headers', 'version', 'method')
def __init__(self, protocol, url, headers, version, method):
self.protocol = protocol
self.url = url
self.headers = headers
self.version = version
self.method = method
STATUS_CODES = {
200: 'OK',
404: 'Not Found'
}
class Response:
__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):
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'),
b'\r\n',
body
])
class HttpProtocol(asyncio.Protocol):
__slots__ = ('loop',
'transport', 'request', 'parser',
'url', 'headers', 'router')
def __init__(self, *, router, loop):
self.loop = loop
self.transport = None
self.request = None
self.parser = None
self.url = None
self.headers = None
self.router = router
# -------------------------------------------- #
# Connection
# -------------------------------------------- #
def connection_made(self, transport):
self.transport = transport
# TCP Nodelay
# I have no evidence to support this makes anything faster
# So I'll leave it commented out for now
# sock = transport.get_extra_info('socket')
# try:
# sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
# except (OSError, NameError):
# pass
def connection_lost(self, exc):
self.request = self.parser = None
# -------------------------------------------- #
# Parsing
# -------------------------------------------- #
def data_received(self, data):
if self.parser is None:
assert self.request is None
self.headers = []
self.parser = httptools.HttpRequestParser(self)
self.parser.feed_data(data)
def on_url(self, url):
self.url = url
def on_header(self, name, value):
self.headers.append((name, value))
def on_headers_complete(self):
self.request = Request(
protocol=self,
url=self.url,
headers=dict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method()
)
self.loop.call_soon(self.handle, self.request)
# -------------------------------------------- #
# Responding
# -------------------------------------------- #
def handle(self, request):
handler = self.router.get(request.url)
if handler.is_async:
future = asyncio.Future()
self.loop.create_task(self.handle_response(future, handler, request))
future.add_done_callback(self.handle_result)
else:
response = handler(request)
self.write_response(response)
def write_response(self, response):
self.transport.write(response.output(request.version))
if not self.parser.should_keep_alive():
self.transport.close()
self.parser = None
self.request = None
# -------------------------------------------- #
# Async
# -------------------------------------------- #
async def handle_response(self, future, handler, request):
result = await handler(request)
future.set_result(result)
def handle_result(self, future):
response = future.result()
self.write_response(response)
def abort(msg):
log.info(msg, file=sys.stderr)
sys.exit(1)
def serve(router, host, port, debug=False):
# Create Event Loop
loop = async_loop.new_event_loop()
asyncio.set_event_loop(loop)
loop.set_debug(debug)
# Add signal handlers
def ask_exit(signame):
log.debug("Exiting, received signal %s" % signame)
loop.stop()
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()
import argparse
import sys
import asyncio
import signal
import functools
import httptools
import logging
import httptools
try:
import uvloop as async_loop
except:
async_loop = asyncio
from socket import *
from .log import log
from .config import LOGO
from .response import HTTPResponse
PRINT = 0
class Request:
__slots__ = ('protocol', 'url', 'headers', 'version', 'method')
def __init__(self, protocol, url, headers, version, method):
self.protocol = protocol
self.url = url
self.headers = headers
self.version = version
self.method = method
class HttpProtocol(asyncio.Protocol):
__slots__ = ('loop',
'transport', 'request', 'parser',
'url', 'headers', 'router')
def __init__(self, *, router, loop):
self.loop = loop
self.transport = None
self.request = None
self.parser = None
self.url = None
self.headers = None
self.router = router
# -------------------------------------------- #
# Connection
# -------------------------------------------- #
def connection_made(self, transport):
self.transport = transport
#TODO: handle keep-alive/connection timeout
# TCP Nodelay
# I have no evidence to support this makes anything faster
# So I'll leave it commented out for now
# sock = transport.get_extra_info('socket')
# try:
# sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
# except (OSError, NameError):
# pass
def connection_lost(self, exc):
self.request = self.parser = None
# -------------------------------------------- #
# Parsing
# -------------------------------------------- #
def data_received(self, data):
if self.parser is None:
assert self.request is None
self.headers = []
self.parser = httptools.HttpRequestParser(self)
try:
#print(data)
self.parser.feed_data(data)
except httptools.parser.errors.HttpParserError:
#log.error("Invalid request data, connection closed")
self.transport.close()
def on_url(self, url):
self.url = url
def on_header(self, name, value):
self.headers.append((name, value))
def on_headers_complete(self):
self.request = Request(
protocol=self,
url=self.url,
headers=dict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method()
)
global n
n += 1
self.n = n
#print("res {} - {}".format(n, self.request))
self.loop.call_soon(self.handle, self.request)
# -------------------------------------------- #
# Responding
# -------------------------------------------- #
def handle(self, request):
handler = self.router.get(request.url)
if handler.is_async:
future = asyncio.Future()
self.loop.create_task(self.handle_response(future, handler, request))
future.add_done_callback(self.handle_result)
else:
response = handler(request)
self.write_response(request, response)
def write_response(self, request, response):
#print("response - {} - {}".format(self.n, self.request))
try:
keep_alive = self.parser.should_keep_alive()
self.transport.write(response.output(request.version, keep_alive))
#print("KA - {}".format(self.parser.should_keep_alive()))
if not keep_alive:
self.transport.close()
except:
log.error("Writing request failed, connection closed")
self.transport.close()
self.parser = None
self.request = None
# -------------------------------------------- #
# Async
# -------------------------------------------- #
async def handle_response(self, future, handler, request):
response = await handler(request)
future.set_result((request, response))
def handle_result(self, future):
request, response = future.result()
self.write_response(request, response)
def abort(msg):
log.info(msg, file=sys.stderr)
sys.exit(1)
def serve(router, host, port, debug=False):
# Create Event Loop
loop = async_loop.new_event_loop()
asyncio.set_event_loop(loop)
loop.set_debug(debug)
# Add signal handlers
def ask_exit(signame):
log.debug("Exiting, received signal %s" % signame)
loop.stop()
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()

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")