Simple Client and Server walkthrough ==================================== It is important to note, that the ``Connection`` object through which you do websocket things is completely agnostic of the opening handshake. *You can use it without doing the handshake.* Also, noio_ws is a bother to type, so throughout the examples I'll be importing it as ``ws`` and I recommend you do the same in your applications. This guide is slightly backwards, in that it will show you how to write a pure websocket client first, and how to do the opening handshake later. I do this because although the websocket protocol is strict about "requiring" the opening handshake, the reality is that unless you're connecting to a service that already employs the http handshake, it is not actually required in order to connect and use websockets. I'm sure there's an rfc author out there who'd throw rocks at me for saying that, but it's the truth. The basics __________ The noio_ws ``Connection`` object is your main (and really only) interface with the websocket protocol. You instance it, passing either 'CLIENT' or 'SERVER', depending on the role you're fulfilling. :: import noio_ws as ws ws_conn = ws.Connection('CLIENT') At this point the ``Connection`` considers its self to be open and ready to send and receive data. The ``Connection`` is an event based state-machine, as is typical of these sans-io libs. This means you get new frames as events from the connection using ``.next_event()``. Events are generated by passing network data through the ``Connection`` object with the ``.recv()`` method. *receive bytes from socket* ↴ *pass bytes to* ``Connection.recv()`` ↴ *get event with* ``Connection.next_event()`` It's best to wrap this workflow in a greater ``next_event()`` function. like so:: def ws_next_event(): while True: event = ws_conn.next_event() if event is ws.Information.NEED_DATA: # Feed the ws_conn network data. ws_conn.recv(sock.recv(2048)) continue # Keep going if there is no `Message` object. return event This function will only ever return ``Message`` objects, which are nice ways of handling a received websocket frame. *You can get more information about the ``message`` object mentioned above, and ``Frame`` object mentioned below,* `here `_ . To send data, we use the ``Frame`` object. It's also best to create a greater ``ws_send()`` function, much like ``ws_next_event()`` above. :: def ws_send(message, type, fin=True, status_code=None): sock.sendall(ws_conn.send(ws.Frame(message, type, fin, status_code))) To use this ``ws_send()`` function to send a 'hello world!' message, we'd simply call:: ws_send('hello world!', 'text') As you should know from reading the `'stuff you gotta know' `_ section, there are multiple types of frames you may receive at any given time, and you have to respond to them appropriately. Of course you can write this yourself any way you please, but you'll probably want some kind of managerial function to deal with these different frame types, like the following:: def incoming_message_manager(): while True: event = ws_next_event() if event.type == 'text': ... # print the message or whatever elif event.type == 'binary': ... # do some binary-ish things elif event.type == 'ping': ... # send the pong, like: # ws_send(event.message, 'pong') elif event.type == 'pong': ... # confirmed, connection isn't pointless :) elif event.type == 'close': ... # feel free to get the status code or w/e # then send your side of the close: # ws_send('', 'close') # at this point, we can exit the client. That's pretty much all you need to know to write a client and / or server. Combining these parts together, here is a barebones example client. Note that I have not included any particular type of concurrent io solution, but where I say "spawn an x" you can take that to mean "start a thread" or "spawn a curio / trio task". :: import noio_ws as ws import socket class WsClient: def __init__(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.ws_conn = ws.Connection(role='CLIENT') def main(self, location): self.sock.connect(location) # spawn an x to control sending messages # spawn an x to control incoming messages self.incoming_message_manager() def incoming_message_manager(): while True: event = self.ws_next_event() if event.type == 'text': ... # print the message or whatever elif event.type == 'binary': ... # do some binary-ish things elif event.type == 'ping': ... # send the pong, like: # self.ws_send(event.message, 'pong') elif event.type == 'pong': ... # confirmed, connection isn't pointless :) elif event.type == 'close': ... # feel free to get the status code or w/e # then send your side of the close: # self.ws_send('', 'close') # at this point, we can exit the client. def ws_send(self, message, type, fin=True, status_code=None): self.sock.sendall( self.ws_conn.send(ws.Frame(message, type, fin, status_code))) def ws_next_event(self): while True: event = self.ws_conn.next_event() if event is ws.Information.NEED_DATA: self.ws_conn.recv(self.sock.recv(2048)) continue return event websock_client = WsClient() websock_client.main(('some_location.com', 80)) And here is an example server :: class WsServer: def __init__(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) def main(self, location): self.sock.bind(location) self.sock.listen(5) while True: client_sock, addr = self.sock.accept() # Here we spawn something to handle a connected client, # like an async task or threaded handler. handler = WsClientHandler(client_sock, addr) handler.main() class WsClientHandler: def __init__(self, sock, addr): self.sock = sock self.addr = addr self.ws_conn = ws.Connection(role='SERVER') def main(self): # here we'll just spawn an x for the message manager self.incoming_message_manager() def incoming_message_manager(): while True: event = self.next_event() elif event.type == 'text': ... # print the message or whatever elif event.type == 'binary': ... # do some binary-ish things elif event.type == 'ping': ... # send the pong, like: # self.ws_send(event.message, 'pong') elif event.type == 'pong': ... # confirmed, connection isn't pointless :) elif event.type == 'close': ... # feel free to get the status code or w/e # then send your side of the close: # self.ws_send('', 'close') # at this point, we can exit the client. def ws_send(self, message, type, fin=True, status_code=None): self.sock.sendall( ws_conn.send(ws.Frame(message, type, fin, status_code))) def next_event(self): while True: event = self.ws_conn.next_event() if event is ws.Information.NEED_DATA: self.ws_conn.recv(self.sock.recv(2048)) continue return event websock_server = WsServer() websock_server.main(('some_location.com', 80)) Extensions, reserved bits and opcodes example _____________________________________________ In the previous section we took a look at a client and server that use the base opcodes, default reserved bits, and did nothin' fancy. Here we will write a similar example, adding a custom control frame, non-control frame, and implement a basic compression extension. As before we'll begin by instancing our ``Connection`` object, though we'll pass some extra arguments. :: import noio_ws as ws from zlib import compress, decompress from time import time # We'll add a new control frame that sends the current unix time when requested. new_control_frame = {11: 'time'} # We'll add a new non-control frame to indicate our message is ascii compatible. new_non_control_frame = {3: 'ascii'} ws_conn = ws.Connection('CLIENT', opcode_non_control_mod=new_non_control_frame, opcode_control_mod=new_control_frame) # Bam! We've started our connection and registered the new frame types. For our compression extension, we'll be using the first reserved bit to indicate if a message is compressed or not. We'll add a check for the reserved bit in our inbound-stuff function and decompress as required. :: def incoming_message_manager(): while True: event = ws_next_event() # here we check for compression, and decompress if needed # adding extensions is easy! if event.reserved[0] is 1: event.message = decompress(event.message) if event.type == 'text': ... # print the message or whatever elif event.type == 'binary': ... # do some binary-ish things elif event.type == 'ping': ... # send the pong, like: # ws_send(event.message, 'pong') elif event.type == 'pong': ... # confirmed, connection isn't pointless :) elif event.type == 'close': ... # feel free to get the status code or w/e # then send your side of the close: # ws_send('', 'close') # at this point, we can exit the client. elif event.type == 'time': ws_send(''.format(time()), 'text') That covers our two new opcodes and extension for incoming frames, but what about outgoing frames? We'll modify the basic ``ws_send()`` from the basic examples to handle our deflate compression and ascii frames. :: def ws_send(message, type, fin=True, status_code=None, deflate=False): if type == 'ascii': message = message.encode('ascii') rsv_1 = 0 if deflate: message = compress(message) rsv_1 = 1 sock.sendall( ws_conn.send(ws.Frame(message, type, fin, status_code, rsv_1=rsv_1))) And that's it. Everything else remains the same. You can add extensions and opcodes as arbitrarily as you like. There is no difference between client and server for extending the protocol like this. Here's the new client example in full: :: import noio_ws as ws import socket from time import time from zlib import compress, decompress class WsClient: def __init__(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.ws_conn = ws.Connection( 'CLIENT', opcode_non_control_mod={3: 'ascii'}, opcode_control_mod={11: 'time'}) def main(self, location): self.sock.connect(location) # spawn an x to control sending messages # spawn an x to control incoming messages self.incoming_message_manager() def incoming_message_manager(self): while True: event = self.ws_next_event() # here we check for compression, and decompress if needed # adding extensions is easy! if event.reserved[0] is 1: event.message = decompress(event.message) if event.type == 'text': ... # print the message or whatever elif event.type == 'binary': ... # do some binary-ish things elif event.type == 'ping': ... # send the pong, like: # self.ws_send(event.message, 'pong') elif event.type == 'pong': ... # confirmed, connection isn't pointless :) elif event.type == 'close': ... # feel free to get the status code or w/e # then send your side of the close: # self.ws_send('', 'close') # at this point, we can exit the client. elif event.type == 'time': self.ws_send(''.format(time()), 'text') def ws_send(self, message, type, fin=True, status_code=None, deflate=False): if type == 'ascii': message = message.encode('ascii') rsv_1 = 0 if deflate: message = compress(message) rsv_1 = 1 self.sock.sendall( self.ws_conn.send( ws.Frame(message, type, fin, status_code, rsv_1=rsv_1))) def ws_next_event(self): while True: event = self.ws_conn.next_event() if event is ws.Information.NEED_DATA: self.ws_conn.recv(self.sock.recv(2048)) continue return event websock_client = WsClient() websock_client.main(('some_location.com', 80)) The opening handshake _____________________ First things first, we need to do the opening handshake. :sup:`1. Sort of.` This is the worst part of dealing with websockets, so let's bear down and get through it together. :sup:`1. It's up to you. If you're writing your own client and server kind of deal, there's nothing stopping you creating an ssl connection or otherwise to a random port and avoiding the http stuff entirely. Like I say, it's a protocol not a cop. Make up your own way of connecting using websocket frames or whatever. It will probably make life easier.` The opening handshake utilities are, as described, direct addons for h11. Preforming the opening handshake to connect to ``ws://echo.websocket.org`` looks like this:: from noio_ws.handshake_utils import Handshake import h11 shaker = Handshake('CLIENT') http_send(shaker.client_handshake('ws://echo.websocket.org'), h11.EndOfMessage()) http_response = shaker.verify_response(http_next_event()) if isinstance(http_response, h11.Response): ... # Further action required. In the example below we'll do the same as above, but with more detail. As stated, Handshake stuff being an addon for h11, we'll be using that to send and recv requests/responses. :: from noio_ws.handshake_utils import Handshake import h11 # Make our socket. This will be used both for the http stuff # and websocket stuff. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.connect(location) # Make our h11.Connection. http_con = h11.Connection(our_role=h11.CLIENT) # Instance the Handshake object, for shakin' shaker = Handshake('CLIENT') # Call the .client_handshake method, passing the resulting # h11.Request object though the http_send func. http_send(shaker.client_handshake('ws://echo.websocket.org'), h11.EndOfMessage()) # Catch the response and verify it. http_response = shaker.verify_response(http_next_event()) # If the server responded with anything other than a # `101 Switching Protocols` (in the case of say, a `401 Unauthorized`) # the verification method will pass us back the h11.Response object # so that we may then go and do some auth or whatever. We'll check for # that here. If http_response isn't a h11.Response object, then we can # move ahead. if isinstance(http_response, h11.Response): ... # Do some auth or whatever. # If we make it to here, then our request has been accepted and we can # do websocket stuff! Woo! # These next two functions are described in the h11 docs. def http_send(*events): for event in events: data = http_con.send(event) if data is not None: sock.sendall(data) def http_next_event(): while True: event = http_con.next_event() if event is h11.NEED_DATA: http_con.receive_data(sock.recv(2048)) continue return event