From dfcac651b4b432e67c8736e525c59f4c224feb3d Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Thu, 22 Apr 2021 12:58:50 +0200 Subject: [PATCH] Updated README with echo server example --- README.md | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 196 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 575f7ad..aa632ed 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,7 @@ the next section, but for the curios among y'all I might as well explain exactly When async functions are called without the `await`, they don't exactly do nothing: they return this weird 'coroutine' object -```python - +``` >>> giambio.sleep(1) ``` @@ -519,6 +518,201 @@ that only `giambio.run` can understand. Other libraries have other private "lang not possible: doing so will cause giambio to get very confused and most likely just explode spectacularly badly. +## Doing I/O + +I don't know about you, but to me all of the code we wrote so far was pretty boring. But here comes the fun part: now +I'll show you how to do actual work with giambio using its I/O primitives. + +__Note__: As with everything in giambio, I/O support is limited and experimental. Any socket kind from python's builtin +socket module can be used with giambio, but other advanced features such as file I/O or memory channels simply don't +exist yet + + +### An echo server + +For the purposes of this document, it's best to keep things simple, so we'll be writing the "Hello, world!" of +network servers: an echo server. An echo server simply replies to the client with the same data that it got from it + +As always, I'll first throw the entire snippet at you and then disassemble it step by step, but since this code is +a little longer than usual we'll be dealing with one function at a time: first, let's write a function that can accept +clients and dispatch them to some other handler. + +```python +import giambio +import socket +import logging + + +async def serve(bind_address: tuple): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(bind_address) + sock.listen(5) + async_sock = giambio.wrap_socket(sock) # We make the socket an async socket + logging.info(f"Serving asynchronously at {bind_address[0]}:{bind_address[1]}") + async with giambio.create_pool() as pool: + while True: + conn, address_tuple = await async_sock.accept() + logging.info(f"{address_tuple[0]}:{address_tuple[1]} connected") + pool.spawn(handler, conn, address_tuple) + +``` + +So, our `serve` function does a few things: +- Sets up our server socket, just like in a synchronous server (notice how we bind and listen **before** wrapping it) +- Uses giambio's `wrap_socket` function to wrap the plain old synchronous socket into an async one +- Opens a task pool and starts listening for clients in loop by using our new `giambio.socket.AsyncSocket` object + - Notice how we use `await async_sock.accept()` and not `sock.accept()`, because that could block the loop +- Once a client connects, we log some information, spawn a new task and pass it the client socket: that is our client handler + +So, let's go over the declaration of `handler` then: + +```python +async def handler(sock, client_address): + address = f"{client_address[0]}:{client_address[1]}" + async with sock: # Closes the socket automatically + await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n") + while True: + await sock.send_all(b"-> ") + data = await sock.receive(1024) + if not data: + break + elif data == b"exit\n": + await sock.send_all(b"Shutting down the server\n") + raise Exception # This kills the entire application! + logging.info(f"Got: {data!r} from {address}") + await sock.send_all(b"Got: " + data) + logging.info(f"Echoed back {data!r} to {address}") + logging.info(f"Connection from {address} closed") +``` + +This is where clients will be dispatched once they connect: +- First, we use the tuple that `serve` gave us to build a nice human-readable IP address +- giambio sockets support the context manager interface, just like regular sockets, so we use `async with sock` which + will automatically close the socket for us when we're done using it +- Since we're nice people, we greet our users once they connect with a welcome message (notice: we sent **bytes**!) + - As a side note, regular python sockets differentiate `sock.send` from `sock.sendall`: The difference is that `send` might + not send the whole payload immediately, while `sendall` is just a wrapper around `send` in a loop which makes sure + that all data is sent before returning. Since this difference is completely unnecessary and can lead to errors, + giambio sockets only have a `send_all` method which **always** sends all the passed data before returning, but the + naming was kept explicit because of the ambiguity caused by the builtin socket library. +- With the greetings out of the way, we enter a loop where we ask our client for data by using the `receive` method. Note that, just + like regular python sockets' `recv` method, `receive` is guaranteed to return **at most** 1024 bytes, but **at least** 1 byte + (or any size in that range) depending on your OS buffers and network congestion +- We do a little check here: if what we receive is an empty message, then our client is gone and we can exit the loop +- Since I want to show off giambio's exception handling, I added a little if condition that will raise an exception if a client + sends us a message with "exit" as content: this will propagate the task in our `serve` function and kill all children tasks +- Here comes the "echo" part of "echo server": We log the message to the screen and then send the same data back to our client + +Finally, some startup code: + +```python + +if __name__ == "__main__": + logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p") + try: + giambio.run(serve, ("localhost", 1500)) + except (Exception, KeyboardInterrupt) as error: # Exceptions propagate! + if isinstance(error, KeyboardInterrupt): + logging.info("Ctrl+C detected, exiting") + else: + logging.error(f"Exiting due to a {type(error).__name__}: {error}") +``` + +This looks fancy, but all it does is just run our server and catch any exception that might happen (because, again, exceptions are never discarded +in giambio): We differentiate KeyboardInterrupt from anything else because that is most likely us shutting down the server from the console. + +So, putting everything together: + +```python + +import giambio +import socket +import logging + + +async def handler(sock, client_address): + address = f"{client_address[0]}:{client_address[1]}" + async with sock: # Closes the socket automatically + await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n") + while True: + await sock.send_all(b"-> ") + data = await sock.receive(1024) + if not data: + break + elif data == b"exit\n": + await sock.send_all(b"Shutting down the server\n") + raise Exception # This kills the entire application! + logging.info(f"Got: {data!r} from {address}") + await sock.send_all(b"Got: " + data) + logging.info(f"Echoed back {data!r} to {address}") + logging.info(f"Connection from {address} closed") + + +async def serve(bind_address): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(bind_address) + sock.listen(5) + async_sock = giambio.wrap_socket(sock) # We make the socket an async socket + logging.info(f"Serving asynchronously at {bind_address[0]}:{bind_address[1]}") + async with giambio.create_pool() as pool: + while True: + conn, address_tuple = await async_sock.accept() + logging.info(f"{address_tuple[0]}:{address_tuple[1]} connected") + pool.spawn(handler, conn, address_tuple) + + +if __name__ == "__main__": + logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p") + try: + giambio.run(serve, ("localhost", 1500)) + except (Exception, KeyboardInterrupt) as error: # Exceptions propagate! + if isinstance(error, KeyboardInterrupt): + logging.info("Ctrl+C detected, exiting") + else: + logging.error(f"Exiting due to a {type(error).__name__}: {error}") +``` + +Save this into a file and try running it, you should see something along the lines of: +``` +[INFO] 22/04/2021 PM Serving asynchronously at localhost:1500 + +``` +Yay! Our echo server is running, let's test it out by using the netcat terminal utility: +``` +user@hostname:~ # nc localhost 1501 +Welcome to the server pal, feel free to send me something! +-> async server test +Got: async server test +-> yay! +Got: yay! +``` + +And, on the server side... +``` +[INFO] 22/04/2021 PM 127.0.0.1:52239 connected +[INFO] 22/04/2021 PM Got: b'async server test\n' from 127.0.0.1:52239 +[INFO] 22/04/2021 PM Echoed back b'async server test\n' to 127.0.0.1:52239 +[INFO] 22/04/2021 PM Got: b'yay!\n' from 127.0.0.1:52239 +[INFO] 22/04/2021 PM Echoed back b'yay!\n' to 127.0.0.1:52239 +``` + +Try opening more terminal windows concurrently and sending messages all at once, you'll see that they all +get replied to at the same time! That's the power of async. + +Just to wrap up, try sending "exit" as a message: +``` +-> exit +Shutting down the server +``` + +And on our server, as expected: +``` +[ERROR] 22/04/2021 PM Exiting due to a Exception: +``` + +If you want to play around with this code you can also try pressing Ctrl+D/Ctrl+C on netcat to close your connection, +or Ctrl+C on the server's console to shut it down completely. + # Contributing This is a relatively young project and it is looking for collaborators! It's not rocket science,