#!/usr/bin/env python # -*- coding: UTF-8 -*- """ tcp_socket (TCP Socket) ======================= **Author:** * Dirk Alders **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 ` 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: 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