Philip Withnall
October 31, 2014
Reading time:
For the past several months, Olivier Crête and I have been working on a project using libniceat Collabora, which is now coming to a close. Through the project we’ve managed to add a number of large, new features to libnice, and implement hundreds (no exaggeration) of cleanups and bug fixes. All of this work was done upstream, and is available in libnice 0.1.8, released recently! GLib has also gained a number of networking fixes, API additions and documentation improvements.
tl;dr: libnice now has a GIOStream implementation, support for scatter–gather send and receive, and more mature pseudo-TCP support — so its API should be much nicer to integrate; GLib hasgained a number of fixes.
Firstly, what is libnice? It’s a GLib implementation of ICE, the standard protocol for NAT traversal. Briefly, NAT traversal is needed when two hosts want to communicate peer-to-peer in a network where there is at least one NAT translator between them, meaning that at least one of the hosts cannot directly address the other until a mapping is created in the NAT translator. This is a very common situation (due to the shortage of IPv4 addresses, and the consequence that most home routers act as NAT translators) and affects virtually all peer-to-peer communications. It’s well covered in the literature, and the rest of this post will assume a basic understanding of NAT and ICE, a topic about which I recently gave a talk.
Conceptually, libnice exists just to create a reliable (TCP-like) or unreliable (UDP-like) socket which connects your host with a remote one in a manner that traverses any intervening NATs. At its core, it is effectively an implementation of send(), recv(), and some ancillary functions to negotiate the ICE stream at startup time.
The biggest change is the addition of nice_agent_get_io_stream(), and the GIOStream subclass it returns. This allows reliable ICE streams to be used via GIOStream, with all the API sugar which comes with GIO streams — for example, g_output_stream_splice(). Unreliable (UDP-like) ICE streams can’t be used this way because they’re not technically streams.
Highly related, the original receive API has been augmented with scatter–gather support in the form of a recvmmsg()-like API: nice_agent_recv_messages(). Along with appropriate improvements to libnice’s underlying socket implementations (the most obscure of which are still to be plumbed in), this allows performance improvements by batching messages, reducing the number of system calls needed for communication. Furthermore (perhaps more importantly) it reduces memory copies when assembling and parsing packets, by allowing the packets to be split across multiple non-contiguous buffers. This is a well-studied and long-known performance technique in networking, and it’s nice that libnice now supports it.
So, if you have an ICE connection (stream 1 on agent, with 2 components) exchanging packets with 20B headers and variable-length payloads, instead of:
nice_agent_attach_recv (agent, 1, 1, main_context, recv_cb, NULL); nice_agent_attach_recv (agent, 1, 2, main_context, recv_cb, NULL); … static void recv_cb (NiceAgent *agent, guint stream_id, guint component_id, guint len, const gchar *buf, gpointer user_data) { if (stream_id != 1 || (component_id != 1 && component_id != 2)) { g_assert_not_reached (); } if (parse_header (buf)) { if (component_id == 1) parse_component1_data (buf + 20, len - 20); else parse_component2_data (buf + 20, len - 20); } } … static void send_to_component (guint component_id, const gchar *data_buf, gsize data_len) { gsize len = 20 + data_len; guint8 *buf = malloc (len); build_header (buf); memcpy (buf + 20, data, data_len); if (nice_agent_send (agent, 1, component_id, len, buf) != len) { /* Handle the error */ } }
you can now do:
/* Only set up 1 NiceInputMessage as an illustration. */ static guint8 buf1_1[20]; /* header */ static guint8 buf1_2[1024]; /* payload size limit */ static GInputVector buffers1[2] = { { &buf1_1, sizeof (buf1_1) }, /* header */ { &buf1_2, sizeof (buf1_2) }, /* payload */ }; static NiceInputMessage messages[1] = { buffers1, G_N_ELEMENTS (buffers1), NULL, 0 }; GError *error = NULL; n_messages = nice_agent_recv_messages (agent, 1, 1, &messages, G_N_ELEMENTS (messages), NULL, &error); if (n_messages == 0 || error != NULL) { /* Handle the EOS or error. */ if (error != NULL) g_error ("Error: %s", error->message); return; } /* Component 2 can be handled similarly and code paths combined. */ for (i = 0; i < n_messages; i++) { NiceInputMessage *message = &messages[i]; if (parse_header (message->buffers[0].buffer)) { parse_component1_data (message->buffers[1].buffer, message->buffers[1].size); } } … static void send_to_component (guint component_id, const gchar *data_buf, gsize data_len) { GError *error = NULL; guint8 header_buf[20]; GOutputVector vec[2] = { { header_buf, sizeof (header_buf) }, { data_buf, data_len }, }; NiceOutputMessage message = { vec, G_N_ELEMENTS (vec) }; build_header (header_buf); if (nice_agent_send_messages_nonblocking (agent, 1, component_id, &message, 1, NULL, &error) != 1) { /* Handle the error */ g_error ("Error: %s", error->message); } }
libnice has also gained non-blocking variants of its I/O functions. Previously, one had to explicitly attach a libnice stream to a GMainContext to start receiving packets. Packets would be delivered individually via a callback function (set with nice_agent_attach_recv()), which was inefficient and made for awkward control flow. Now, the non-blocking I/O functions can be used with a custom GSource from g_pollable_input_stream_create_source() to allow for more flexible reception of packets using the more standard GLib pattern of attaching aGSource to the GMainContext and in its callback, callingg_pollable_input_stream_read_nonblocking() until all pending packets have been read. libnice’s internal timers (used for retransmit timeouts, etc.) are automatically added to theGMainContext passed into nice_agent_new() at construction time, which you must run all the time as before.
GIOStream *stream = nice_agent_get_io_stream (agent, 1, 1); GInputStream *istream; GPollableInputStream *pollable_istream; istream = g_io_stream_get_input_stream (stream); pollable_istream = G_POLLABLE_INPUT_STREAM (); source = g_pollable_input_stream_create_source (pollable_istream, NULL); g_source_set_callback (source, readable_cb, NULL, pollable_istream); g_source_attach (main_context, source); static gboolean readable_cb (gpointer user_data) { GPollableInputStream *pollable_istream = user_data; GError *error = NULL; guint8 buf[1024]; /* whatever the maximum packet size is */ /* Read packets until the queue is empty. */ while ( (len = g_pollable_input_stream_read_nonblocking (pollable_istream, buf, sizeof (buf), NULL, &error) ) > 0) { /* Do something with the received packet. */ } if (error != NULL) { /* Handle the error. */ } }
libnice also gained much-improved support for restarting individual streams using ICE restarts with the addition of nice_agent_restart_stream(), switching TURN relays withnice_agent_forget_relays(), plus a number of bug fixes.
Finally, FIN/ACK support has been added to libnice’s pseudo-TCP implementation. The code was originally based on Google’s libjingle pseudo-TCP, establishing a reliable connection over UDP by encapsulating TCP-like packets within UDP. This implemented the basics of TCP, but left things like the closing FIN/ACK handshake to higher-level protocols. Fine for Google, but not for our use case, so we added support for that. Furthermore, we needed to layer TLS over a pseudo-TCP connection using GTlsConnection, which required implementing half-duplex close support and fixing a few nasty leaks in GTlsConnection.
Thanks to the libnice community for testing out the changes, and thanks to the GLib developers for patiently reviewing the stream of tiny documentation fixes and several larger GLib patches! All of the libnice API changes are shown on the handy upstream-tracker.org tool.
19/12/2024
In the world of deep learning optimization, two powerful tools stand out: torch.compile, PyTorch’s just-in-time (JIT) compiler, and NVIDIA’s…
08/10/2024
Having multiple developers work on pre-merge testing distributes the process and ensures that every contribution is rigorously tested before…
15/08/2024
After rigorous debugging, a new unit testing framework was added to the backend compiler for NVK. This is a walkthrough of the steps taken…
01/08/2024
We're reflecting on the steps taken as we continually seek to improve Linux kernel integration. This will include more detail about the…
27/06/2024
With each board running a mainline-first Linux software stack and tested in a CI loop with the LAVA test framework, the Farm showcased Collabora's…
26/06/2024
WirePlumber 0.5 arrived recently with many new and essential features including the Smart Filter Policy, enabling audio filters to automatically…
Comments (0)
Add a Comment