qmsk/net/transport/socket.py
author Tero Marttila <terom@fixme.fi>
Sun, 23 Aug 2009 22:59:40 +0300
changeset 37 14db3fe42b6c
parent 33 c71de00715d6
child 45 bb49bf8222ed
permissions -rw-r--r--
move address-family from tcp/socket interface to endpoint interface. The address family of a socket is strictly a property of the address passed to it
"""
    Socket implementation helpers
"""

from qmsk.net.socket import socket
from qmsk.net.socket.constants import *

class SocketError (Exception) :
    """
        Base class of errors raised by the Socket classes.
    """

class SocketBindAddrinfoError (SocketError) :
    """
        The socket was unable to socket()+bind() to the given addrinfo.
    """

    def __init__ (self, addrinfo, error) :
        """
            addrinfo            - the addrinfo we tried to use
            error               - the resulting error
        """

        self.addrinfo = addrinfo
        self.error = error

    def __str__ (self) :
        return "Unable to bind() to %s: %s" % (self.addrinfo, self.error)

class SocketBindEndpointError (SocketError) :
    """
        The socket was unable to bind() to the any of the given endpoint's addresses.
    """

    def __init__ (self, endpoint, errors) :
        """
            endpoint            - the endpoint we tried to bind to
            errors              - a sequence of ServiceBindAddrinfoErrors describing the failed bind attempts.
        """

        self.endpoint = endpoint
        self.errors = errors

    def __str__ (self) :
        # XXX: too verbose?
        return "Unable to bind() to any addresses on endpoint %s:\n%s" % (self.endpoint, "\n".join(
            "\t%s" % (error, ) for error in self.errors
        ))


class Base (object) :
    """
        Base class for all other socket-related classes, contains the underlying socket object.
    """

    # the underlying socket object
    sock = None
    
    def _init_sock (self, sock) :
        """
            Initialize with the given pre-existing socket.
        """

        self.sock = sock


class Common (Base) :
    """
        Common operations for Client/Service
    """

    # default socktype
    _SOCKTYPE = 0

    @classmethod
    def _socket (cls, family, socktype = None, protocol = 0) :
        """
            Construct and return a new socket object using the given parameters.
        """

        if socktype is None :
            socktype = cls._SOCKTYPE

        return socket.socket(family, socktype, protocol)

    @classmethod
    def _bind_addrinfo (cls, ai) :
        """
            This will attempt to create a new socket and bind it, based on the given addrinfo, returning the new socket.

            Raises a ServiceBindAddrinfoError if this fails
        """

        try :
            # socket()
            sock = cls._socket(ai.family, ai.socktype, ai.protocol)

            # bind()
            sock.bind(ai.addr)

        # XXX: except socket.error as e :
        except OSError, error : 
            raise SocketBindAddrinfoError(ai, error)
           
        else :
            return sock

    @classmethod
    def _bind_endpoint (cls, endpoint, socktype = None, protocol=0) :
        """
            This will resolve the given endpoint, and attempt to create and bind a suitable socket and return it.

                endpoint        - local Endpoint to bind() to.
                socktype        - (optiona) socket type to use, defaults to _SOCKTYPE
                protocol        - (optional) specific protocol

            Raises a ServiceBindError if this is unable to create a bound socket.

            XXX: bind to all of the given endpoint's addresses instead of just one...?
        """
        
        errors = []
        
        if socktype is None :
            socktype = cls._SOCKTYPE
        
        # resolve the endpoint and try socket+bind
        for ai in endpoint.resolve(socktype, protocol, AI_PASSIVE) :
            try :
                # try to socket+bind this addrinfo
                sock = cls._bind_addrinfo(ai)
            
            except SocketBindAddrinfoError, error :
                # collect
                errors.append(error)
                
                # keep trying
                continue

            else :
                # got a working socket :)
                return sock
       
        else :
            # no suitable address found :(
            raise SocketBindEndpointError(endpoint, errors)
    
    def _init_endpoint (self, endpoint, socktype = None, protocol = 0, family = None) :
        """
            Initialize this socket by constructing a new socket with the given parameters, bound to the given endpoint,
            if given. If no endpoint is given, this simply creates a socket with the given settings and does not bind
            it anywhere.
        """
        
        if endpoint is not None :
            # create a suitable socket bound to a the given endpoint
            self.sock = self._bind_endpoint(endpoint, socktype, protocol)
        
        else :
            assert family

            # simply create a socket
            self.sock = self._socket(family, socktype, protocol)

class Service (Common) :
    """
        Listener socket
    """

    def _listen (self, backlog) :
        """
            Puts this socket into listen() mode with the given backlog.
        """
        
        self.sock.listen(backlog)

