summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
authorVasudev Kamath <vasudev@copyninja.info>2017-08-20 10:13:54 +0530
committerVasudev Kamath <vasudev@copyninja.info>2017-12-23 20:47:45 +0530
commitdfc3da83de3802f6c67313d238e459e0db780451 (patch)
tree8bf58772e229e2c7d1bfe841d94110dd9b5c6f0f /content
parente1b7293dcfff0972de92d52c1230dbde2deca1a3 (diff)
Added post on UDP iterator.
Diffstat (limited to 'content')
-rw-r--r--content/development/udp_server_as_iterator.rst183
1 files changed, 183 insertions, 0 deletions
diff --git a/content/development/udp_server_as_iterator.rst b/content/development/udp_server_as_iterator.rst
new file mode 100644
index 0000000..171e3bd
--- /dev/null
+++ b/content/development/udp_server_as_iterator.rst
@@ -0,0 +1,183 @@
+Writing a UDP Broadcast Receiver as Python Iterator
+####################################################
+
+:date: 2017-08-19 18:58 +0530
+:slug: udp-server-as-generator
+:tags: python, iterators, udp server
+:author: copyninja
+:summary: Post explaining my experiment of writing a UDP Broadcast Receiving
+ service as a Python iterator.
+
+I had to write a small Python application to listen for some broadcast message
+and process the message. This broadcast messages are actually sort of discovery
+messages to find some peers in a network. Writing a simple UDP Server to listen
+on a particular port was easy; but while designing an application I was
+wondering how can I plugin this server into my main code. There are 2
+possibility
+
+1. Use threading module of python to send the server code in back ground and
+ give it a callback to communicate the data to main thread.
+2. Periodically read some messages from server code and then dispose of server.
+
+I didn't like first approach because I need to pass a callback function and I
+some how will end up complicating code. Second approach sounded sane but I did
+want to make server more like *iterator*. I searched around to see if some one
+has attempted to write something similar, but did not find anything useful (may
+be my Googling skills aren't good enough). Anyway so I thought what is wrong in
+trying?. If it works then I'll be happy that I did something different :-).
+
+The first thing for making iterator in Python is having function `__iter__` and
+`__next__` defined in your class. For Python 2 iterator protocol wanted `next`
+to be defined instead of `__next__`. So for portable code you can define a
+`next` function which in return calls `__next__`.
+
+So here is my first shot at writing `BroadCastReceiver` class.
+
+.. code-block:: python
+
+ from socket import socket, AF_INET, SOCK_DGRAM
+
+
+ class BroadCastReceiver:
+
+ def __init__(self, port, msg_len=8192):
+ self.sock = socket(AF_INET, SOCK_DGRAM)
+ self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
+ self.sock.bind(('', port))
+ self.msg_len = msg_len
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ try:
+ addr, data = self.sock.recvfrom(self.msg_len)
+ return addr, data
+ except Exception as e:
+ print("Got exception trying to recv %s" % e)
+ raise StopIteration
+
+This version of code can be used in a `for` loop to read from socket UDP
+broadcasts. One problem will be that if no packet is received which might be due
+to disconnected network the loop will just block forever. So I had to modify the
+code slightly to add timeout parameter. So changed portion of code is below.
+
+.. code-block:: python
+
+ ...
+ def __init__(self, port, msg_len=8192, timeout=15):
+ self.sock = socket(AF_INET, SOCK_DGRAM)
+ self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
+ self.sock.settimeout(timeout)
+ self.sock.msg_len = msg_len
+ self.sock.bind(('', port))
+
+ ...
+
+So now if network is disconnected or no packet was received for `timeout` period
+we get a `socket.timeout` exception due to which `StopIteration` will be raised
+causing the `for` loop using server as iterator to exit. This avoids us just
+blocking our periodic code run forever when network is disconnected or no
+messages are received for long time. (may be due to connected to wrong network).
+
+Now every thing looks fine but only part is if we create the server object each
+time our periodic code is called we will have binding issue as we did not
+properly close the socket once iterator has stopped. So I added socket closing
+code in `__del__` function for the class. `__del__` will be called when garbage
+collector try to recollect object when it goes out of scope.
+
+.. code-block:: python
+
+ ...
+ def __del__(self):
+ self.sock.close()
+
+So the server can be used in `for` loop or by passing the object of server to
+`next` built-in function. Here are 2 examples.
+
+.. code-block:: python
+
+ r = BroadCastReceiver(5000, timeout=10)
+ count = 0
+ for (address, data) in r:
+ print('Got packet from %s: %s' % address, data)
+ count += 1
+ # do whatever you want with data
+ if count > 10:
+ break
+
+Here we use an counter variable to track iteration and after some iteration we
+exit for loop. Another way is use `for` loop with range of iteration like below.
+
+.. code-block:: python
+
+ r = BroadCastReceiver(5000, timeout=10)
+ for i in range(20):
+ try:
+ address, data = next(r)
+ # do whatever you want with data
+ except:
+ break
+
+Here an additional try block was needed inside the for loop to card call to
+`next`, this is to handle the timeout or other exception and exit the loop. In
+first case this is not needed as `StopIteration` is understood by `for`.
+
+Both use cases I described above are mostly useful when it is not critical to
+handle each and every packet (mostly peer discovery) and packets will always be
+sent. So if we miss some peers in one iteration we will still catch them in next
+iteration. We just need to make sure we provide big enough counter to catch most
+peers in each iteration.
+
+If its critical to receive each packet we can safely send this iterating logic
+to a separate thread which keeps receiving packets and process data as needed.
+
+For now I tried this pattern mostly with UDP protocol but I'm sure with some
+modification this can be used with TCP as well. I'll be happy to get feed back
+from Pythonistas out there on what you think of this approach. :-)
+
+Update
+======
+
+I got a suggestion from *Ryan Nowakowski* to make the server object as `context
+manager` and close the socket in `__exit__` as it can't be guaranteed that
+`__del__` will be called for objects which exists during interpreter exits. So I slightly modified the class to add `__enter__` and `__exit__` method like below and removed `__del__`
+
+.. code-block:: python
+
+ ...
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.sock.close()
+
+
+Usage pattern is slightly modified because of this and we need to use `with`
+statement while creating object.
+
+.. code-block:: python
+
+ with BroadCastReceiver(2000) as r:
+ # use server object as you wish
+ ...
+
+It is also possible to cleanly close socket without adding context manager that
+is adding `finally` statement to our `try` and `except` block in `__next__`. The
+modified code without adding context manager looks like below.
+
+.. code-block:: python
+
+ def __next__(self):
+ try:
+ addr, data = self.sock.recvfrom(self.msg_len)
+ return addr, data
+ except Exception as e:
+ print("Got exception trying to recv %s" % e)
+ raise StopIteration
+ finally:
+ self.sock.close()
+
+When we raise `StopIteration` again from except block, it will be temporarily
+saved and `finally` block is executed which will now close the socket.