pants.http.websocket
¶
pants.http.websocket
implements the WebSocket protocol, as described by
RFC 6455, on top of the Pants HTTP server using an API similar to that
provided by pants.stream.Stream
.
Using WebSockets¶
To start working with WebSockets, you’ll need to create a subclass of
WebSocket
. As with Stream
, WebSocket
instances are meant to contain the majority of your networking logic through
the definition of custom event handlers. Event handlers are methods that have
names beginning with on_
that can be safely overridden within
your subclass.
Listening for Connections¶
WebSocket
is designed to be used as a request handler for the Pants
HTTP server, pants.http.server.HTTPServer
. As such, to begin listening
for WebSocket connections, you must create an instance of
HTTPServer
using your custom WebSocket
subclass as its request handler.
from pants.http import HTTPServer, WebSocket
from pants import Engine
class EchoSocket(WebSocket):
def on_read(self, data):
self.write(data)
HTTPServer(EchoSocket).listen(8080)
Engine.instance().start()
If you need to host traditional requests from your HTTPServer instance, you may
invoke the WebSocket handler simply by creating an instance of your
WebSocket
subclass with the appropriate
pants.http.server.HTTPRequest
instance as its only argument:
from pants.http import HTTPServer, WebSocket
from pants import Engine
class EchoSocket(WebSocket):
def on_read(self, data):
self.write(data)
def request_handler(request):
if request.path == '/_ws':
EchoSocket(request)
else:
request.send_response("Nothing to see here.")
HTTPServer(request_handler).listen(8080)
Engine.instance().start()
WebSocket
and Application
¶
WebSocket
has support for pants.web.application.Application
and can easily be used as a request handler for any route. Additionally,
variables captured from the URL by Application
will be made accessible to the WebSocket.on_connect()
event handler. The
following example of a WebSocket echo server displays a customized welcome
message depending on the requested URL.
from pants.http import WebSocket
from pants.web import Application
app = Application()
@app.route("/ws/<name>")
class EchoSocket(WebSocket):
def on_connect(self, name):
self.write(u"Hello, {name}!".format(name=name))
def on_read(self, data):
self.write(data)
app.run(8080)
WebSocket Security¶
Secure Connections¶
WebSocket
relies upon the pants.http.server.HTTPServer
instance serving it to provide SSL. This can be as easy as calling the server’s
startSSL()
method.
To determine whether or not the WebSocket
instance is using a
secure connection, you may use the is_secure
attribute.
Custom Handshakes¶
You may implement custom logic during the WebSocket’s handshake by overriding
the WebSocket.on_handshake()
event handler. The on_handshake
handler
is called with a reference to the HTTPRequest
instance the WebSocket handshake is happening upon as well as an empty
dictionary that may be used to set custom headers on the HTTP response.
on_handshake
is expected to return a True value if the request is alright.
Returning a False value will result in the generation of an error page. The
following example of a custom handshake requires a secret HTTP header in the
request, and that the connection is secured:
from pants.http import WebSocket
class SecureSocket(WebSocket):
def on_handshake(self, request, headers):
return self.is_secure and 'X-Pizza' in request.headers
def on_connect(self):
self.write(u"Welcome to PizzaNet.")
Reading and Writing Data¶
WebSockets are a bit different than normal Stream
instances, as a WebSocket can transmit both byte strings and unicode strings,
and data is encapsulated into formatted messages with definite lengths. Because
of this, reading from one can be slightly different.
Mostly, however, the read_delimiter
works in exactly the
same way as that of pants.stream.Stream
.
Unicode Strings and Byte Strings¶
WebSocket
strictly enforces the difference between byte strings and
unicode strings. As such, the connection will be closed with a protocol error
if any of the following happen:
- The string types of the
read_delimiter
and the buffer differ.- There is an existing string still in the buffer when the client sends another string of a different type.
- The
read_delimiter
is currently a struct and the buffer does not contain a byte string.
Of those conditions, the most likely to occur is the first. Take the following code:
from pants.http import WebSocket, HTTPServer
from pants import Engine
def process(text):
return text.decode('rot13')
class LineOriented(WebSocket):
def on_connect(self):
self.read_delimiter = "\n"
def on_read(self, line):
self.write(process(line))
HTTPServer(LineOriented).listen(8080)
Engine.instance().start()
And, on the client:
<!DOCTYPE html>
<textarea id="editor"></textarea><br>
<input type="submit" value="Send">
<script>
var ws = new WebSocket("ws://localhost:8080/"),
input = document.querySelector('#editor'),
button = document.querySelector('input');
ws.onmessage = function(e) {
alert("Got back: " + e.data);
}
button.addEventListener("click", function() {
ws.send(input.value + "\n");
});
</script>
On Python 2.x, the read delimiter will be a byte string. The WebSocket will expect to receive a byte string. However, the simple JavaScript shown above sends unicode strings. That simple service would fail immediately on Python 2.
To avoid the problem, be sure to use the string type you really want for your read delimiters. For the above, that’s as simple as setting the read delimiter with:
self.read_delimiter = u"\n"
WebSocket Messages¶
In addition to the standard types of read_delimiter
,
WebSocket
instances support the use of a special value called
EntireMessage
. When using EntireMessage
, full messages will
be sent to your on_read
event handler, as framed by
the remote end-point.
EntireMessage
is the default read_delimiter
for
WebSocket instances, and it makes it dead simple to write simple services.
The following example implements a simple RPC system over WebSockets:
import json
from pants.http.server import HTTPServer
from pants.http.websocket import WebSocket, FRAME_TEXT
from pants import Engine
class RPCSocket(WebSocket):
methods = {}
@classmethod
def method(cls, name):
''' Add a method to the RPC. '''
def decorator(method):
cls.methods[name] = method
return method
return decorator
def json(self, **data):
''' Send a JSON object to the remote end-point. '''
# JSON outputs UTF-8 encoded text by default, so use the frame
# argument to let WebSocket know it should be sent as text to the
# remote end-point, rather than as binary data.
self.write(json.dumps(data), frame=FRAME_TEXT)
def on_read(self, data):
# Attempt to decode a JSON message.
try:
data = json.loads(data)
except ValueError:
self.json(ok=False, result="can't decode JSON")
return
# Lookup the desired method. Return an error if it doesn't exist.
method = data['method']
if not method in self.methods:
self.json(ok=False, result="no such method")
return
method = self.methods[method]
args = data.get("args", tuple())
kwargs = data.get("kwargs", dict())
ok = True
# Try running the method, and capture the result. If it errors, set
# the result to the error string and ok to False.
try:
result = method(*args, **kwargs)
except Exception as ex:
ok = False
result = str(ex)
self.json(ok=ok, result=result)
@RPCSocket.method("rot13")
def rot13(string):
return string.decode("rot13")
HTTPServer(RPCSocket).listen(8080)
Engine.instance().start()
As you can see, it never even uses read_delimiter
. The
client simply sends JSON messages, with code such as:
my_websocket.send(JSON.stringify({method: "rot13", args: ["test"]}));
This behavior is completely reliable, even when the client is sending fragmented messages.
WebSocket
¶
-
class
pants.http.websocket.
WebSocket
(request, *arguments)[source]¶ An implementation of WebSockets on top of the Pants HTTP server using an API similar to that of
pants.stream.Stream
.A
WebSocket
instance represents a WebSocket connection to a remote client. In the future, WebSocket will be modified to support acting as a client in addition to acting as a server.When using WebSockets you write logic as you could for
Stream
, using the sameread_delimiter
and event handlers, while the WebSocket implementation handles the initial negotiation and all data framing for you.Argument Description request The HTTPRequest
to begin negotiating a WebSocket connection over.-
is_secure
¶ Whether or not the underlying HTTP connection is secured.
-
allow_old_handshake
¶ Whether or not to allow clients using the old draft-76 protocol to connect. By default, this is set to False.
Due to the primitive design of the draft-76 version of the WebSocket protocol, there is significantly reduced functionality when it is being used.
- Binary data cannot be transmitted. All communications between
the
WebSocket
instance and the remote end-point must take place using unicode strings. - Connections are closed immediately with no concept of close
reasons. When you use
close()
on a draft-76 WebSocket, it will flush the buffer and then, once the buffer empties, close the connection immediately. - There are no control frames, such as the PING frames created
when you invoke
ping()
.
There are other missing features as well, such as extensions and the ability to fragment long messages, but Pants does not currently provide support for those features at this time.
- Binary data cannot be transmitted. All communications between
the
-
buffer_size
¶ The maximum size, in bytes, of the internal buffer used for incoming data.
When buffering data it is important to ensure that inordinate amounts of memory are not used. Setting the buffer size to a sensible value can prevent coding errors or malicious use from causing your application to consume increasingly large amounts of memory. By default, a maximum of 64kb of data will be stored.
The buffer size is mainly relevant when using a string value for the
read_delimiter
. Because you cannot guarantee that the string will appear, having an upper limit on the size of the data is appropriate.If the read delimiter is set to a number larger than the buffer size, the buffer size will be increased to accommodate the read delimiter.
When the internal buffer’s size exceeds the maximum allowed, the
on_overflow_error()
callback will be invoked.Attempting to set the buffer size to anything other than an integer or long will raise a
TypeError
.
-
close
(flush=True, reason=1000, message=None)[source]¶ Close the WebSocket connection. If flush is True, wait for any remaining data to be sent and send a close frame before closing the connection.
Argument Default Description flush True
Optional. If False, closes the connection immediately, without ensuring all buffered data is sent. reason 1000
Optional. The reason the socket is closing, as defined at RFC 6455#section-7.4. message None
Optional. A message string to send with the reason code, rather than the default.
-
local_address
¶ The address of the WebSocket on the local machine.
By default, this will be the value of
socket.getsockname
or None. It is possible for user code to override the default behaviour and set the value of the property manually. In order to return the property to its default behaviour, user code then has to delete the value. Example:# default behaviour channel.local_address = custom_value # channel.local_address will return custom_value now del channel.local_address # default behaviour
-
on_connect
(*arguments)[source]¶ Placeholder. Called after the WebSocket has connected to a client and completed its handshake. Any additional arguments passed to the
WebSocket
instance’s constructor will be passed to this method when it is invoked, making it easy to useWebSocket
together with the URL variables captured bypants.web.application.Application
, as shown in the following example:from pants.web import Application from pants.http import WebSocket app = Application() @app.route("/ws/<int:id>") class MyConnection(WebSocket): def on_connect(self, id): pass
-
on_handshake
(request, headers)[source]¶ Placeholder. Called during the initial handshake, making it possible to validate the request with custom logic, such as Origin checking and other forms of authentication.
If this function returns a False value, the handshake will be stopped and an error will be sent to the client.
Argument Description request The pants.http.server.HTTPRequest
being upgraded to a WebSocket.headers An empty dict. Any values set here will be sent as headers when accepting (or rejecting) the connection.
-
on_overflow_error
(exception)[source]¶ Placeholder. Called when an internal buffer on the WebSocket has exceeded its size limit.
By default, logs the exception and closes the WebSocket.
Argument Description exception The exception that was raised.
-
on_pong
(data)[source]¶ Placeholder. Called when a PONG control frame is received from the remote end-point in response to an earlier ping.
When used together with the
ping()
method,on_pong
may be used to measure the connection’s round-trip time. Seeping()
for more information.Argument Description data Either the RTT expressed as seconds, or an arbitrary byte string that served as the PONG frame’s payload.
-
on_read
(data)[source]¶ Placeholder. Called when data is read from the WebSocket.
Argument Description data A chunk of data received from the socket. Binary data will be provided as a byte string, and text data will be provided as a unicode string.
-
ping
(data=None)[source]¶ Write a ping frame to the WebSocket. You may, optionally, provide a byte string of data to be used as the ping’s payload. When the end-point returns a PONG, and the
on_pong()
method is called, that byte string will be provided toon_pong
. Otherwise,on_pong
will be called with the elapsed time.Argument Description data Optional. A byte string of data to be sent as the ping’s payload.
-
read_delimiter
¶ The magical read delimiter which determines how incoming data is buffered by the WebSocket.
As data is read from the socket, it is buffered internally by the WebSocket before being passed to the
on_read()
callback. The value of the read delimiter determines when the data is passed to the callback. Valid values areNone
, a string, an integer/long, a compiled regular expression, an instance ofstruct.Struct
, an instance ofnetstruct.NetStruct
, or theEntireMessage
object.When the read delimiter is the
EntireMessage
object, entire WebSocket messages will be passed toon_read()
immediately once they have been received in their entirety. This is the default behavior forWebSocket
instances.When the read delimiter is
None
, data will be passed toon_read()
immediately after it has been received.When the read delimiter is a byte string or unicode string, data will be buffered internally until that string is encountered in the incoming data. All data up to but excluding the read delimiter is then passed to
on_read()
. The segment matching the read delimiter itself is discarded from the buffer.Note
When using strings as your read delimiter, you must be careful to use unicode strings if you wish to send and receive strings to a remote JavaScript client.
When the read delimiter is an integer or a long, it is treated as the number of bytes to read before passing the data to
on_read()
.When the read delimiter is an instance of
struct.Struct
, the Struct’ssize
is fully buffered and the data is unpacked before the unpacked data is sent toon_read()
. Unlike other types of read delimiters, this can result in more than one argument being sent to theon_read()
event handler, as in the following example:import struct from pants.http import WebSocket class Example(WebSocket): def on_connect(self): self.read_delimiter = struct.Struct("!ILH") def on_read(self, packet_type, length, id): pass
You must send binary data from the client when using structs as your read delimiter. If Pants receives a unicode string while a struct read delimiter is set, it will close the connection with a protocol error. This holds true for the
Netstruct
delimiters as well.When the read delimiter is an instance of
netstruct.NetStruct
, the NetStruct’sminimum_size
is buffered and unpacked with the NetStruct, and additional data is buffered as necessary until the NetStruct can be completely unpacked. Once ready, the data will be passed toon_read()
. Using Struct and NetStruct are very similar.When the read delimiter is a compiled regular expression (
re.RegexObject
), there are two possible behaviors that you may switch between by setting the value ofregex_search
. Ifregex_search
is True, as is the default, the delimiter’ssearch()
method is used and, if a match is found, the string before that match is passed toon_read()
. The segment that was matched by the regular expression will be discarded.If
regex_search
is False, the delimiter’smatch()
method is used instead and, if a match is found, the match object itself will be passed toon_read()
, giving you access to the capture groups. Again, the segment that was matched by the regular expression will be discarded from the buffer.Attempting to set the read delimiter to any other value will raise a
TypeError
.The effective use of the read delimiter can greatly simplify the implementation of certain protocols.
-
remote_address
¶ The remote address to which the WebSocket is connected.
By default, this will be the value of
socket.getpeername
or None. It is possible for user code to override the default behaviour and set the value of the property manually. In order to return the property to its default behaviour, user code then has to delete the value. Example:# default behaviour channel.remote_address = custom_value # channel.remote_address will return custom_value now del channel.remote_address # default behaviour
-
write
(data, frame=None, flush=False)[source]¶ Write data to the WebSocket.
Data will not be written immediately, but will be buffered internally until it can be sent without blocking the process.
Calling
write()
on a closed or disconnected WebSocket will raise aRuntimeError
.If data is a unicode string, the data will be sent to the remote end-point as text using the frame opcode for text. If data is a byte string, the data will be sent to the remote end-point as binary data using the frame opcode for binary data. If you manually specify a frame opcode, the provided data must be a byte string.
An appropriate header for the data will be generated by this method, using the length of the data and the frame opcode.
Arguments Description data A string of data to write to the WebSocket. Unicode will be converted automatically. frame Optional. The frame opcode for this message. flush Optional. If True, flush the internal write buffer. See pants.stream.Stream.flush()
for details.
-
write_file
(sfile, nbytes=0, offset=0, flush=False)[source]¶ Write a file to the WebSocket.
This method sends an entire file as one huge binary frame, so be careful with how you use it.
Calling
write_file()
on a closed or disconnected WebSocket will raise aRuntimeError
.Arguments Description sfile A file object to write to the WebSocket. nbytes Optional. The number of bytes of the file to write. If 0, all bytes will be written. offset Optional. The number of bytes to offset writing by. flush Optional. If True, flush the internal write buffer. See flush()
for details.
-
write_packed
(*data, **kwargs)[source]¶ Write packed binary data to the WebSocket.
Calling
write_packed()
on a closed or disconnected WebSocket will raise aRuntimeError
.If the current
read_delimiter
is an instance ofstruct.Struct
ornetstruct.NetStruct
the format will be read from that Struct, otherwise you will need to provide aformat
.Argument Description *data Any number of values to be passed through struct
and written to the remote host.format Optional. A formatting string to pack the provided data with. If one isn’t provided, the read delimiter will be used. flush Optional. If True, flush the internal write buffer. See flush()
for details.
-
EntireMessage
¶
-
pants.http.websocket.
EntireMessage
¶ EntireMessage
is a unique Python object that, when set as theread_delimiter
for aWebSocket
instance, will cause entire messages to be passed to theon_read()
event handler at once.