** Documentation for MX. ** ** Copyright: (c) 2014-2022 Jacco van Schaik (jacco@jaccovanschaik.net) ** ** This software is distributed under the terms of the MIT license. See ** http://www.opensource.org/licenses/mit-license.php for details.
MX is a light-weight communication library written in C. It provides programs that use it with a way to publish and subscribe to messages. There is no predetermined structure to the payload of a message. To MX, a message payload is a simple, opaque byte string.
MX is not designed to be in operation 24/7. Although it is possible, with some care, to build such a system using MX, that is not what MX was designed for. It is assumed that systems built on top of MX will be started, run for some time and then be shut down.
(The reason for this is that message types, once created, are never re-used, which makes them a limited resource. If you design a system that only allocates a fixed number of message types it should be possible to run it indefinitely, but MX will not prevent you from exhausting the available message types. On the other hand, a message type is a 32-bit unsigned integer so it'll take a while to exhaust that supply).
MX is not designed to be particularly resilient against external hardware or software failures. It is assumed that a run-time failure in any of the participating components signifies a serious problem that will be rectified before trying again. MX will report the failure but will not try to restart the failed component, for example.
Also, the master component constitutes an obvious single point of failure. If it crashes or is killed all other participating components will also exit. This should not be a big problem for a standalone master (started using mx master) because it is fairly robust, but if another component assumes the master role (see the mxMaster function) and it exits or crashes, all other participating components will exit too.
Finally, security was not a big design consideration for MX. If you can connect to the master component it is trivially easy to kill it with a hand-crafted message (in fact, this message was specifically designed to make it so). This will then cause all other components to exit too. Furthermore, components connect to each other without any authentication, and messages are exchanged without any encryption. MX is expected to run on a private network where all connected devices can be trusted.
TL;DR: If you are planning to use MX for a nuclear power station you should probably reconsider.
A message exchange is like a stock exchange, but instead of connecting buyers and sellers of stocks it connects publishers and subscribers of messages. It allows connected programs (known as components) to subscribe to certain types of messages. Other components can broadcast messages, and they will automatically be sent to those components that have subscribed to that particular message type. It is also possible for components to send messages directly to another component if needed.
One of the components in a message exchange is the master. It is started first, and it maintains the central store of message types, all participating components and their subscriptions. All other components (known as clients) are informed about changes in this central store by messages from the master and use these to update their own copy of it. The master component can be a standalone program but its role can also be incorporated into one of the other components. There can, however, only be one master component in any message exchange, and if it crashes or exits all connected client components will exit too.
Client components exchange messages directly; they don't all go through the master component. The connections between all components end up looking like a star network: all components have a single connection to every other component.
This is the name of the message exchange. The MX name is determined using the following steps:
The MX name is hashed to generate a port number between 1024 and 65535 inclusive. The master listens on this port on all interfaces and the clients connect to it. It is therefore a fixed "access point" to the message exchange. All the other port numbers used by MX are allocated dynamically by the operating system. This scheme makes it possible to run many message exchanges on a single host (assuming the used MX names don't result in the same hash value, and assuming you don't simply run out of IP ports).
This is the host where the master component for a message exchange runs. A message exchange is identified by the combination of the host on which it runs and its MX name. It is therefore possible to run multiple, separate message exchanges with the same MX name on differents hosts, and it is possible to run multiple message exchanges on the same host as long as they have different names.
The master component binds its listen port to all available interfaces. The other components use the following steps to determine the MX host to connect to:
A message type is a unique ID assigned to a kind of message (not to individual messages!) They are allocated starting at 0, but message types 0 through 9 are used by MX itself (see the Message structure section for a description of these messages). Message types available to the user therefore start at 10. A message type is a 32-bit unsigned integer, so there are 232 = 4,294,967,296 message types available in total. If that isn't enough it is also possible to transmit a version number along with the message. The version number is also a 32-bit unsigned integer.
Message types must be registered before they can be used. Components do this by calling the mxRegister function. They can pass in a name by which the message type will henceforth be known. If any other component registers a message with the same name they will get the same message type. This way, message types are coordinated between components.
Components can also register an anonymous message (by using NULL for the message name). This allocates a new message type without associating it with a name. This way components can allocate a message type for their own private use.
Message types are never reused, so it is possible to exhaust the number of message types in a single run, depending of course on how quickly they are used up.
MX comes with an executable, appropriately called "mx". Typing just mx or mx help will show the following usage information:
Usage: mx <command> [ <options> ] Commands: help Show help master Run a master component name Print the effective MX name host Print the effective MX host port Print the effective MX port list Show a list of participating components quit Ask the master component to exit version Show the current version of MX Use "mx help <command>" to get help on a specific command.
In a program that uses MX, there are two phases to be distinguished: first there is some setup, then you enter the message loop.
In the setup phase you do the following:
There are two ways to run the message loop: you can call the mxRun function which will handle everything for you and exit when the user calls mxShutdown, or listen for events yourself and call the mxProcessEvents function to have MX handle them.
By the way, it is perfectly fine to register new messages and/or subscribe to them once the message loop is running.
Return the mx_name that would be used if mx_name were to be given to mxClient. If it is a valid name (i.e. not NULL) that same name is returned. Otherwise, if the environment variable MX_NAME is set it is returned. Otherwise, if the environment variable USER (the user's login name) is set it is returned. Otherwise MX gives up and NULL is returned.
Return the listen port that the master component will use for the message exchange with name mx_name. The same algorithm as indicated for mxEffectiveName is used to determine the effective mx_name.
Return the mx_host that would be used if mx_host were to be given to mxClient or mxMaster. If it is a valid name (i.e. not NULL) it is returned. Otherwise, if the environment variable MX_HOST is set it is returned. Otherwise "localhost" is returned.
Connect to the message exchange with MX name mx_name at mx_host and introduce ourselves as my_name. Creates and returns a new MX struct.
Start a new message exchange with MX name mx_name. The new MX struct that is returned can be used as if it were a regular component. In the background, however, this component will also perform the duties of a master component.
If background is set to true, this master component will put itself in the background after it has opened its listen socket. This is useful if you are starting all components in a shell script: you can start the master component first and have it put itself into the background, which means the foreground process will exit. Once the foreground process has exited you can start any additional client components which will find the master component's listen port already waiting for them so they can connect to the master component without delay.
This "backgrounding" is done by using a fork() system call, which has the unfortunate consequence that any threads started before the call to mxMaster will be killed. So if you want to background the master component and also start additional threads, do the latter after calling this function.
Return the name of the local component, as passed to mxClient or mxMaster as my_name.
Return the current MX name for mx.
Return the current MX host, i.e. the host where mx's master component is running.
Return the current MX port, i.e. the port on which the master for mx listens.
Register the message named msg_name. Returns the associated message type. If you pass in NULL for msg_name, an anonymous message type is allocated and returned to you. This message type will not be issued to anyone else.
Returns the name associated with message type type.
Returns the name of the component connected on fd fd.
Subscribe to messages of type type. handler will be called for all incoming messages of this type (unless you are explicitly waiting for it by calling any of the Wait or Await functions). The udata parameter that you pass into this function will be passed back to you unchanged in the callback.
Cancel your subscription to messages of type type.
Call handler for new subscribers to message type type, passing in the same udata that was passed in here. fd is the file descriptor on which we are connected with the component that has subscribed to type.
Call handler when a subscriber cancels their subscription to messages of type type.
Call handler when a new component reports in. handler is called with the file descriptor through which we're connected to the new component in fd, and its name in name.
Call handler when connection with a component is lost. handler is called with the file descriptor through which we were connected to the new component in fd, and its name in name.
Call handler when a new message type is registered.
Send a message of type with version to file descriptor fd. Send payload payload which has size size.
The message is sent even if the destination component is not subscribed to it. It is usually used in combination with one of the Wait or Await functions in the destination (this goes for all of the Send functions).
Write a message of type type with version version to file descriptor fd over message exchange mx. The payload of the message is constructed using the PACK_* method as described in libjvs/utils.h.
Write a message of type type with version version to file descriptor fd over message exchange mx. The payload of the message is constructed using the PACK_* arguments contained in ap, as described in libjvs/utils.h.
Broadcast a message with type type, version version and payload payload with size size to all subscribers of this message type.
Broadcast a message with type type and version version to all subscribers of this message type. The payload of the message is constructed using the PACK_* method as described in libjvs/utils.h.
Broadcast a message with type type and version version to all subscribers of this message type. The payload of the message is constructed using the PACK_* method as described in libjvs/utils.h.
Wait for a message of type type to arrive on file descriptor fd. If the message arrives within timeout seconds, 1 is returned and the version, payload and payload size of the message are returned via version, payload and size. Otherwise, 0 is returned and version, payload and size are unchanged.
Send a message of type request_type with version request_version, payload request_payload and payload size request_size to file descriptor fd, and wait for a reply with type reply_type. If the reply arrives within timeout seconds, 1 is returned and the version, payload and payload size of the reply are returned via reply_version, reply_payload and reply_size. Otherwise, 0 is returned and reply_version, reply_payload and reply_size are unchanged.
Send a message of type request_type with version request_version and the payload that follows (specified using the PACK_* method from libjvs/utils.h) to file descriptor fd, and wait for a reply with type reply_type. If the reply arrives within timeout seconds, 1 is returned and the version, payload and payload size of the reply are returned via reply_version, reply_payload and reply_size. Otherwise, 0 is returned and reply_version, reply_payload and reply_size are unchanged.
Send a message of type request_type with version request_version and the payload that follows in ap (specified using the PACK_* method from libjvs/utils.h) to file descriptor fd, and wait for a reply with type reply_type. If the reply arrives within timeout seconds, 1 is returned and the version, payload and payload size of the reply are returned via reply_version, reply_payload and reply_size. Otherwise, 0 is returned and reply_version, reply_payload and reply_size are unchanged.
Create a timer that will call handler at time t (seconds since the UNIX epoch). In future calls to mxAdjustTimer and mxRemoveTimer this timer will be identified by id. When calling handler, the same pointer udata given here will be passed back.
Adjust the time of the timer with id id to t.
Remove the timer with id id. This timer will not be triggered after all.
Return the current UNIX timestamp as a double-precision floating point number.
Return the file descriptor on which all events associated with mx arrive.
Process any pending events associated with mx.
Start the event loop for mx.
Shut down mx. After this function is called, the mxRun function will return.
Destroy mx. Call this function only after mxRun has returned.
MX messages consist of two parts: the header and the payload. The header has a fixed size of 12 bytes and looks like this:
It contains:
The header is followed by the payload, consisting of as many bytes as was given in the payload size. MX imposes no structure whatsoever on the payload. To MX, the payload is an opaque string of bytes.
All messages are exchanged over TCP/IP connections.
The first 10 message types (0 to 9) are used by MX itself. This section describes these messages.
They fall into four categories:
All integers in these messages are sent in big-endian or network byte order format, with the most significant byte transmitted first.
This message type asks the master component to quit. It has type 0, version 0 and no payload, so it is the simplest message type, consisting of just 12 null-bytes. If the master component quits, all attached components will also quit when they see their network connection to the master go down.
This message is sent by normal components to the master to introduce themselves. It contains the component's name and the port on which it can be reached (the master determines the new component's IP address by looking at the connection over which this message is received).
This message is sent by the master as a reply to a normal components HelloRequest message. It informs the component of the name that the master is running under. For the standalone master, started using mx master, this is always "master". But if another component has assumed the role of master (by calling mxMaster instead of mxClient during setup), this may be a different name.
This message is sent by the master to all connected components to report a new component.
This message is sent by clients after they've connected to another client to introduce themselves.
This message is sent by normal components to the master to register a message type.
This message is sent by the master to a normal component as a reply to a register request.
This message is sent from the master to a normal component to report a newly registered message.
This message is sent between normal components to tell the recipient about a new subscription by the sender.
This message is sent between normal components to tell the recipient about a cancelled subscription by the sender.
Messages are identified with a name and a numeric message type. Registering a message is a way to link an ASCII name (which is more convenient for the user) with an integer type (which is more convenient for the system). Each name has one and only one associated type, so if multiple components register messages with the same name, they are guaranteed to get the same type. Messages are registered once before their first use, and solely referenced by their type after that.
Messages are registered using the mxRegister function. First of all, this function checks the local database to see if it already knows about the given message, If so, it immediately returns the associated message type to the caller. Otherwise it sends a RegisterRequest message to the master.
When the master receives the RegisterRequest message, it checks its database for the given message name. If found, it retrieves the associated type. Otherwise a new type is generated (simply by increasing a counter) and stored in the database, along with the name. The message type is then returned to the component in a RegisterReply message.
If the master had to generate a new type it also sends all other existing components a RegisterReport message to inform them of the new message type (this is the reason that a component might already have the message type in its local database, even though no mxRegister call has yet been made for it).
When the component receives the RegisterReply message it stores the message's name and associated type in its database, and the mxRegister function returns the new message type.
All message names are associated with a unique message type, but not all message types have to have an associated message name. Components can ask the master for a unique message type that isn't associated with a message name. They do this by calling the mxRegister function with a NULL message name, and they will get a unique message type every time. This can then be distributed to other components, and used without the fear of getting message type collisions.
This section describes how a new component is added to an existing message exchange. We'll assume that we already have a master (M) and a component (C1) running. These are the events that take place when the new component, C2 calls the mxClient function:
mxClient opens a listen socket, but without binding it to a port number, in which case the operating system chooses a random free port and binds the listen socket to it. The mxClient function then connects to the master and sends a HelloRequest message containing the component's name and the listen port number that the OS has chosen. The master stores these attributes for C2, along with its host name, which it determines by looking at the "peer name" of the component's TCP connection.
The master replies to the HelloRequest message with a HelloReply message. This contains the name that the master is running under.
Usually, the master role will be fullfilled by the mx executable, and in this case its name will simply be "master". But it is also possible for another component to fullfill this role (by calling the mxMaster function instead of mxClient), and in that case it may be running under a different name.
For each component that the master already knows about (only C1 in this example but there can be more), it sends a HelloReport back to C2. These messages contain the name, hostname and listen port of an existing component.
Next, the master sends a RegisterReport message to C2 for each message type that has already been registered.
Finally, the master sends C2 a SubscribeUpdate message for each of its subscriptions.
At this point the master component considers its work done and returns to listening for new connections and messages.
For each HelloReport message that was received, the mxClient function connects to the component described in the message and sends it a HelloUpdate message to introduce the new component C2. This message contains only C2's name.
It then sends the existing component a SubscribeUpdate message for each of its subscriptions.
At the same time, C1 responds to the new connection by sending its subscriptions to C2, again using SubscribeUpdate messages
After this, C2 is fully integrated into the MX system and mxClient returns.
It is worth noting that the SubscribeUpdate messages being sent to-and-fro are not unique to the setup phase. If a component calls the mxSubscribe function at any later time, the same message type is used to communicate the new subscription to all other components.
This section describes the threads associated with an MX component.
First of all, the MX main loop runs in the application's main thread. It may consist of the application repeatedly calling mxProcessEvents whenever data is available on the file descriptor returned by mxConnectionNumber, or of the application's single call to the mxRun function, which does essentially the same.
In addition, a number of separate threads handle communication with the outside world. The main loop sends commands (if necessary) to these threads using command queues, and they report back events through an event pipe (a standard UNIX pipe). This means that any event that the main loop needs to respond to comes in through a single file descriptor, and it is this file descriptor that is returned by the mxConnectionNumber function. When an event does come in, a subsequent call to the mxProcessEvents function reads it from the event pipe and handles it.
The following threads exist:
Then, for every connection to another component there are two additional threads:
The timer and writer threads exit when an explicit "exit" command comes in over their command queue. The listener and reader threads exit when the main loop shuts down the TCP sockets that they are connected to.