Python Library TCP Socket
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

__init__.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. """
  4. tcp_socket (TCP Socket)
  5. =======================
  6. **Author:**
  7. * Dirk Alders <sudo-dirk@mount-mockery.de>
  8. **Description:**
  9. This Module supports a client/ server tcp socket connection.
  10. **Submodules:**
  11. * :class:`tcp_socket.tcp_client`
  12. * :class:`tcp_socket.tcp_client_stp`
  13. * :class:`tcp_socket.tcp_server`
  14. * :class:`tcp_socket.tcp_server_stp`
  15. **Unittest:**
  16. See also the :download:`unittest <tcp_socket/_testresults_/unittest.pdf>` documentation.
  17. **Module Documentation:**
  18. """
  19. __DEPENDENCIES__ = ['stringtools', 'task', ]
  20. import stringtools
  21. import task
  22. import logging
  23. import socket
  24. import time
  25. try:
  26. from config import APP_NAME as ROOT_LOGGER_NAME
  27. except ImportError:
  28. ROOT_LOGGER_NAME = 'root'
  29. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  30. __DESCRIPTION__ = """The Module {\\tt %s} is designed to help with client / server tcp socket connections.
  31. For more Information read the documentation.""" % __name__.replace('_', '\_')
  32. """The Module Description"""
  33. __INTERPRETER__ = (2, 3)
  34. """The Tested Interpreter-Versions"""
  35. class tcp_base(object):
  36. """
  37. This is the base class for other classes in this module.
  38. :param host: The host IP for the TCP socket functionality
  39. :type host: str
  40. :param port: The port for the TCP socket functionality
  41. :type port: int
  42. :param channel_name: The name for the logging channel
  43. :type channel_name: str
  44. .. note:: This class is not designed for direct usage.
  45. """
  46. DEFAULT_CHANNEL_NAME = 'all_others'
  47. RX_LENGTH = 0xff
  48. COM_TIMEOUT = 0.5
  49. IS_CLIENT = False
  50. def __init__(self, host, port, channel_name=None, rx_tx_log_lvl=logging.INFO):
  51. self.host = host
  52. self.port = port
  53. self.init_channel_name(channel_name)
  54. self.__rx_tx_log_lvl__ = rx_tx_log_lvl
  55. self.__socket__ = None
  56. self.__data_available_callback__ = None
  57. self.__supress_data_available_callback__ = False
  58. self.__connect_callback__ = None
  59. self.__disconnect_callback__ = None
  60. self.__clean_receive_buffer__()
  61. self.__connection__ = None
  62. self.__listening_message_displayed__ = False
  63. self.__client_address__ = None
  64. self.__queue__ = task.threaded_queue()
  65. self.__queue__.enqueue(5, self.__receive_task__)
  66. self.__queue__.run()
  67. def __call_data_available_callback__(self):
  68. if len(self.__receive_buffer__) > 0 and not self.__supress_data_available_callback__ and self.__data_available_callback__ is not None:
  69. self.__supress_data_available_callback__ = True
  70. self.__data_available_callback__(self)
  71. self.__supress_data_available_callback__ = False
  72. def __clean_receive_buffer__(self):
  73. self.logger.debug("%s Cleaning up receive-buffer", self.__log_prefix__())
  74. self.__receive_buffer__ = b''
  75. def __connection_lost__(self):
  76. self.__listening_message_displayed__ = False
  77. self.__connection__.close()
  78. self.__connection__ = None
  79. self.__client_address__ = None
  80. self.logger.info('%s Connection lost...', self.__log_prefix__())
  81. if self.__disconnect_callback__ is not None:
  82. self.__disconnect_callback__()
  83. def __del__(self):
  84. self.close()
  85. def __log_prefix__(self):
  86. return 'comm-client:' if self.IS_CLIENT else 'comm-server:'
  87. def __receive_task__(self, queue_inst):
  88. if self.__connection__ is not None:
  89. try:
  90. data = self.__connection__.recv(self.RX_LENGTH)
  91. except socket.error as e:
  92. if e.errno != 11:
  93. raise
  94. else:
  95. time.sleep(.05)
  96. else:
  97. if len(data) > 0:
  98. self.logger.log(self.__rx_tx_log_lvl__, '%s RX <- "%s"', self.__log_prefix__(), stringtools.hexlify(data))
  99. self.__receive_buffer__ += data
  100. else:
  101. self.__connection_lost__()
  102. self.__call_data_available_callback__()
  103. else:
  104. self.__connect__()
  105. queue_inst.enqueue(5, self.__receive_task__)
  106. def client_address(self):
  107. """
  108. This method returns the address of the connected client.
  109. :return: The client address.
  110. :rtype: str
  111. """
  112. return self.__client_address__[0]
  113. def close(self):
  114. """
  115. This method closes the connected communication channel, if exists.
  116. """
  117. self.__queue__.stop()
  118. self.__queue__.join()
  119. if self.__connection__ is not None:
  120. self.__connection_lost__()
  121. if self.__socket__ is not None:
  122. self.__socket__.close()
  123. def init_channel_name(self, channel_name=None):
  124. """
  125. With this Method, the channel name for logging can be changed.
  126. :param channel_name: The name for the logging channel
  127. :type channel_name: str
  128. """
  129. if channel_name is None:
  130. self.logger = logger.getChild(self.DEFAULT_CHANNEL_NAME)
  131. else:
  132. self.logger = logger.getChild(channel_name)
  133. def is_connected(self):
  134. """
  135. With this Method the connection status can be identified.
  136. :return: True, if a connection is established, otherwise False.
  137. :rtype: bool
  138. """
  139. return self.__connection__ is not None
  140. def receive(self, timeout=1, num=None):
  141. """
  142. This method returns received data.
  143. :param timeout: The timeout for receiving data (at least after the timeout the method returns data or None).
  144. :type timeout: float
  145. :param num: the number of bytes to receive (use None to get all available data).
  146. :type num: int
  147. :return: The received data.
  148. :rtype: bytes
  149. """
  150. rv = None
  151. if self.__connection__ is not None:
  152. tm = time.time()
  153. while (num is not None and len(self.__receive_buffer__) < num) or (num is None and len(self.__receive_buffer__) < 1):
  154. if self.__connection__ is None:
  155. return None
  156. if time.time() > tm + timeout:
  157. 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__))
  158. return None
  159. time.sleep(0.05)
  160. if num is None:
  161. rv = self.__receive_buffer__
  162. self.__clean_receive_buffer__()
  163. else:
  164. rv = self.__receive_buffer__[:num]
  165. self.__receive_buffer__ = self.__receive_buffer__[num:]
  166. return rv
  167. def register_callback(self, callback):
  168. """
  169. This method stores the callback which is executed, if data is available. You need to execute :func:`receive` of this instance
  170. given as first argument.
  171. :param callback: The callback which will be executed, when data is available.
  172. :type callback:
  173. """
  174. self.__data_available_callback__ = callback
  175. def register_connect_callback(self, callback):
  176. """
  177. This method stores the callback which is executed, if a connection is established.
  178. :param callback: The callback which will be executed, when a connection is established.
  179. :type callback:
  180. """
  181. self.__connect_callback__ = callback
  182. def register_disconnect_callback(self, callback):
  183. """
  184. This method stores the callback which is executed, after the connection is lost.
  185. :param callback: The callback which will be executed, after the connection is lost.
  186. :type callback:
  187. """
  188. self.__disconnect_callback__ = callback
  189. def send(self, data, timeout=1):
  190. """
  191. This method sends data via the initiated communication channel.
  192. :param data: The data to be send over the communication channel.
  193. :type data: bytes
  194. :param timeout: The timeout for sending data (e.g. time to establish new connection).
  195. :type timeout: float
  196. :return: True if data had been sent, otherwise False.
  197. :rtype: bool
  198. """
  199. tm = time.time()
  200. while time.time() - tm < timeout:
  201. if self.__connection__ is not None:
  202. try:
  203. self.__connection__.sendall(data)
  204. except BlockingIOError:
  205. time.sleep(.1) # try again till timeout exceeds
  206. except (BrokenPipeError, ConnectionResetError) as e:
  207. self.logger.exception('%s Exception while sending data', self.__log_prefix__())
  208. self.__connection_lost__()
  209. return False
  210. else:
  211. self.logger.log(self.__rx_tx_log_lvl__, '%s TX -> "%s"', self.__log_prefix__(), stringtools.hexlify(data))
  212. return True
  213. else:
  214. time.sleep(.1) # give some time to establish the connection
  215. self.logger.warning('%s Cound NOT send -> "%s"', self.__log_prefix__(), stringtools.hexlify(data))
  216. return False
  217. class tcp_server(tcp_base):
  218. """
  219. This class creates a tcp-server for transfering a serial stream of bytes (characters). See also parent :class:`tcp_base`.
  220. :param host: The host IP for the TCP socket functionality
  221. :type host: str
  222. :param port: The port for the TCP socket functionality
  223. :type port: int
  224. :param channel_name: The name for the logging channel
  225. :type channel_name: str
  226. .. note:: You need a :class:`tcp_client` to communicate with the server.
  227. **Example:**
  228. .. literalinclude:: tcp_socket/_examples_/tcp_socket__tcp_server.py
  229. .. literalinclude:: tcp_socket/_examples_/tcp_socket__tcp_server.log
  230. """
  231. def __connect__(self):
  232. if self.__socket__ is None:
  233. # Create a TCP/IP socket
  234. self.__socket__ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  235. # Bind the socket to the port
  236. server_address = (self.host, self.port)
  237. self.__socket__.bind(server_address)
  238. # Listen for incoming connections
  239. self.__socket__.listen(1)
  240. self.__socket__.settimeout(self.COM_TIMEOUT)
  241. self.__socket__.setblocking(False)
  242. if not self.__listening_message_displayed__:
  243. self.logger.info('%s Server listening to %s:%d', self.__log_prefix__(), self.host, self.port)
  244. self.__listening_message_displayed__ = True
  245. try:
  246. self.__connection__, self.__client_address__ = self.__socket__.accept()
  247. except socket.error as e:
  248. if e.errno != 11:
  249. raise
  250. else:
  251. time.sleep(.05)
  252. else:
  253. self.logger.info('%s Connection established... (from %s:%s)', self.__log_prefix__(), self.client_address(), self.port)
  254. self.__clean_receive_buffer__()
  255. self.__connection__.setblocking(False)
  256. if self.__connect_callback__ is not None:
  257. self.__connect_callback__()
  258. class tcp_client(tcp_base):
  259. """
  260. This class creates a tcp-client for transfering a serial stream of bytes (characters). See also parent :class:`tcp_base`.
  261. :param host: The host IP for the TCP socket functionality
  262. :type host: str
  263. :param port: The port for the TCP socket functionality
  264. :type port: int
  265. :param channel_name: The name for the logging channel
  266. :type channel_name: str
  267. .. note:: You need a running :class:`tcp_server` listening at the given IP and Port to be able to communicate.
  268. **Example:**
  269. .. literalinclude:: tcp_socket/_examples_/tcp_socket__tcp_client.py
  270. .. literalinclude:: tcp_socket/_examples_/tcp_socket__tcp_client.log
  271. """
  272. IS_CLIENT = True
  273. def __connect__(self):
  274. if self.__socket__ is None:
  275. # Create a TCP/IP socket
  276. self.__socket__ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  277. self.__socket__.setblocking(False)
  278. # Connect the socket to the port where the server is listening
  279. try:
  280. self.__socket__.connect((self.host, self.port))
  281. except socket.error as e:
  282. if e.errno == 9:
  283. self.__socket__.close()
  284. elif e.errno != 115 and e.errno != 111 and e.errno != 114:
  285. raise
  286. else:
  287. self.__connection__ = None
  288. time.sleep(.05)
  289. else:
  290. self.logger.info('%s Connection established... (to %s:%s)', self.__log_prefix__(), self.host, self.port)
  291. self.__clean_receive_buffer__()
  292. self.__connection__ = self.__socket__
  293. if self.__connect_callback__ is not None:
  294. self.__connect_callback__()
  295. def __connection_lost__(self):
  296. self.__socket__ = None
  297. tcp_base.__connection_lost__(self)
  298. def reconnect(self):
  299. self.__connect__()
  300. class tcp_base_stp(tcp_base):
  301. """
  302. This is the base class for other classes in this module. See also parent :class:`tcp_base`.
  303. :param host: The host IP for the TCP socket functionality
  304. :type host: str
  305. :param port: The port for the TCP socket functionality
  306. :type port: int
  307. :param channel_name: The name for the logging channel
  308. :type channel_name: str
  309. .. note:: This class is not designed for direct usage.
  310. """
  311. def __init__(self, host, port, channel_name=None):
  312. tcp_base.__init__(self, host, port, channel_name=channel_name, rx_tx_log_lvl=logging.DEBUG)
  313. self.__stp__ = stringtools.stp.stp()
  314. def __clean_receive_buffer__(self):
  315. self.logger.debug("%s Cleaning up receive-buffer", self.__log_prefix__())
  316. self.__receive_buffer__ = []
  317. def __receive_task__(self, queue_inst):
  318. if self.__connection__ is not None:
  319. try:
  320. data = self.__connection__.recv(self.RX_LENGTH)
  321. except socket.error as e:
  322. if e.errno == 104:
  323. self.__connection_lost__()
  324. elif e.errno != 11:
  325. raise
  326. else:
  327. time.sleep(.05)
  328. else:
  329. if len(data) > 0:
  330. self.logger.log(self.__rx_tx_log_lvl__, '%s RX <- "%s"', self.__log_prefix__(), stringtools.hexlify(data))
  331. content = self.__stp__.process(data)
  332. for msg in content:
  333. self.logger.info('%s RX <- "%s"', self.__log_prefix__(), stringtools.hexlify(msg))
  334. self.__receive_buffer__.append(msg)
  335. else:
  336. self.__connection_lost__()
  337. self.__call_data_available_callback__()
  338. else:
  339. self.__connect__()
  340. queue_inst.enqueue(5, self.__receive_task__)
  341. def receive(self, timeout=1):
  342. """
  343. This method returns one received messages via the initiated communication channel.
  344. :param timeout: The timeout for receiving data (at least after the timeout the method returns data or None).
  345. :type timeout: float
  346. :return: The received data.
  347. :rtype: bytes
  348. """
  349. try:
  350. return tcp_base.receive(self, timeout=timeout, num=1)[0]
  351. except TypeError:
  352. return None
  353. def send(self, data, timeout=1):
  354. """
  355. This method sends one stp message via the initiated communication channel.
  356. :param data: The message to be send over the communication channel.
  357. :type data: bytes
  358. :param timeout: The timeout for sending data (e.g. time to establish new connection).
  359. :type timeout: float
  360. :return: True if data had been sent, otherwise False.
  361. :rtype: bool
  362. """
  363. if tcp_base.send(self, stringtools.stp.build_frame(data), timeout=timeout):
  364. self.logger.info('%s TX -> "%s"', self.__log_prefix__(), stringtools.hexlify(data))
  365. return True
  366. else:
  367. return False
  368. class tcp_server_stp(tcp_server, tcp_base_stp):
  369. """
  370. 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`.
  371. See :mod:`stringtools.stp` for more information on packing and unpacking.
  372. :param host: The host IP for the TCP socket functionality
  373. :type host: str
  374. :param port: The port for the TCP socket functionality
  375. :type port: int
  376. :param channel_name: The name for the logging channel
  377. :type channel_name: str
  378. .. note:: You need a :class:`tcp_client_stp` to communicate with the server.
  379. **Example:**
  380. .. literalinclude:: tcp_socket/_examples_/tcp_socket__stp_server.py
  381. .. literalinclude:: tcp_socket/_examples_/tcp_socket__stp_server.log
  382. """
  383. pass
  384. class tcp_client_stp(tcp_client, tcp_base_stp):
  385. """
  386. 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`.
  387. See :mod:`stringtools.stp` for more information on packing and unpacking.
  388. :param host: The host IP for the TCP socket functionality
  389. :type host: str
  390. :param port: The port for the TCP socket functionality
  391. :type port: int
  392. :param channel_name: The name for the logging channel
  393. :type channel_name: str
  394. .. note:: You need a running :class:`tcp_server_stp` listening at the given IP and Port to be able to communicate.
  395. **Example:**
  396. .. literalinclude:: tcp_socket/_examples_/tcp_socket__stp_client.py
  397. .. literalinclude:: tcp_socket/_examples_/tcp_socket__stp_client.log
  398. """
  399. pass