http.server — Clases base para implementar servidores Web

Propósito:http.server incluye clases que pueden formar la base de un servidor Web.

http.server usa clases de socketserver para crear clases base para hacer servidores HTTP. HTTPServer puede ser usada directamente, pero la BaseHTTPRequestHandler está destinada a ser ampliada para manejar cada método de del protocolo (GET, POST, etc.).

HTTP GET

Para agregar soporte para un método HTTP en una clase de manejador de solicitudes, implementa el método do_METHOD(), reemplazando METHOD con el nombre del método HTTP (por ejemplo, do_GET(), do_POST(), etc.). Por consistencia, los métodos de manejo de solicitudes no toman argumentos. Todos los parámetros para la solicitud son analizados por BaseHTTPRequestHandler y almacenados como atributos de la instancia de solicitud.

Este controlador de solicitud de ejemplo ilustra cómo devolver una respuesta al cliente, y algunos de los atributos locales que pueden ser útiles para construir la respuesta.

http_server_GET.py
from http.server import BaseHTTPRequestHandler
from urllib import parse


class GetHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        parsed_path = parse.urlparse(self.path)
        message_parts = [
            'CLIENT VALUES:',
            'client_address={} ({})'.format(
                self.client_address,
                self.address_string()),
            'command={}'.format(self.command),
            'path={}'.format(self.path),
            'real path={}'.format(parsed_path.path),
            'query={}'.format(parsed_path.query),
            'request_version={}'.format(self.request_version),
            '',
            'SERVER VALUES:',
            'server_version={}'.format(self.server_version),
            'sys_version={}'.format(self.sys_version),
            'protocol_version={}'.format(self.protocol_version),
            '',
            'HEADERS RECEIVED:',
        ]
        for name, value in sorted(self.headers.items()):
            message_parts.append(
                '{}={}'.format(name, value.rstrip())
            )
        message_parts.append('')
        message = '\r\n'.join(message_parts)
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()
        self.wfile.write(message.encode('utf-8'))


if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), GetHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

El texto del mensaje se ensambla y luego se escribe en wfile, el manejador de archivo que envuelve el socket de respuesta. Cada respuesta necesita un código de respuesta, establecido a través de send_response(). Si se utiliza un código de error (404, 501, etc.), se incluye un mensaje de error predeterminado apropiado en el encabezado, o se puede pasar un mensaje con el código de error.

Para ejecutar el manejador de solicitudes en un servidor, pásalo al constructor de HTTPServer, en la parte de procesamiento __main__ del script de ejemplo.

A continuación, inicia el servidor:

$ python3 http_server_GET.py

Starting server, use <Ctrl-C> to stop

En una terminal separada, usa curl para acceder a él:

$ curl -v -i http://127.0.0.1:8080/?foo=bar

*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /?foo=bar HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf-8
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 20:44:11 GMT