class SocketConnectAddrinfoError (SocketError) :
    """
        The socket was unable to socket()+connect() to the given addrinfo.
    """

    def __init__ (self, addrinfo, error) :
        """
            addrinfo            - the addrinfo we tried to use
            error               - the resulting error
        """

        self.addrinfo = addrinfo
        self.error = error

    def __str__ (self) :
        return "Unable to connect() to %s: %s" % (self.addrinfo, self.error)

class SocketConnectEndpointError (SocketError) :
    """
        The socket was unable to connect() to the any of the given endpoint's addresses.
    """

    def __init__ (self, endpoint, errors) :
        """
            endpoint            - the endpoint we tried to connect to
            errors              - a sequence of ServiceBindAddrinfoErrors describing the failed connect attempts.
        """

        self.endpoint = endpoint
        self.errors = errors

    def __str__ (self) :
        # XXX: too verbose?
        return "Unable to connect() to any addresses on endpoint %s:\n%s" % (self.endpoint, "\n".join(
            "\t%s" % (error, ) for error in self.errors
        ))


class Client (Common) :
    """
        Connecting socket
    """

    @classmethod
    def _connect_sock_addr (cls, sock, addr) :
        """
            Attempt to connect the given socket to the given address.
        """

        sock.connect(addr)

    @classmethod
    def _connect_sock_addrinfo (cls, sock, ai) :
        """
            Attempt to connect the given socket to the given addrinfo's address.
        """

        try :
            cls._connect_sock_addr(sock, ai.addr)

        # XXX: except socket.error as e :
        except OSError, error :
            raise SocketConnectAddrinfoError(ai, error)

    @classmethod
    def _connect_addrinfo (cls, ai) :
        """
            Attempt to create a socket and connect it based on the given addrinfo, returning the new socket is succesfull.
        """

        try :
            # socket()
            sock = cls._socket(ai.family, ai.socktype, ai.protocol)

        # XXX: except socket.error as e :
        except OSError, error : 
            raise SocketConnectAddrinfoError(ai, error)


        # try and connect() it
        cls._connect_sock_addrinfo(sock, ai)
        
        # return once succesfully
        return sock

    @classmethod
    def _connect_sock_endpoint (cls, sock, endpoint, socktype = None, protocol = 0) :
        """
            Connect this socket to the given remote endpoint, using the given parameters to resolve the endpoint.
        """
        
        errors = []
        
        if socktype is None :
            socktype = cls._SOCKTYPE

        # resolve the endpoint and try socket+bind
        for ai in endpoint.resolve(socktype, protocol) :
            try :
                # try to connect the socket to this addrinfo
                cls._connect_sock_addrinfo(sock, ai)
            
            except SocketConnectAddrinfoError, error :
                # collect
                errors.append(error)
                
                # keep trying
                continue

            else :
                # got a working socket :)
                return

        else :
            # no suitable address found :(
            raise SocketConnectEndpointError(endpoint, errors)
    
    @classmethod
    def _connect_endpoint (cls, endpoint, socktype = None, protocol = 0) :
        """
            Create a new socket and connect it to the given remote endpoint, using the given parameters to resolve the
            endpoint.
        """

        errors = []
        
        if socktype is None :
            socktype = cls._SOCKTYPE

        # resolve the endpoint and try socket+bind
        for ai in endpoint.resolve(socktype, protocol) :
            try :
                # try to socket+connect this addrinfo
                sock = cls._connect_addrinfo(ai)
            
            except SocketConnectAddrinfoError, error :
                # collect
                errors.append(error)
                
                # keep trying
                continue

            else :
                # got a working socket :)
                return sock
       
        else :
            # no suitable address found :(
            raise SocketConnectEndpointError(endpoint, errors)

    def _init_connect_endpoint (self, endpoint, socktype = None, protocol = 0):
        """
            If we already have an existing socket, connect it to the given endpoint, otherwise try and connect to the
            given endpoint with a new socket.

            There is a subtle difference here, because if we have e.g. an IPv4 socket and try and connect it to an
            endpoint with both IPv6 and IPv4 addresses, we will try to connect to an IPv6 address using the IPv4 socket,
            and then to the IPv4 address using the IPv6 socket.
            
            If we do not yet have a socket, then we will attempt to connect to the IPv6 address using an IPv6 socket,
            and to the IPv4 address using an IPv4 socket.
        """

        if self.socket :
            # connect with existing socket
            self._connect_sock_endpoint(self.socket, endpoint, socktype, protocol)

        else :
            # connect with new socket
            self._connect_endpoint(endpoint, socktype, protocol)

class Stream (Base) :
    """
        Unbuffered byte-stream interface.
    """
   
    def read (self, iov) :
        return self.sock.read(iov)

    def readv (self, iovecs) :
        return self.sock.readv(iovecs)

    def write (self, buf) :
        return self.sock.write(buf)

    def writev (self, iovecs) :
        return self.sock.writev(iovecs)

    def close (self) :
        self.sock.close()

    def abort (self) :
        # XXX: SO_LINGER magic

        raise NotImplementedError()