Writing Selectors
00:00 In the previous lesson, I introduced you to selectors. In this lesson, I’ll show you how to write one in practice.
00:07 A selector is a single-threaded way of multiplexing I/O. It works by using read and write events against registered callbacks, periodically sending events to each registered handler.
00:18 Let’s see some code that uses selectors to handle multiple socket connections.
00:24
This is multibork.py
. At its heart, it’s still a bork server, but this one multiplexes between multiple socket connections. To do that, you need the selectors
module, and here I’m creating a new selector using DefaultSelector()
so Python chooses just what kind of multiplexing algorithm works on my machine.
00:44 The simplest of selectors just polls repeatedly, but if your OS supports it, having queues that only trigger the code when data changes is much more efficient.
00:53 Thankfully, you don’t have to figure out whether or not your OS can do this.
00:58 When you register a callback for a selector, you can choose which events you’ll be interested in. For our sockets, I want both read and write events, so I’m sticking this in a constant.
01:09 I’m going to scroll down to the bottom. I want to start with the selector creation code and then I’ll come back to the actual callbacks in a moment.
01:16 This is the socket creation code like you’ve seen before. It creates the socket, binds to the address, and then listens. Remember way back when I kind of quickly said, listen and accept have to be different things, but was wishy-washy about why?
01:30 Well, this is why. Our selector listens for the connections, but the acceptance code needs to be inside the initializer. For multiplexing to work, nobody can block. Blocking prevents the multiplexer from switching to the next event.
01:44 Once I’ve got the socket set up, the first registration is for initialization, and it’s only for read events. I’m associating no data with this socket, and you’ll see why in a second.
01:56 I’ve wrapped my selector code in a try-except block so that no matter what goes wrong, I can properly close the selector. I don’t want any of those TIME-WAIT sockets hanging around.
02:07
And here’s the core part. The select()
method on the selector returns one or more events. Each event has an object known as a key and an event mask.
02:18
The key includes the data associated with the connection during registration, and the event mask has its bits turned on for read and write events. If the data is None
, then this is the first registration, which is the bouncer at the door, accepting new requests.
02:33
In this case, I call a function called start_connection()
, passing in the file object being multiplexed, which for us is the socket. If there is data associated with the key, then this is an established connection.
02:45
So instead, I call the process()
function on it.
02:49 Okay, now let me scroll back up and let’s look at these two functions.
02:55
Remember, this gets called on the first read when a new client connects. The accept()
call establishes the connection, returning the actual socket connection and associated address.
03:06 This is a new thing that itself can be multiplexed. Before doing anything else, I set this socket so it’s non-blocking. I don’t want it to gum up the works.
03:16
Then I need to create some sort of state object. If you haven’t seen the SimpleNamespace
typing object before, it’s a shortcut for a dictionary-like thing, which in this case stores the address of the socket and an empty byte string as a buffer.
03:30 Now that I’ve created the data, I register the socket and associate it with this data and ask for it to listen for read and write events.
03:40
Remember, this function only gets called once when a new socket connects. This function registers a new thing with the multiplexer, which from then on, will call the process()
function as it has data associated with it now. And this is the process()
function. The file object inside the key is our actual socket, so I’m assigning it to a variable for clarity in the code.
04:05 Just below that is our state associated with the connection. Again, I’m just renaming it for convenience sake. If the process had its read flag on, then I read data from the socket.
04:17 If there was data, then I put it in our buffer, and if not, then it’s time to clean up the connection, so I unregister it from the selector and close the socket.
04:28 If the write flag is set and there’s data in our buffer, then borkify it and send it back. Don’t forget to clear out the buffer so it doesn’t get sent twice.
04:39 I’ve made some assumptions in this code that are problematic. For example, this all falls apart if what the client sends is more than one kilobyte in size.
04:47 But I wanted to keep the code as concise as possible so I can concentrate on the selector part of this code. Alright, let’s go play with this.
04:58 In the top window, I’m going to start multibork,
05:04 and you can see that it’s waiting to receive a connection. Now in the middle window, I’ll start my counter client with a value of 100.
05:13 Ah, ah, ah. In the top window, you can see that a new connection got accepted and that it received a value of 100 and borked it back. Note the port number for our first count client is 52324.
05:26 Once again, I’m fiddling with the length of a second to allow myself time to talk. How you doing? You have time to chat? Would you like some tea? Well, look at the time.
05:36 A second’s gone by and 101 got borked back from the server.
05:41 102, and now in the bottom window let’s start another counter.
05:49 In the top window, you can see that a new connection has been established. Note the port number is 52329. Two clients mean two different ports on the source side, even though they’re both connecting to the server on 65432.
06:04 Now let’s let some time go by. Play it again, Sam. When the middle window is done, it cleanly closes its connection. The server sees that and deregisters the socket, no crashes, no problems.
06:19 And more importantly, when a second goes by, 503, 504, and still borking along. Finally, when the second counter is done, the server is still going. I could fire up another counter without any problem.
06:34 To close the server, I simply hit Control-C, and because the server code catches the exception, the selector gets cleaned up without a problem.
06:45 So far, you’ve been mucking around at the TCP/IP level. In the next lesson, I’ll go up a level on the stack and show you what’s involved in writing an application protocol.
Become a Member to join the conversation.