CLIENT VALUES:
client_address=('127.0.0.1', 52934) (127.0.0.1)
command=GET
path=/?foo=bar
real path=/
query=foo=bar
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept=*/*
Host=127.0.0.1:8080
User-Agent=curl/7.43.0
* Connection #0 to host 127.0.0.1 left intact

Nota

La salida producida por diferentes versiones de curl puede variar. Si la ejecución de los ejemplos produce una salida diferente, verifica el número de versión reportado por curl.

HTTP POST

Soportar solicitudes POST es un poco más de trabajo, porque la clase base no analiza los datos del formulario automáticamente. El módulo cgi proporciona la clase FieldStorage que sabe cómo analizar el formulario, si se le da las entradas correctas.

http_server_POST.py
import cgi
from http.server import BaseHTTPRequestHandler
import io


class PostHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        # Parse the form data posted
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={
                'REQUEST_METHOD': 'POST',
                'CONTENT_TYPE': self.headers['Content-Type'],
            }
        )

        # Begin the response
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()

        out = io.TextIOWrapper(
            self.wfile,
            encoding='utf-8',
            line_buffering=False,
            write_through=True,
        )

        out.write('Client: {}\n'.format(self.client_address))
        out.write('User-agent: {}\n'.format(
            self.headers['user-agent']))
        out.write('Path: {}\n'.format(self.path))
        out.write('Form data:\n')

        # Echo back information about what was posted in the form
        for field in form.keys():
            field_item = form[field]
            if field_item.filename:
                # The field contains an uploaded file
                file_data = field_item.file.read()
                file_len = len(file_data)
                del file_data
                out.write(
                    '\tUploaded {} as {!r} ({} bytes)\n'.format(
                        field, field_item.filename, file_len)
                )
            else:
                # Regular form value
                out.write('\t{}={}\n'.format(
                    field, form[field].value))

        # Disconnect our encoding wrapper from the underlying
        # buffer so that deleting the wrapper doesn't close
        # the socket, which is still being used by the server.
        out.detach()


if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), PostHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

Ejecuta el servidor en una ventana:

$ python3 http_server_POST.py

Starting server, use <Ctrl-C> to stop

Los argumentos para curl pueden incluir datos de formulario para ser publicados en el servidor usando la opción -F. El último argumento, -F datafile=@http_server_GET.py, publica el contenido del archivo http_server_GET.py para ilustrar la lectura de datos de archivos del formulario.

$ curl -v http://127.0.0.1:8080/ -F name=dhellmann -F foo=bar \
-F datafile=@http_server_GET.py

*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Length: 1974
> Expect: 100-continue
> Content-Type: multipart/form-data;
boundary=------------------------a2b3c7485cf8def2
>
* Done waiting for 100-continue
HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf-8
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 20:53:48 GMT

Client: ('127.0.0.1', 53121)
User-agent: curl/7.43.0
Path: /
Form data:
    name=dhellmann
    Uploaded datafile as 'http_server_GET.py' (1612 bytes)
    foo=bar
* Connection #0 to host 127.0.0.1 left intact

Hilos y bifuración

HTTPServer es una subclase simple de socketserver.TCPServer, y no usa múltiples hilos o procesos para manejar las solicitudes. Para agregar hilos o bifurcación, crea una nueva clase utilizando la combinación apropiada de socketserver.

http_server_threads.py
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading


class Handler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()
        message = threading.currentThread().getName()
        self.wfile.write(message.encode('utf-8'))
        self.wfile.write(b'\n')


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Handle requests in a separate thread."""


if __name__ == '__main__':
    server = ThreadedHTTPServer(('localhost', 8080), Handler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

Ejecute el servidor de la misma manera que los otros ejemplos.

$ python3 http_server_threads.py

Starting server, use <Ctrl-C> to stop

Cada vez que el servidor recibe una solicitud, inicia un nuevo hilo o proceso para manejarla:

$ curl http://127.0.0.1:8080/

Thread-1

$ curl http://127.0.0.1:8080/

Thread-2

$ curl http://127.0.0.1:8080/

Thread-3

El intercambio de ForkingMixIn por ThreadingMixIn logrará resultados similares, utilizando procesos separados en lugar de hilos.

Manejo de errores

Maneja los errores llamando a send_error(), pasando el código de error apropiado y un mensaje de error opcional. La respuesta completa (con encabezados, código de estado y cuerpo) se genera automáticamente.

http_server_errors.py
from http.server import BaseHTTPRequestHandler


class ErrorHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_error(404)


if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), ErrorHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

En este caso, siempre se devuelve un error 404.

$ python3 http_server_errors.py

Starting server, use <Ctrl-C> to stop

El mensaje de error se informa al cliente utilizando un documento HTML como así como el encabezado para indicar un código de error.

$ curl -i http://127.0.0.1:8080/

HTTP/1.0 404 Not Found
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 20:58:08 GMT
Connection: close
Content-Type: text/html;charset=utf-8
Content-Length: 447

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type"
        content="text/html;charset=utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 404</p>
        <p>Message: Not Found.</p>
        <p>Error code explanation: 404 - Nothing matches the
        given URI.</p>
    </body>
</html>

Configuración de encabezados

El método send_header agrega datos del encabezado a la respuesta HTTP. Toma dos argumentos: el nombre del encabezado y el valor.

http_server_send_header.py
from http.server import BaseHTTPRequestHandler
import time


class GetHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header(
            'Content-Type',
            'text/plain; charset=utf-8',
        )
        self.send_header(
            'Last-Modified',
            self.date_time_string(time.time())
        )
        self.end_headers()
        self.wfile.write('Response body\n'.encode('utf-8'))


if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), GetHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

Este ejemplo establece el encabezado Last-Modified en marca actual de tiempo, formateada de acuerdo con RFC 7231.

$ curl -i http://127.0.0.1:8080/

HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 21:00:54 GMT
Content-Type: text/plain; charset=utf-8
Last-Modified: Thu, 06 Oct 2016 21:00:54 GMT

Response body

El servidor registra la solicitud a la terminal, como en los otros ejemplos.

$ python3 http_server_send_header.py

Starting server, use <Ctrl-C> to stop
127.0.0.1 - - [06/Oct/2016 17:00:54] "GET / HTTP/1.1" 200 -

Uso de la línea de comando

http.server incluye un servidor incorporado para servir archivos desde el sistema de archivos local. Inícialo desde la línea de comando usando la opción -m de el intérprete de Python.

$ python3 -m http.server 8080

Serving HTTP on 0.0.0.0 port 8080 ...
127.0.0.1 - - [06/Oct/2016 17:12:48] "HEAD /index.rst HTTP/1.1" 200 -

El directorio raíz del servidor es el directorio de trabajo donde el se inicia el servidor.

$ curl -I http://127.0.0.1:8080/index.rst

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 21:12:48 GMT
Content-type: application/octet-stream
Content-Length: 8285
Last-Modified: Thu, 06 Oct 2016 21:12:10 GMT

Ver también