Browse Source

Initial tcp_socket implementation

master
Dirk Alders 5 years ago
parent
commit
62f13e2d87
1 changed files with 435 additions and 0 deletions
  1. 435
    0
      __init__.py

+ 435
- 0
__init__.py View File

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

Loading…
Cancel
Save