In the last post we have sent a message to a message queue on the Artemis server. We did this by utilizing the STOMP client service program available at repo.rpgnextgen.com.

In this part of the blog series we want to receive the message we sent in our last episode.

The message was sent with the content type application/octet-stream (constant STOMP_MIME_BINARY) which describes that we sent a bunch of binary data. The data is encoded with the CCSID of the job.

This works fine as long as we are staying on the same server and/or have full control over our environment. But it may not work so good if different parties need to communicate with each other.

So how does the code look like for receiving data?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
1   **FREE
2
3 ctl-opt main(main);
4
5 /include 'stomp.rpginc'
6
7 dcl-proc main;
8 dcl-s client pointer;
9 dcl-s frame pointer;
10 dcl-ds frameBuffer likeds(stomp_util_buffer_t);
11
12 client = stomp_create('127.0.0.1' : 40002);
13 stomp_setClientId(client : 'mihael');
14
15 stomp_open(client);
16 stomp_command_connect(client : 'admin' : 'udontknow');
17 stomp_command_subscribe(client : 'auction-events');
18
19 // start receiving data
20 frame = stomp_receiveFrame(client);
21 dow (frame <> *null);
22 if (stomp_frame_getCommand(frame) = 'ERROR');
23 dsply 'An error occured';
24 leave;
25 endif;
26
27 // display message content
28 frameBuffer = stomp_frame_getBody(frame);
29 message_info(%str(frameBuffer.data : frameBuffer.length));
30 stomp_frame_finalize(frame);
31
32 // receive next frame
33 frame = stomp_receiveFrame(client);
34 enddo;
35
36 stomp_command_disconnect(client);
37 stomp_close(client);
38
39 on-exit;
40 stomp_finalize(client);
41 end-proc;

Lets have a look at the code line by line. We start exactly the same as the sending programing.

Line 1 : We are writing fully free RPG code. Not half-assing this already from the start ;-)
Line 3 : We will compile this program in two steps. Thus we only declare the main procedure in the control options.
Line 5 : The STOMP copybook needs to be included so that we can use the constants and prototypes from the STOMP project. Everything else will be added to the compile commands.
Line 8 : For the client a pointer variable is declared. We are not interested what lies behind that pointer. We just pass it to the STOMP procedures.
Line 9 : A variable for the received frames is declared.
Line 10 : The frame body is accessed via a pointer. The frameBuffer data structure contains this pointer (data) and the length of the data which lies behind the data pointer.
Line 12 : An instance of the STOMP client is created. The client is accessed via the pointer. The internal data of the client doesn’t matter to us. We are only using the procedures from the STOMP project anyway. The host and port we want to connect to are passed to the create procedure. The port should be the same we used in the Artemis configuration for our STOMP acceptor.
Line 13 : A client id is set on the client. This id can be used to identify this specific client connection to the server in the Artemis web console. The id is just a string and can be set to any value.
Line 15 : A socket connection to the Artemis server instance is created.
Line 16 : To connect on a (STOMP) protocol level we need to send a STOMP CONNECT frame/command to the server. This also includes the credentials used to login to the message broker. For an anonymous connect we can just pass the client pointer.
Line 17 : We need to tell the Artemis instance from which queue we want to receive messages. This is done by subscribing to a queue or topic (or in Artemis lingo: address).
Line 20 - 21 : Here we are starting the loop for receiving messages (STOMP lingo: frames) from the Artemis instance. The program will wait here till the next message has been received. stomp_receiveFrame will return *NULL if the timeout has been reached before any frame has been received. We can change the timeout on the client by calling stomp_setTimeout.
Line 22 : The server may also send an ERROR frame to indicate that something went wrong. We need to be ready for that and react accordingly.
Line 28 : We are mostly interested in the body of the message. We get that with stomp_frame_getBody. The returned data structure contains a pointer to the message body and a subfield for the length. We could also get the headers of the frame with stomp_frame_listHeaders and stomp_frame_getHeader if we needed to.
Line 29 : We are expecting some string data in the same CCSID as our job. Because of that we can use the %str built-in function which returns a string based on the passed pointer.
Line 30 : Evey stomp frame we receive has its memory dynamically allocated. We need to free this memory by calling stomp_frame_finalize.
Line 36 : To properly disconnect from the Artemis server instance after we have finished the loop we need to send a DISCONNECT frame to the server.
Line 37 : After disconnecting we can close the socket connection.
Line 40 : At the end we also need to free the memory allocated by the client.

