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

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

View File

@ -1,6 +1,47 @@
import ujson
from .server import Response
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 Response(ujson.dumps(input), content_type="application/json")
return HTTPResponse(ujson.dumps(input), content_type="application/json")
def text(input):
return HTTPResponse(input, content_type="text/plain")

View File

@ -12,4 +12,8 @@ class Router():
self.routes[route] = handler
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,15 +1,16 @@
import inspect
from .router import Router
from .server import Response, serve
from .response import HTTPResponse, error_404
from .server import serve
from .log import log
class Sanic:
name = None
routes = []
def __init__(self, name):
def __init__(self, name, router=None):
self.name = name
self.router = Router(default=self.handler_default)
self.router = router or Router(default=error_404)
def route(self, *args, **kwargs):
def response(handler):
@ -21,6 +22,3 @@ class Sanic:
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

@ -16,6 +16,7 @@ from socket import *
from .log import log
from .config import LOGO
from .response import HTTPResponse
PRINT = 0
@ -29,41 +30,6 @@ class Request:
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',
@ -85,6 +51,8 @@ class HttpProtocol(asyncio.Protocol):
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
@ -108,7 +76,12 @@ class HttpProtocol(asyncio.Protocol):
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
@ -124,6 +97,10 @@ class HttpProtocol(asyncio.Protocol):
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)
# -------------------------------------------- #
@ -138,13 +115,20 @@ class HttpProtocol(asyncio.Protocol):
future.add_done_callback(self.handle_result)
else:
response = handler(request)
self.write_response(response)
self.write_response(request, response)
def write_response(self, response):
self.transport.write(response.output(request.version))
if not self.parser.should_keep_alive():
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
@ -153,12 +137,12 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- #
async def handle_response(self, future, handler, request):
result = await handler(request)
future.set_result(result)
response = await handler(request)
future.set_result((request, response))
def handle_result(self, future):
response = future.result()
self.write_response(response)
request, response = future.result()
self.write_response(request, response)
def abort(msg):

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