socketserver — Creación de servidores de red

Propósito:Creación de servidores de red.

El módulo socketserver es un marco para crear servidores de red. Define clases para manejar solicitudes de red síncronas (el controlador de solicitudes del servidor se bloquea hasta que se complete la solicitud) sobre TCP, UDP, flujos Unix y datagramas Unix. También proporciona clases de mezcla para convertir fácilmente servidores para usar un hilo separado o proceso para cada solicitud.

La responsabilidad de procesar una solicitud se divide entre una clase servidor y una clase manejador de solicitudes. El servidor se ocupa de la comunicación, como escuchar en un conector y aceptar conexiones, y el controlador de solicitudes se ocupa del «protocolo» como la interpretación de los datos entrantes, su procesamiento y envío de vuelta al cliente. Esta división de responsabilidad significa que muchas aplicaciones pueden usar una de las clases de servidor existentes sin ninguna modificación, y proporcionar una clase manejador de solicitudes para que funcione con el protocolo personalizado.

Tipos de servidor

Hay cinco clases de servidor diferentes definidas en socketserver. BaseServer define la interfaz de programación, y no está destinado a ser instanciado y utilizado directamente. TCPServer utiliza conectores TCP/IP para comunicarse. UDPServer usa conectores de datagramas. UnixStreamServer y UnixDatagramServer usan conectores de dominio Unix y solo están disponibles en plataformas Unix.

Objetos servidor

Para construir un servidor, pásale una dirección en la que escuchar solicitudes y una clase (no instancia) manejador de solicitudes. El formato de dirección depende del tipo de servidor y la familia de conector que se utiliza. Revisa a la documentación del módulo socket para más detalles.

Una vez que se crea una instancia del objeto servidor, usa handle_request() o serve_forever() para procesar las peticiones. El método serve_forever() llama handle_request() en un bucle infinito, pero si una aplicación necesita integrar el servidor con otro bucle de eventos o usar select() para monitorear varios conectores para diferentes servidores, puede llamar a handle_request() directamente.

Implementar un servidor

Al crear un servidor, normalmente es suficiente reutilizar uno de las clases existentes y proporcionar una clase de controlador de solicitud personalizada. Para otros casos, BaseServer incluye varios métodos que pueden ser anulados en una subclase.

  • verify_request(request, client_address): Devuelve True para procesar la solicitud o False para ignorarla. Por ejemplo, un servidor podría rechazar las solicitudes de un rango de IP o si está sobrecargado.
  • process_request(request, client_address): Llama a finish_request() para hacer el trabajo de manejar la solicitud. También puede crear un hilo o proceso separado, como lo hacen las clases de mezcla.
  • finish_request(request, client_address): Crea una instancia de controlador de solicitud utilizando la clase dada al constructor de os servidores. Llama a handle() en el controlador de solicitudes para procesar la solicitud.

Manejadores de solicitudes

Los manejadores de solicitudes realizan la mayor parte del trabajo de recibir solicitudes entrantes y decidir qué acción tomar. El manejador es responsable de implementar el protocolo en la parte superior de la capa de conector (es decir, HTTP, XML-RPC, o AMQP). El manejador de solicitudes lee la solicitud del canal de datos entrantes, lo procesa y escribe una respuesta de salida. Hay tres métodos disponibles para ser superado.

  • setup(): prepara el manejador de solicitudes para la solicitud. En el StreamRequestHandler el método setup() crea objetos tipo archivo para leer y escribir en el conector.
  • handle(): Realiza el trabajo real para la solicitud. Analizar la solicitud entrante procesar los datos, y enviar una respuesta.
  • finish(): Limpia cualquier cosa creada durante setup().

Muchos controladores se pueden implementar con solo un método handle().

Ejemplo de eco

Este ejemplo implementa un par simple servidor/manejador de solicitudes que acepta conexiones TCP y devuelve cualquier información enviada por el cliente. Comienza con el manejador de solicitudes.

socketserver_echo.py
import logging
import sys
import socketserver

logging.basicConfig(level=logging.DEBUG,
                    format='%(name)s: %(message)s',
                    )


class EchoRequestHandler(socketserver.BaseRequestHandler):

    def __init__(self, request, client_address, server):
        self.logger = logging.getLogger('EchoRequestHandler')
        self.logger.debug('__init__')
        socketserver.BaseRequestHandler.__init__(self, request,
                                                 client_address,
                                                 server)
        return

    def setup(self):
        self.logger.debug('setup')
        return socketserver.BaseRequestHandler.setup(self)

    def handle(self):
        self.logger.debug('handle')

        # Echo the back to the client
        data = self.request.recv(1024)
        self.logger.debug('recv()->"%s"', data)
        self.request.send(data)
        return

    def finish(self):
        self.logger.debug('finish')
        return socketserver.BaseRequestHandler.finish(self)