We will see the content of the message in the job log (output by the message_info procedure from the MESSAGE service program).

Durable Subscriber

On subscribing to a message queue Artemis will create a queue for our client which receives all the messages for which we have subscribed. For as long as our program runs we will receive those messages. But Artemis will delete this queue (not the address) when we disconnect. This means that we might miss some messages. STOMP has no concept of a persistent subscriptions. But Artemis supports Durable Subscriptions when using the STOMP protocol. From the Artemis docs:

To create a durable subscription the client-id header must be set on the CONNECT frame and the durable-subscription-name must be set on the SUBSCRIBE frame. The combination of these two headers will form the identity of the durable subscription.

The STOMP service program has dedicated procedure for setting those values.

1
2
3
4
5
dcl-s client pointer;

client = stomp_create('127.0.0.1' : 40002);
stomp_setClientId(client : 'mihael');
stomp_setDurableSubscriberName(client : 'auctions');

Now we won’t miss any messages.

Content Type

Currently the message itself doesn’t convey anything about the message itself. The content type is just application/octet-stream which means that it can be anything. It is just a bunch of bytes. The receiving program needs to know a lot about the sending program and thus has a tight coupling to it. And as long as we just want to decouple parts of the execution of process and have full control over the environment this might by ok.

But when other players enter the game this situation might not be so convenient. The data format is just a data structure (which doesn’t have any meaning anywhere other than RPG where we could share the data structure via a copy book). The encoding (EBCDIC) is also not very mainstream and thus not necessarily generally available on other platforms / programming languages.

If a Java program needs to receive an auction message it could use the JTOpen project for getting things together (data format and encoding) but that would be rather tedious.

If a Node.js program needs to receive this message you would be rather hard pressed to get things to work.

Solution

One possible solution would be to send the data as a JSON string. JSON is a very universal format and has the default encoding of UTF-8 which is also widely available. Thus you are not limited to your own platform but are also able to invite other players into the game without having to change anything on your side.

Multiple Formats

On the receiving side we might want to support different formats or versions of the data. One way to implement this would be to publish the format on you intranet / extranet in some way and use a custom content type. Content types are not limit by the strings we see so often on the internet like application/json or text/html. You can use whatever you want though there are some best practices, see Media Type Specifications.

If we want to convey that this is an auction we could use the content type application/x.rpgnextgen.auction. There is an established way of adding additional information like format and version to the content type by appending the info with an add sign.

1
application/x.rpgnextgen.auction+json+v1

Now the receiver knows pretty well from the message alone what he can expect of the message body.

We can query the frame header by using stomp_frame_getHeader.

1
stomp_frame_getHeader(frame : 'content-type');

JSON

The noxDB project in its master branch supports parsing a JSON string from a pointer variable out-of-the-box. We can just pass the pointer from the frame buffer to noxDB.

1
2
3
4
5
6
7
8
9
10
dcl-ds frameBuffer likeds(stomp_util_buffer_t);
dcl-s json pointer;

...

frame = stomp_receiveFrame(client);

json = jx_parseString(frameBuffer.data);

jx_close(json);

In the utf8 branch which is packaged for use with iPKG we need to take a little extra effort into account as we cannot directly pass the pointer. We need to pass the data into a string variable and pass that extra variable to the noxDB procedure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dcl-ds frameBuffer likeds(stomp_util_buffer_t);
dcl-s json pointer;
dcl-s string like(UTF8);

...

frame = stomp_receiveFrame(client);
frameBuffer = stomp_frame_getBody(frame);
string = %str(frameBuffer.data : frameBuffer.length);
json = nox_parseString(string);

...

jx_close(json);

API Documentation

You can find the API documentation of the STOMP for ILE project and noxDB project at iledocs.rpgnextgen.com. For those interested in generating API documentation for ILE programs and service programs: The documentation at iledocs.rpgnextgen.com has been generated with the project iledocs-page-builder.

Happy integrating!

Mihael