4 Feb, 2022
Following on from Fast Python HTTP Server here is a gevent server that uses greenlets instead of asyncio to do non-blocking IO.
The advantage of this approach is that libraries like boto3 can be used with gevent if you monkeypatch them.
It also allows you to write code that is normal Python without async
or await
.
This server does about 80k requests per second on my laptop, but it doesn't use multiprocessing.
from gevent.server import StreamServer
DEV = LOGGING = False
if LOGGING:
info = print
debug = lambda *x: None
log = error = print
class BadRequest(Exception):
pass
def headers(reader, headers_to_return):
if DEV:
for header in headers_to_return:
assert isinstance(header, bytes) and header.lower() == header, f'Expected lowercase header name as a type \'bytes\', not {header}'
found = [None for name in headers_to_return]
while True:
line = (reader.readline())
if line == b'\r\n':
if len(headers_to_return) == 1:
# Just return an item, not an array so that the caller doesn't need array index access
return found[0]
return found
if len(line) < 4:
raise BadRequest(f'Invalid header: {line}')
for name_index, name in enumerate(headers_to_return):
match = True
index = 0
for i, c in enumerate(name):
if LOGGING:
debug('Header characters', line, name, c, line[i], line[i] + 32)
if not (c == line[i] or c == line[i] + 32):
match = False
break
else:
index = i
if match and len(line) > 2:
while index < len(line) - 3:
c = line[index + 1]
if LOGGING:
debug('Header finding ":"', line[index+1], index, c)
if c == 32:
index += 1
elif c == 58:
index +=1
break
else:
raise Exception(line[index:])
if found[name_index] is None:
found[name_index] = line[index+1:].strip()
else:
# https://stackoverflow.com/questions/4371328/are-duplicate-http-response-headers_to_return-acceptable
found[name_index] += b', ' + line[index+1:].strip()
# this handler will be run for each incoming connection in a dedicated greenlet
def echo(socket, address):
reader = socket.makefile(mode='rb')
try:
while True:
line = reader.readline()
if not line:
reader.close()
break
try:
method, path, version = line.strip().split(b' ')
except Exception as e:
raise BadRequest(str(e))
if path == b'/ok':
connection = headers(reader, [b'connection'])
response = b'HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK'
else:
connection = headers(reader, [b'connection'])
response = b'HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot Found'
if LOGGING:
debug(f"Response: {response!r}")
socket.sendall(response)
if connection:
connection_lower = connection.lower()
if connection_lower == b'close' or version == b'HTTP/1.0' and connection_lower != 'keep-alive':
if LOGGING:
debug("Close the connection")
reader.close()
break
except ConnectionResetError:
# The connection is already closed, nothing to do
pass
except BadRequest as e:
error(e)
response = b'HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 11\r\n\r\nBad Request'
socket.sendall(response)
reader.close()
# https://stackoverflow.com/questions/7160983/catching-all-exceptions-in-python
except Exception as e:
error(e)
response = b'HTTP/1.1 500 Error\r\nConnection: close\r\nContent-Length: 5\r\n\r\nError'
socket.sendall(response)
reader.close()
if __name__ == '__main__':
server = StreamServer(('127.0.0.1', 16000), echo)
print('Starting echo server on port 16000')
server.serve_forever()
Be the first to comment.
Copyright James Gardner 1996-2020 All Rights Reserved. Admin.