El único método que realmente necesita ser implementado es EchoRequestHandler.handle(), pero las versiones de todos los los métodos descritos anteriormente se incluyen para ilustrar la secuencia de llamadas hechas. La clase EchoServer no hace nada diferente de TCPServer, excepto el registro cuando cada método es llamado.

class EchoServer(socketserver.TCPServer):

    def __init__(self, server_address,
                 handler_class=EchoRequestHandler,
                 ):
        self.logger = logging.getLogger('EchoServer')
        self.logger.debug('__init__')
        socketserver.TCPServer.__init__(self, server_address,
                                        handler_class)
        return

    def server_activate(self):
        self.logger.debug('server_activate')
        socketserver.TCPServer.server_activate(self)
        return

    def serve_forever(self, poll_interval=0.5):
        self.logger.debug('waiting for request')
        self.logger.info(
            'Handling requests, press <Ctrl-C> to quit'
        )
        socketserver.TCPServer.serve_forever(self, poll_interval)
        return

    def handle_request(self):
        self.logger.debug('handle_request')
        return socketserver.TCPServer.handle_request(self)

    def verify_request(self, request, client_address):
        self.logger.debug('verify_request(%s, %s)',
                          request, client_address)
        return socketserver.TCPServer.verify_request(
            self, request, client_address,
        )

    def process_request(self, request, client_address):
        self.logger.debug('process_request(%s, %s)',
                          request, client_address)
        return socketserver.TCPServer.process_request(
            self, request, client_address,
        )

    def server_close(self):
        self.logger.debug('server_close')
        return socketserver.TCPServer.server_close(self)

    def finish_request(self, request, client_address):
        self.logger.debug('finish_request(%s, %s)',
                          request, client_address)
        return socketserver.TCPServer.finish_request(
            self, request, client_address,
        )

    def close_request(self, request_address):
        self.logger.debug('close_request(%s)', request_address)
        return socketserver.TCPServer.close_request(
            self, request_address,
        )

    def shutdown(self):
        self.logger.debug('shutdown()')
        return socketserver.TCPServer.shutdown(self)

El último paso es agregar un programa principal que configure el servidor para que se ejecute en un hilo, y le envía datos para ilustrar qué métodos se llaman cuando se repiten los datos.

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0)  # let the kernel assign a port
    server = EchoServer(address, EchoRequestHandler)
    ip, port = server.server_address  # what port was assigned?

    # Start the server in a thread
    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # don't hang on exit
    t.start()

    logger = logging.getLogger('client')
    logger.info('Server on %s:%s', ip, port)

    # Connect to the server
    logger.debug('creating socket')
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    logger.debug('connecting to server')
    s.connect((ip, port))

    # Send the data
    message = 'Hello, world'.encode()
    logger.debug('sending data: %r', message)
    len_sent = s.send(message)

    # Receive a response
    logger.debug('waiting for response')
    response = s.recv(len_sent)
    logger.debug('response from server: %r', response)

    # Clean up
    server.shutdown()
    logger.debug('closing socket')
    s.close()
    logger.debug('done')
    server.socket.close()

Ejecutar el programa produce la siguiente salida.

$ python3 socketserver_echo.py

EchoServer: __init__
EchoServer: server_activate
EchoServer: waiting for request
EchoServer: Handling requests, press <Ctrl-C> to quit
client: Server on 127.0.0.1:55484
client: creating socket
client: connecting to server
client: sending data: b'Hello, world'
EchoServer: verify_request(<socket.socket fd=7, family=AddressFamily
.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1',
55484), raddr=('127.0.0.1', 55485)>, ('127.0.0.1', 55485))
EchoServer: process_request(<socket.socket fd=7, family=AddressFamil
y.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1',
 55484), raddr=('127.0.0.1', 55485)>, ('127.0.0.1', 55485))
EchoServer: finish_request(<socket.socket fd=7, family=AddressFamily
.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1',
55484), raddr=('127.0.0.1', 55485)>, ('127.0.0.1', 55485))
EchoRequestHandler: __init__
EchoRequestHandler: setup
EchoRequestHandler: handle
client: waiting for response
EchoRequestHandler: recv()->"b'Hello, world'"
EchoRequestHandler: finish
client: response from server: b'Hello, world'
EchoServer: shutdown()
EchoServer: close_request(<socket.socket fd=7, family=AddressFamily.
AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 5
5484), raddr=('127.0.0.1', 55485)>)
client: closing socket
client: done

Nota

El número de puerto utilizado cambiará cada vez que se ejecute el programa porque el kernel asigna un puerto disponible automáticamente. Para hacer que el servidor escuche en un puerto específico cada vez, provea que número en la tupla de direcciones en lugar de 0.

