-
Notifications
You must be signed in to change notification settings - Fork 20
Exercise4EscapeRoomAsychUserInput
Assigned | 2/11/2019 |
Due | 2/13/2019 |
Points | 25 |
This is a follow-up to the exercise 3. The only change you will make is to remove the function input()
from your client code and replace it with something else that is asynchronous.
Most of this assignment is a freebie. I will explain most of how this works and you will just have to put it together.
The first thing to learn about is add_reader()
. This is a method of the asyncio event loop. It takes a raw I/O device, such as stdin
and a callback, and alerts the callback whenever the I/O is ready for reading. Here's some basic code:
def handle_stdin():
line_in = sys.stdin.readline()
line_in = line_in[:-1] # remove \n
# handle line_in
loop = asyncio.get_event_loop()
loop.add_reader(sys.stdin, handle_stdin)
loop.run_forever()
Notice a few things. First, the input isn't actually passed to handle_stdin()
. Instead, the function is called when data is ready. Then the function itself actually gets the data (i.e., sys.stdin.readline()
).
The second thing to notice is that the read-in line has a newline at the end of it. Make sure to strip it off.
Finally, we don't actually do anything with the data in the code above. What should we do with it? For this exercise, we're just going to store it in a Python list. some of you have been working with asyncio Queue
's. You're welcome to use these instead, but for those of you just learning asyncio, a list will work just fine. Observe:
stdin_queue = []
def handle_stdin():
line_in = sys.stdin.readline()
line_in = line_in[:-1] # remove \n
stdin_queue.append(line_in)
loop = asyncio.get_event_loop()
loop.add_reader(sys.stdin, handle_stdin)
loop.run_forever()
Now what? How can we use this? No problem! Let's move on to the next piece!
So, we have all of our input going into a queue (list). That's great and all, but how do we merge it with output? We'd like to write a prompt to the screen (e.g., >>
) and then get data. How do we do that?
The trick here, is to understand that you can print something out to the screen and then wait for data to be available in the input queue! In other words, let your input reader load something into the queue in an asynchronous fashion. Similarly, in an asynchronous fashion, you can read data out of the queue.
Again, asyncio has a special queue class that is designed to do this. If you know how that class works, feel free to use it. But just using asyncio's sleep()
you can even make this work with a regular python list.
Let's start by writing an asynchronous function that prints something out to the screen:
async def async_input(prompt):
print(prompt, end="")
sys.stdout.flush()
Ok. What's going on so far? Really, not much. All we're doing so far is printing out the prompt and exiting the function. Notice the end=""
in the print function. This tells it to print no newline, which is useful for prompts. However, without a newline, sometimes it won't print to the screen. The flush()
method forces it out.
So, how do we get the input? Remember, the input is going into the global stdin_queue
. We want to wait until there's at least one element in the queue. How can we do that asynchronously? If you paid attention in the lecture on 2/11, you should remember the asyncio.sleep
function. This can be used to asynchronously wait.
async def async_input(prompt):
print(prompt, end="")
sys.stdout.flush()
while len(stdin_queue) == 0:
await asyncio.sleep(.1)
return stdin_queue.pop(0)
This co-routine will print a prompt and then wait asynchronously (with a tenth of a second resolution) for a new input line from stdin. Remember! The asyncio system is NOT multi-threaded! What is happening here is that the async_input co-routine can "pause" mid-execution during the await asyncio.sleep
call. You MUST use await
with asyncio.sleep
! The sleep
function returns a co-routine! If you don't await
it, it does absolutely nothing! Your while loop will just run forever!
(The await
is a syntactic sugar for a yield from
call. If you don't use await
, there's no yield and the function doesn't "pause").
So we're all good, right? Now we have an asynchronous Input?
Not quite. The weakness of these kinds of functions is that they return coroutines! You can't just drop async_input
in where ever you had input
because this function doesn't do anything when just called as a function. Remember, it's like a function with yield in it. It just returns a co-routine (generator).
To actually make this function do something useful, you either have to turn it into a Future
or it has to be await
ed in another async
function.
To put all these pieces together, we're going to have another function that is going to be our client game runner. I could call it a "loop", but that name is already used by asyncio and I don't want to make it confusing.
Our game runner should basically do the following in an endless loop:
- Get a user input
- Send the input to the server
- Wait for the response
The code to do this might look like the following:
async def game_runner(protocol):
while True:
command = await async_input(">> ")
protocol.transport.write(command.encode())
# await a response... see if you can figure this part out!
Notice that we pass the protocol instance in. I'll show you how to do this in a moment. But first, notice that this is a async
function. It returns a co-routine and does not execute any of this until it's running in the event loop.
The second thing to notice is that an await
ed function can return a value. This is a very handy syntax.
I haven't written the code for waiting for a response. But as a hint, you are waiting for data to come back from the server. You could have a variable that gets set when the data comes back and you could sleep until it's set. Or, if you know how to use the asyncio Queue
class, you could use that instead (and await
it).
The setup in the main part of your program is more-or-less the same, but requires just a few small changes. The first thing to do is get the connection to the server setup, then start the game_runner
. If you've seen some of the examples of asyncio, you should have seen the run_until_complete
method. This runs the event loop until a co-routine parameter completes. We use this to get the connection process complete.
loop = asyncio.get_event_loop()
coro = loop.create_connection(EscapeRoomClientProtocol, host=host, port=port)
transport, protocol = loop.run_until_complete(coro)
Notice that, like the return value from await
, the run_until_complete
returns a value from the co-routine. The create_connection
co-routine, when complete, returns the transport and protocol instance representing the connection. We're going to pass the protocol
object to the game_runner
.
loop.add_reader(sys.stdin, stdin_reader) # setup the call-back!
asyncio.ensure_future(game_runner(protocol))
loop.run_forever()
loop.close()
As you can see, we first add our reader to our stdin_reader
function. This is important! Don't forget it!
Next, we run game_runner
with the protocol
parameter. Remember, this is a async
function that returns a co-routine. It doesn't actually start execution yet! But we pass that co-routine (generator) to ensure_future
. This method makes sure it runs once the loop starts (again). Of course, by now, run_forever
should be familiar to you.
I have written 98% of this lab for you. All you need to figure out is how to wait for a response from the server. Hopefully you can figure this out. Because there is so little to figure out on this lab, PLEASE DO NOT WORK IN GROUPS OR COLLABORATE! If you get stuck, please come and talk directly to me or the TA.
Please remember, we are using Python 3.6 syntax! Python 3.7 introduced some new syntax that we aren't up-to-date on yet. So when looking at help files, make sure to select 3.6.
You should have the following file in your GitHub repository.
<your_repository_root>/src/exercises/ex4/escape_room_client_asyncio2.py
The grading breakdown is a binary determination as to whether you got this lab working correctly.
- Correct client: 25