123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- #!/usr/bin/env python
- # -*- coding: UTF-8 -*-
-
- """
- tcp_socket (TCP Socket)
- =======================
-
- **Author:**
-
- * Dirk Alders <sudo-dirk@mount-mockery.de>
-
- **Description:**
-
- This Module supports a client/ server tcp socket connection.
-
- **Submodules:**
-
- * :class:`tcp_socket.tcp_client`
- * :class:`tcp_socket.tcp_client_stp`
- * :class:`tcp_socket.tcp_server`
- * :class:`tcp_socket.tcp_server_stp`
-
- **Unittest:**
-
- See also the :download:`unittest <tcp_socket/_testresults_/unittest.pdf>` documentation.
-
- **Module Documentation:**
-
- """
- __DEPENDENCIES__ = ['stringtools', 'task', ]
-
- import stringtools
- import task
-
- import logging
- import socket
- import time
-
- try:
- from config import APP_NAME as ROOT_LOGGER_NAME
- except ImportError:
- ROOT_LOGGER_NAME = 'root'
-
- logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
-
- __DESCRIPTION__ = """The Module {\\tt %s} is designed to help with client / server tcp socket connections.
- For more Information read the documentation.""" % __name__.replace('_', '\_')
- """The Module Description"""
- __INTERPRETER__ = (2, 3)
- """The Tested Interpreter-Versions"""
-
-
- class tcp_base(object):
- """
- This is the base class for other classes in this module.
-
- :param host: The host IP for the TCP socket functionality
- :type host: str
- :param port: The port for the TCP socket functionality
- :type port: int
- :param channel_name: The name for the logging channel
- :type channel_name: str
-
- .. note:: This class is not designed for direct usage.
- """
- DEFAULT_CHANNEL_NAME = 'all_others'
-
- RX_LENGTH = 0xff
- COM_TIMEOUT = 0.5
- IS_CLIENT = False
-
- def __init__(self, host, port, channel_name=None, rx_tx_log_lvl=logging.INFO):
- self.host = host
- self.port = port
- self.init_channel_name(channel_name)
- self.__rx_tx_log_lvl__ = rx_tx_log_lvl
- self.__socket__ = None
- self.__data_available_callback__ = None
- self.__supress_data_available_callback__ = False
- self.__connect_callback__ = None
- self.__disconnect_callback__ = None
- self.__clean_receive_buffer__()
- self.__connection__ = None
- self.__listening_message_displayed__ = False
- self.__client_address__ = None
- self.__queue__ = task.threaded_queue()
- self.__queue__.enqueue(5, self.__receive_task__)
- self.__queue__.run()
-
- def __call_data_available_callback__(self):
- if len(self.__receive_buffer__) > 0 and not self.__supress_data_available_callback__ and self.__data_available_callback__ is not None:
- self.__supress_data_available_callback__ = True
- self.__data_available_callback__(self)
- self.__supress_data_available_callback__ = False
-
- def __clean_receive_buffer__(self):
- self.logger.debug("%s Cleaning up receive-buffer", self.__log_prefix__())
- self.__receive_buffer__ = b''
-
- def __connection_lost__(self):
- self.__listening_message_displayed__ = False
- self.__connection__.close()
- self.__connection__ = None
- self.__client_address__ = None
- self.logger.info('%s Connection lost...', self.__log_prefix__())
- if self.__disconnect_callback__ is not None:
- self.__disconnect_callback__()
-
- def __del__(self):
- self.close()
-
- def __log_prefix__(self):
- return 'comm-client:' if self.IS_CLIENT else 'comm-server:'
-
- def __receive_task__(self, queue_inst):
- if self.__connection__ is not None:
- try:
- data = self.__connection__.recv(self.RX_LENGTH)
- except socket.error as e:
- if e.errno != 11:
- raise
- else:
- time.sleep(.05)
- else:
- if len(data) > 0:
- self.logger.log(self.__rx_tx_log_lvl__, '%s RX <- "%s"', self.__log_prefix__(), stringtools.hexlify(data))
- self.__receive_buffer__ += data
- else:
- self.__connection_lost__()
- self.__call_data_available_callback__()
- else:
- self.__connect__()
- queue_inst.enqueue(5, self.__receive_task__)
-
- def client_address(self):
- """
- This method returns the address of the connected client.
-
- :return: The client address.
- :rtype: str
- """
- return self.__client_address__[0]
-
- def close(self):
- """
- This method closes the connected communication channel, if exists.
- """
- self.__queue__.stop()
- self.__queue__.join()
- if self.__connection__ is not None:
- self.__connection_lost__()
- if self.__socket__ is not None:
- self.__socket__.close()
-
- def init_channel_name(self, channel_name=None):
- """
- With this Method, the channel name for logging can be changed.
-
- :param channel_name: The name for the logging channel
- :type channel_name: str
- """
- if channel_name is None:
- self.logger = logger.getChild(self.DEFAULT_CHANNEL_NAME)
- else:
- self.logger = logger.getChild(channel_name)
-
- def is_connected(self):
- """
- With this Method the connection status can be identified.
-
- :return: True, if a connection is established, otherwise False.
- :rtype: bool
- """
- return self.__connection__ is not None
-
- def receive(self, timeout=1, num=None):
- """
- This method returns received data.
-
- :param timeout: The timeout for receiving data (at least after the timeout the method returns data or None).
- :type timeout: float
- :param num: the number of bytes to receive (use None to get all available data).
- :type num: int
- :return: The received data.
- :rtype: bytes
- """
- rv = None
- if self.__connection__ is not None:
- tm = time.time()
- while (num is not None and len(self.__receive_buffer__) < num) or (num is None and len(self.__receive_buffer__) < 1):
- if self.__connection__ is None:
- return None
- if time.time() > tm + timeout:
- self.logger.warning('%s TIMEOUT (%ss): Not enough data in buffer. Requested %s and buffer size is %d.', self.__log_prefix__(), repr(timeout), repr(num or 'all'), len(self.__receive_buffer__))
- return None
- time.sleep(0.05)
- if num is None:
- rv = self.__receive_buffer__
- self.__clean_receive_buffer__()
- else:
- rv = self.__receive_buffer__[:num]
- self.__receive_buffer__ = self.__receive_buffer__[num:]
- return rv
-
- def register_callback(self, callback):
- """
- This method stores the callback which is executed, if data is available. You need to execute :func:`receive` of this instance
- given as first argument.
-
- :param callback: The callback which will be executed, when data is available.
- :type callback:
- """
- self.__data_available_callback__ = callback
-
- def register_connect_callback(self, callback):
- """
- This method stores the callback which is executed, if a connection is established.
-
- :param callback: The callback which will be executed, when a connection is established.
- :type callback:
- """
- self.__connect_callback__ = callback
-
- def register_disconnect_callback(self, callback):
- """
- This method stores the callback which is executed, after the connection is lost.
-
- :param callback: The callback which will be executed, after the connection is lost.
- :type callback:
- """
- self.__disconnect_callback__ = callback
-
- def send(self, data, timeout=1):
- """
- This method sends data via the initiated communication channel.
-
- :param data: The data to be send over the communication channel.
- :type data: bytes
- :param timeout: The timeout for sending data (e.g. time to establish new connection).
- :type timeout: float
- :return: True if data had been sent, otherwise False.
- :rtype: bool
- """
- tm = time.time()
- while time.time() - tm < timeout:
- if self.__connection__ is not None:
- try:
- self.__connection__.sendall(data)
- except BlockingIOError:
- time.sleep(.1) # try again till timeout exceeds
- except (BrokenPipeError, ConnectionResetError) as e:
- self.logger.exception('%s Exception while sending data', self.__log_prefix__())
- self.__connection_lost__()
- return False
- else:
- self.logger.log(self.__rx_tx_log_lvl__, '%s TX -> "%s"', self.__log_prefix__(), stringtools.hexlify(data))
- return True
- else:
- time.sleep(.1) # give some time to establish the connection
- self.logger.warning('%s Cound NOT send -> "%s"', self.__log_prefix__(), stringtools.hexlify(data))
- return False
-
-
- class tcp_server(tcp_base):
- """
- This class creates a tcp-server for transfering a serial stream of bytes (characters). See also parent :class:`tcp_base`.
-
- :param host: The host IP for the TCP socket functionality
- :type host: str
- :param port: The port for the TCP socket functionality
- :type port: int
- :param channel_name: The name for the logging channel
- :type channel_name: str
-
- .. note:: You need a :class:`tcp_client` to communicate with the server.
-
- **Example:**
-
- .. literalinclude:: tcp_socket/_examples_/tcp_socket__tcp_server.py
-
- .. literalinclude:: tcp_socket/_examples_/tcp_socket__tcp_server.log
- """
- def __connect__(self):
- if self.__socket__ is None:
- # Create a TCP/IP socket
- self.__socket__ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # Bind the socket to the port
- server_address = (self.host, self.port)
- self.__socket__.bind(server_address)
- # Listen for incoming connections
- self.__socket__.listen(1)
- self.__socket__.settimeout(self.COM_TIMEOUT)
- self.__socket__.setblocking(False)
- if not self.__listening_message_displayed__:
- self.logger.info('%s Server listening to %s:%d', self.__log_prefix__(), self.host, self.port)
- self.__listening_message_displayed__ = True
- try:
- self.__connection__, self.__client_address__ = self.__socket__.accept()
- except socket.error as e:
- if e.errno != 11:
- raise
- else:
- time.sleep(.05)
- else:
- self.logger.info('%s Connection established... (from %s:%s)', self.__log_prefix__(), self.client_address(), self.port)
- self.__clean_receive_buffer__()
- self.__connection__.setblocking(False)
- if self.__connect_callback__ is not None:
- self.__connect_callback__()
-
-
- class tcp_client(tcp_base):
- """
- This class creates a tcp-client for transfering a serial stream of bytes (characters). See also parent :class:`tcp_base`.
-
- :param host: The host IP for the TCP socket functionality
- :type host: str
- :param port: The port for the TCP socket functionality
- :type port: int
- :param channel_name: The name for the logging channel
- :type channel_name: str
-
- .. note:: You need a running :class:`tcp_server` listening at the given IP and Port to be able to communicate.
-
- **Example:**
-
- .. literalinclude:: tcp_socket/_examples_/tcp_socket__tcp_client.py
-
- .. literalinclude:: tcp_socket/_examples_/tcp_socket__tcp_client.log
- """
- IS_CLIENT = True
-
- def __connect__(self):
- if self.__socket__ is None:
- # Create a TCP/IP socket
- self.__socket__ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.__socket__.setblocking(False)
- # Connect the socket to the port where the server is listening
- try:
- self.__socket__.connect((self.host, self.port))
- except socket.error as e:
- if e.errno == 9:
- self.__socket__.close()
- elif e.errno != 115 and e.errno != 111 and e.errno != 114:
- raise
- else:
- self.__connection__ = None
- time.sleep(.05)
- else:
- self.logger.info('%s Connection established... (to %s:%s)', self.__log_prefix__(), self.host, self.port)
- self.__clean_receive_buffer__()
- self.__connection__ = self.__socket__
- if self.__connect_callback__ is not None:
- self.__connect_callback__()
-
- def __connection_lost__(self):
- self.__socket__ = None
- tcp_base.__connection_lost__(self)
-
- def reconnect(self):
- self.__connect__()
-
-
- class tcp_base_stp(tcp_base):
- """
- This is the base class for other classes in this module. See also parent :class:`tcp_base`.
-
- :param host: The host IP for the TCP socket functionality
- :type host: str
- :param port: The port for the TCP socket functionality
- :type port: int
- :param channel_name: The name for the logging channel
- :type channel_name: str
-
- .. note:: This class is not designed for direct usage.
- """
-
- def __init__(self, host, port, channel_name=None):
- tcp_base.__init__(self, host, port, channel_name=channel_name, rx_tx_log_lvl=logging.DEBUG)
- self.__stp__ = stringtools.stp.stp()
-
- def __clean_receive_buffer__(self):
- self.logger.debug("%s Cleaning up receive-buffer", self.__log_prefix__())
- self.__receive_buffer__ = []
-
- def __receive_task__(self, queue_inst):
- if self.__connection__ is not None:
- try:
- data = self.__connection__.recv(self.RX_LENGTH)
- except socket.error as e:
- if e.errno == 104:
- self.__connection_lost__()
- elif e.errno != 11:
- raise
- else:
- time.sleep(.05)
- else:
- if len(data) > 0:
- self.logger.log(self.__rx_tx_log_lvl__, '%s RX <- "%s"', self.__log_prefix__(), stringtools.hexlify(data))
- content = self.__stp__.process(data)
- for msg in content:
- self.logger.info('%s RX <- "%s"', self.__log_prefix__(), stringtools.hexlify(msg))
- self.__receive_buffer__.append(msg)
- else:
- self.__connection_lost__()
- self.__call_data_available_callback__()
- else:
- self.__connect__()
- queue_inst.enqueue(5, self.__receive_task__)
-
- def receive(self, timeout=1):
- """
- This method returns one received messages via the initiated communication channel.
-
- :param timeout: The timeout for receiving data (at least after the timeout the method returns data or None).
- :type timeout: float
- :return: The received data.
- :rtype: bytes
- """
- try:
- return tcp_base.receive(self, timeout=timeout, num=1)[0]
- except TypeError:
- return None
-
- def send(self, data, timeout=1):
- """
- This method sends one stp message via the initiated communication channel.
-
- :param data: The message to be send over the communication channel.
- :type data: bytes
- :param timeout: The timeout for sending data (e.g. time to establish new connection).
- :type timeout: float
- :return: True if data had been sent, otherwise False.
- :rtype: bool
- """
- if tcp_base.send(self, stringtools.stp.build_frame(data), timeout=timeout):
- self.logger.info('%s TX -> "%s"', self.__log_prefix__(), stringtools.hexlify(data))
- return True
- else:
- return False
-
-
- class tcp_server_stp(tcp_server, tcp_base_stp):
- """
- This class creates a tcp-server for transfering a message. The bytes will be packed on send and unpacked on receive. See also parents :class:`tcp_server` and :class:`tcp_base_stp`.
- See :mod:`stringtools.stp` for more information on packing and unpacking.
-
- :param host: The host IP for the TCP socket functionality
- :type host: str
- :param port: The port for the TCP socket functionality
- :type port: int
- :param channel_name: The name for the logging channel
- :type channel_name: str
-
- .. note:: You need a :class:`tcp_client_stp` to communicate with the server.
-
- **Example:**
-
- .. literalinclude:: tcp_socket/_examples_/tcp_socket__stp_server.py
-
- .. literalinclude:: tcp_socket/_examples_/tcp_socket__stp_server.log
- """
- pass
-
-
- class tcp_client_stp(tcp_client, tcp_base_stp):
- """
- This class creates a tcp-client for transfering a message. The bytes will be packed on send and unpacked on receive. See also parents :class:`tcp_client` and :class:`tcp_base_stp`.
- See :mod:`stringtools.stp` for more information on packing and unpacking.
-
- :param host: The host IP for the TCP socket functionality
- :type host: str
- :param port: The port for the TCP socket functionality
- :type port: int
- :param channel_name: The name for the logging channel
- :type channel_name: str
-
- .. note:: You need a running :class:`tcp_server_stp` listening at the given IP and Port to be able to communicate.
-
- **Example:**
-
- .. literalinclude:: tcp_socket/_examples_/tcp_socket__stp_client.py
-
- .. literalinclude:: tcp_socket/_examples_/tcp_socket__stp_client.log
- """
- pass
|