diff -r 12468e38227e -r 020c89baaa33 qmsk/net/transport/socket.py --- /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() + +