Aquí hay una versión condensada del mismo servidor, sin el registro llamadas. Solo el método handle() en la clase de manejador de solicitudes necesita ser proporcionado.

socketserver_echo_simple.py
import socketserver


class EchoRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # Echo the back to the client
        data = self.request.recv(1024)
        self.request.send(data)
        return


if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0)  # let the kernel assign a port
    server = socketserver.TCPServer(address, EchoRequestHandler)
    ip, port = server.server_address  # what port was assigned?

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # don't hang on exit
    t.start()

    # Connect to the server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Send the data
    message = 'Hello, world'.encode()
    print('Sending : {!r}'.format(message))
    len_sent = s.send(message)

    # Receive a response
    response = s.recv(len_sent)
    print('Received: {!r}'.format(response))

    # Clean up
    server.shutdown()
    s.close()
    server.socket.close()

En este caso, no se requiere una clase de servidor especial ya que TCPServer maneja todos los requerimientos del servidor.

$ python3 socketserver_echo_simple.py

Sending : b'Hello, world'
Received: b'Hello, world'

Cread hilos y bifurcar

Para agregar subprocesos o soporte de bifurcación a un servidor, incluye la mezcla apropiada en la jerarquía de clases para el servidor. Las cases de mezcla anulan process_request() para iniciar un nuevo hilo o proceso cuando una solicitud está lista para ser manejada, y el trabajo se realiza en el nuevo proceso hijo.

Para hilos, usa ThreadingMixIn.

socketserver_threaded.py
import threading
import socketserver


class ThreadedEchoRequestHandler(
        socketserver.BaseRequestHandler,
):

    def handle(self):
        # Echo the back to the client
        data = self.request.recv(1024)
        cur_thread = threading.currentThread()
        response = b'%s: %s' % (cur_thread.getName().encode(),
                                data)
        self.request.send(response)
        return


class ThreadedEchoServer(socketserver.ThreadingMixIn,
                         socketserver.TCPServer,
                         ):
    pass


if __name__ == '__main__':
    import socket

    address = ('localhost', 0)  # let the kernel assign a port
    server = ThreadedEchoServer(address,
                                ThreadedEchoRequestHandler)
    ip, port = server.server_address  # what port was assigned?

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # don't hang on exit
    t.start()
    print('Server loop running in thread:', t.getName())

    # Connect to the server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Send the data
    message = b'Hello, world'
    print('Sending : {!r}'.format(message))
    len_sent = s.send(message)

    # Receive a response
    response = s.recv(1024)
    print('Received: {!r}'.format(response))

    # Clean up
    server.shutdown()
    s.close()
    server.socket.close()

La respuesta de este servidor con hilos incluye el identificador del hilo donde se maneja la solicitud.

$ python3 socketserver_threaded.py

Server loop running in thread: Thread-1
Sending : b'Hello, world'
Received: b'Thread-2: Hello, world'

Para procesos separados, use el ForkingMixIn.

socketserver_forking.py
import os
import socketserver


class ForkingEchoRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # Echo the back to the client
        data = self.request.recv(1024)
        cur_pid = os.getpid()
        response = b'%d: %s' % (cur_pid, data)
        self.request.send(response)
        return


class ForkingEchoServer(socketserver.ForkingMixIn,
                        socketserver.TCPServer,
                        ):
    pass


if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0)  # let the kernel assign a port
    server = ForkingEchoServer(address,
                               ForkingEchoRequestHandler)
    ip, port = server.server_address  # what port was assigned?

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # don't hang on exit
    t.start()
    print('Server loop running in process:', os.getpid())

    # Connect to the server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Send the data
    message = 'Hello, world'.encode()
    print('Sending : {!r}'.format(message))
    len_sent = s.send(message)

    # Receive a response
    response = s.recv(1024)
    print('Received: {!r}'.format(response))

    # Clean up
    server.shutdown()
    s.close()
    server.socket.close()

En este caso, el ID de proceso del hijo se incluye en la respuesta desde el servidor:

$ python3 socketserver_forking.py

Server loop running in process: 22599
Sending : b'Hello, world'
Received: b'22600: Hello, world'

Ver también

  • Standard library documentation for socketserver
  • socket – Comunicación de red de bajo nivel
  • select – Herramientas de E/S asíncronas de bajo nivel
  • asyncio – E/S asíncronas, bucle de eventos y herramientas de concurrencia
  • SimpleXMLRPCServer – Servidor XML-RPC construido usando socketserver.
  • Unix Network Programming, Volume 1: The Sockets Networking API, 3/E Por W. Richard Stevens, Bill Fenner, y Andrew M. Rudoff. Publicado por Addison-Wesley Professional, 2004. ISBN-10: 0131411551
  • Foundations of Python Network Programminng, 3/E Por Brandon Rhodes y John Goerzen. Publicado por Apress, 2014. ISBN-10: 1430258543