qmsk/net/transport/socket.py
changeset 28 020c89baaa33
child 33 c71de00715d6
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/net/transport/socket.py	Fri Aug 21 00:30:06 2009 +0300
@@ -0,0 +1,359 @@
+"""
+    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
+    """
+
+    @classmethod
+    def _socket (self, family, socktype, protocol = 0) :
+        """
+            Construct and return a new socket object using the given parameters.
+        """
+
+        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 = self._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, family, socktype, 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.
+                family          - socket address family to use.
+                socktype        - socket type to use
+                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 = []
+        
+        # resolve the endpoint and try socket+bind
+        for ai in endpoint.getaddrinfo(family, 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, family, socktype, protocol = 0) :
+        """
+            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.
+        """
+
+        # create local socket
+        if endpoint :
+            # create a suitable socket bound to a the given endpoint
+            self.sock = self._bind_endpoint(endpoint, family, socktype, protocol)
+
+        else :
+            # create a suitable socket not bound to anything
+            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, family, socktype, protocol = 0) :
+        """
+            Connect this socket to the given remote endpoint, using the given parameters to resolve the endpoint.
+        """
+        
+        errors = []
+        
+        # resolve the endpoint and try socket+bind
+        for ai in endpoint.getaddrinfo(family, 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 (self, endpoint, family, socktype, protocol = 0) :
+        """
+            Create a new socket and connect it to the given remote endpoint, using the given parameters to resolve the
+            endpoint.
+        """
+
+        errors = []
+        
+        # resolve the endpoint and try socket+bind
+        for ai in endpoint.getaddrinfo(family, 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, family, socktype, 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, family, socktype, protocol)
+
+        else :
+            # connect with new socket
+            self._connect_endpoint(endpoint, family, 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()
+
+