So, if you read my previous posts, you know that, up till now, we have a Jupyter kernel that opens its needed ZeroMQ sockets on arbitrary ports and does pretty much nothing besides that.
If you are a little versed in socket programming, or even just Internet Protocol (IP) things, you know that this won’t work: we can’t just start listening (bind) to a port on a server and not tell the client which port it should connect to, or vice versa.
So how do we (or Jupyter, in that case) keep everyone in synchrony, talking on the same communication channels ?
Remember that in the end of the last post I mentioned a configuration file being read ? That’s the connection file generated by provided to both client (frontend) and server (kernel) when they are started, containing the necessary communication info in JSON format.
The default file provided by Jupyter (here located in /run/user/1000/jupyter/kernel-9617.json) looks like this:
You can see it contains the IP host string and the port numbers used by the kernel sockets. Transport protocol (TCP) and other relevant (well, not to me for now, lol) info are also provided. The fact that the kernel name is there too makes me thing that the file is generated each time we start the client console with the –kernel parameter, but that’s just a guess.
As we started talking about JSON, let me say that its usage is widespread in Jupyter. Which is subjectively good, since I like it (looks nicer than XML, at least).
I didn’t talk about it in details before, but to make a new kernel available for Jupyter clients, its executable/script should be located at one of the default paths (or some defined in the JUPYTER_PATHenv variable), inside its own directory, accompanied by a kernel.json configuration file.
Mine looks like this:
The first line represent the command-line arguments passed to the client to use our kernel: the first one is the executable path (guess it shoul be relative, but I didn’t get how do it so far), the second, “{connection_file}”, is replaced by the shared connection file name.
After that, we can verify the available kernels list:
Now, guess which format is used for the frontend-backend message passing… Would you believe it is also JSON ?
“It must be hard to read/parse/write all that, right ?”
Well, I didn’t even think about it, as I immediately start looking for an easier solution (laziness can, paradoxically, be a driving force). If even C has nice libraries for this task (like Klib or Json-C), how on earth C++ wouldn’t have it ?
Fairly quickly, I ended up finding the JsonCpp library. It is so simple that even the author recommends that you bundle its source with your application (which is fine, since both my code and his use the MIT license*).
JsonCpp is really nice: it allows you to serialize a JSON object/dictionary to a string or deserialize a JSON string to a dictionary in a single call (actually, a overloaded operator). I know that there is no magic in computing, but abstraction still amazes me sometimes…
At long last, let’s put it all to use to set up or connections properly and, for now, just handle the kernel shutdown message, sent from the client as a multi-part JSON string:
Relevant code:
For testing it, I just ran the Jupyter QtConsole and closed it, which makes it emit the shutdown message:
Apart from some weird formatting issues that I have to check later (thread synchronization, maybe), now you can see that we are binding to the proper ports, receiving Heartbeat messages and, at the end, reading and parsing a shutdown message correctly.