Home Blog CV Projects Patterns Notes Book Colophon Search

Fast gevent HTTP Server

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

Comments

Be the first to comment.

Add Comment





Copyright James Gardner 1996-2020 All Rights Reserved. Admin.