Connecting To WebSocket Services
WebSocket is a communications protocol that allows the bidirectional
transmission of text and binary data. There is an insecure variant (WS) and a
secure variant over TLS (WSS). For each of these protocols, the WebSocket
communication is established using an initial HTTP request to "upgrade" the
connection to a WebSocket connection. Communication occurs between a
WebSocket server and a WebSocket client. The Wolfram Language supports
connecting to a WebSocket server as a WebSocket client. The Wolfram Language
does not support establishing a WebSocket server on its own. Various online
services offer access to their APIs via WebSocket. That is the motivation for
including this functionality in the Wolfram Language.
The Basics: Communicating with a Minimal WebSocket Server
To introduce the basic functions for WebSocket communication, we will
connect to a simple echo server. Echo servers just repeat, or echo, the
message data sent to them. For this example, we will connect to echo.websocket.org, which is a server run by the WebSocket standards organization.
The basic function for connecting to WebSocket servers is SocketConnect. SocketConnect will return a SocketObject, which we can use for writing to and reading from the WebSocket server
using SocketWriteMessage and SocketReadMessage.
The code to connect to a WebSocket server is simple:
socket = SocketConnect["wss://echo.websocket.org"]There are two main choices for the first argument to SocketConnect when connecting to a WebSocket server. The first choice is using a plain
URL with a "wss://" or "ws://" scheme, as we did above. The other choice is an HTTPRequest object. We will give a scenario where it would make sense to use an HTTPRequest object instead of a URL string in the section "Navigating Different
Authorization Methods" below.
Now that we have an active WebSocket connection, we can write to and read
from it. This particular echo server sends an initial opening message that we
can read:
response = SocketReadMessage[socket]The data is returned as a ByteArray, so we need to convert it to a string:
ByteArrayToString[response["DataByteArray"]]Now we can send the message that we want echoed:
SocketWriteMessage[socket, "Hello world!!"]response = SocketReadMessage[socket];
ByteArrayToString[response["DataByteArray"]]Finally, we can close the connection using Close:
Close[socket]
Note that the WebSocket protocol allows closing the connection with a
specific status code and reason to indicate to the other side of the
connection why the connection is being closed. Accordingly, the following is
also valid:
socket = SocketConnect["wss://echo.websocket.org"];
Close[<|"SocketObject" -> socket, "ConnectionClosedCode" -> 1000, "ConnectionClosedReason" -> "Finished"|>]A description of valid status codes according to the WebSocket standard can
be found at the WebSocket RFC.
Navigating Different Authorization Methods
Most WebSocket services will require you to provide authorization in order to
use their services, likely in the form of an API key. This section will
explore two common authorization models.
In the first model, the authorization information is provided in the initial
HTTP request. As a reminder, this works because all WebSocket connections
begin with an initial HTTP request that is then transitioned into a WebSocket
connection.
To demonstrate this model, let's start a WebSocket server using Python. This
server will listen on 127.0.0.1 and port 26738. This server will broadcast
the current time every 15 seconds to any established connections. However,
the server will require authorization in the form of the initial HTTP request
having the header "MYAPIKEY" and a value "000." Otherwise, it will reject
the incoming connection:
serverPySource = "...";
serverPy = CreateFile[];
Export[serverPy, serverPySource, "String"];
pythonEvaluator = {"Python", "Evaluator" -> <|"Dependencies" -> {"websockets"}|>};
pythonPath = ExternalEvaluate[pythonEvaluator, "import sys; sys.executable"];
serverProcess = StartProcess[{pythonPath, serverPy}]
The server has been started. Note how attempting to connect to the server
without authorization information fails:
SocketConnect["ws://127.0.0.1:26738"]However, the connection does succeed when providing connection information
using an HTTPRequest wrapper:
socket = SocketConnect[HTTPRequest["ws://127.0.0.1:26738", <|"Headers" -> <|"MYAPIKEY" -> "000"|>|>]]msg = SocketReadMessage[socket];
ByteArrayToString[msg["DataByteArray"]]Close[socket]
KillProcess[serverProcess]
DeleteFile[serverPy]
Another common initialization model requires that the first message that the
client sends include authorization and setup material. For example, an LLM
service may also require that the first message include the particular model
to use.
To demonstrate this model, we will use a similar Python server. However,
instead of checking the initial HTTP request, this server will check the
client's first message. For this example, the server will require that the
first message be formatted in JSON and include a "MYAPIKEY" key with a
"000" value and a "TIMEZONE" key that will control the time zone the
broadcasted times are in:
serverPySource2 = "...";
serverPy2 = CreateFile[];
Export[serverPy2, serverPySource2, "String"];
serverProcess2 = StartProcess[{pythonPath, serverPy2}]socket = SocketConnect["ws://127.0.0.1:26738"]
The connection succeeds, but nothing will be sent over the connection until
we authenticate:
SocketWriteMessage[socket, "{\"MYAPIKEY\":\"000\",\"TIMEZONE\":\"CST\"}"];
msg = SocketReadMessage[socket];
ByteArrayToString[msg["DataByteArray"]]msg = SocketReadMessage[socket];
ByteArrayToString[msg["DataByteArray"]]Close[socket]
KillProcess[serverProcess2]
DeleteFile[serverPy2]Handling WebSocket Data Asynchronously
In the sections above, we manually read from WebSocket connections using SocketReadMessage. An alternative to this is registering a handler that will be
automatically called on incoming data using SocketListen.
As a demonstration of this functionality, let's revisit our Python server
that broadcasts the time. Perhaps we want to speak the current time every
time we receive an update. This can be easily accomplished using a socket
handler.
First, let's start our server again:
serverPySource3 = "...";
serverPy3 = CreateFile[];
Export[serverPy3, serverPySource3, "String"];
serverProcess3 = StartProcess[{pythonPath, serverPy3}]socket = SocketConnect["ws://127.0.0.1:26738"]Before we authenticate, let's define our handler. The handler will receive
an association containing several keys. These keys are documented in SocketListen. However, our handler only needs to care about "DataByteArray", which will contain the ByteArray corresponding to the data of an incoming message.
timeHandler[assoc_] :=
Module[
{jsonAssoc, timeObject},
jsonAssoc = Association[ImportByteArray[assoc["DataByteArray"], "JSON"]];
If[
!KeyExistsQ[jsonAssoc, "time"],
Return[];
];
timeObject = DateObject[jsonAssoc["time"]];
Speak["The time is " <> ToString[timeObject["Hour"]] <> ":" <> ToString[timeObject["Minute"]]];
];Now let's register the handler and authenticate:
listener = SocketListen[socket, timeHandler]SocketWriteMessage[socket, "{\"MYAPIKEY\":\"000\",\"TIMEZONE\":\"CST\"}"]
The current hour and minute should now be spoken every 15 seconds.
We can delete the SocketListener using DeleteObject:
DeleteObject[listener]And finally, we can clean up the other pieces:
Close[socket]
KillProcess[serverProcess3]
DeleteFile[serverPy3]