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. 1. Sort of. This is the worst part of dealing with websockets, so let’s bear down and get through it together.

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