The Internet of Things is essentially a concept that describes a system of interconnected devices that use the Internet for communication. But, oddly enough, one of the primary challenges of IoT is precisely ensuring this Internet-based communication between devices. Modern TCP/IP-enabled devices, even those built on low-cost microcontrollers, might be powerful enough to implement on-device business logic and communicate with clients over IP networks. Typically, they operate in dynamic IP environments, which pose significant connectivity challenges. In most cases, interaction with such devices is impossible without the use of a specially designed communication platform. In fact, along with connectivity, there is a set of additional tasks that an IoT platform is assumed to solve such as security, access control, device administration, shared access to devices, etc.
In this publication, I'd like to present you with Softnet Free, a communication platform for devices with an embedded TCP/IP stack. This is an open source solution distributed under the Apache 2.0 license. Softnet Free makes the TCP and UDP protocols available for use in dynamic IP environments. The platform offers built-in Remote Procedure Calls as an additional IPC mechanism, and Pub/Sub Events used to implement event-driven interaction scenarios. The platform also supports notifications used in asynchronous communication. The Softnet mechanism of Access Control can be used to implement different levels of access to devices, and a mechanism called Service Status Detection provides clients with information about the status of remote devices. Softnet Free also provides a convenient solution for managing devices, clients, user permissions, and shared access to devices with other individuals/organizations. The latter allows owners to establish user-to-user or business-to-customer relationships.
Support for Softnet Free developers is provided in Q&A of the project.
Want to get in touch? My email: robert.koifman (et) gmail.com.
Current State of the ProjectSoftnet Free has recently been released and now awaits evaluation from developer communities. The platform's functionality can be divided into two categories: the core functionality and the extension support. The first one is presented by the mechanisms listed in the introduction and described below. The second one, the extension support, is responsible for enabling the seamless integration of well-established protocols such as HTTP, WebSocket, SSH, RTSP, etc., within the IoT/IIoT domain, along with the corresponding software and utilization techniques. This part of the platform is under development. For now, I invite you to assess the core functionality and provide your feedback.
Currently, an endpoint library for use in Java applications is available. The library utilizes data types and packages from Java SE 1.7 and runs on Linux platforms, embedded Linux, Armbian, Raspbian, Android 5.0 and later. The endpoint libraries for other platforms are under development. First of all, we focus on FreeRTOS, Zephyr, RIOT, Contiki and other platforms widely used in industrial microcontrollers such as STM32, TI SimpleLink, Infineon XMC, ESP32, etc.
A Quick Guide Before We BeginIn order to run the code snippets and examples provided below, you're assumed to have a Softnet MS account and be in the “Provider” role. For this purpose, I suggest you use a demo server with free registrations. You can also deploy your own server. All the necessary resources for this are published on GitHub.
To utilize the Softnet platform, Java applications use the following two libraries: Softnet Endpoint Library (Java) and Softnet ASN.1 Codec (Java). For setting up the libraries, see here. If your application is assumed to use the Java 1.7 or Java 1.8 runtime, you can also target these libraries to that runtime. In this case, to compile these libraries you need to comment out the code in 'src/module-info.java'. Otherwise, starting with Java 9, Java applications use Java Platform Module System (JMPS), and the code in 'src/module-info.java' is required to declare module-related information and dependencies. You can also download executables compiled in Java 1.7 and java 9 from the releases section of the repository.
As for the network infrastructure requirements, there is only one related to outbound TCP and UDP ports, which is described here.
When explaining the key features of the platform, I may refer you to the following guides for more detailed information:
- Softnet Programming Model in Java / This guide explains the process of developing embedded applications using Softnet Endpoint Library (Java);
- The Developer Guide to Softnet ASN.1 Codec (Java) / This guide explains how to use the Softnet ASN.1 codec in Java applications;
- The User Guide to Softnet Management System / This guide explains how to use Softnet MS for managing devices and clients utilizing the Softnet Free platform. It is designed for both developers and users.
In this chapter I want to show you the use of Softnet Free mechanisms. Devices and their clients utilizing this platform must be registered in a Softnet network. This is done through the Softnet management system (Softnet MS), which is primarily designed for device users. It is almost as easy to use as email or a social network. To deploy an IoT project, you create a domain in Softnet MS. It contains a list of users and one or more sites. Site is a key object of the platform. You create it to register a single device or multiple identical devices, as well as clients that interact with those devices. A device registered on the site is presented as a service with a specific interface.
In the first three sections, I show the creation of a test project in Softnet MS and two console applications with minimal code for a test service and a test client. I use them in the remaining sections to demonstrate the use of communication patterns, as well as mechanisms called Access Control and Service Status Detection.
1. Creating a test projectAs a developer, in order to test your Softnet code, you need a test project on the Softnet MS. So, let's create a Softnet MS account on the demo server. If registration on the Softnet MS is allowed, the New User
button is displayed in the upper right corner of the panel:
Clicking this button moves you to the New User panel:
Type your email address and click send confirmation mail
. In case of success, it prints: “The message has been successfully sent”. Then in the inbox of your email service, you will see a message like this:
If you follow the confirmation URL, you will be taken to the registration page:
Type the required parameters and create an account. If everything is fine, you’ll be taken to the Log In page:
After logging in, your home page may look like this:
Next, you need to create a new domain. Clicking on the Domains menu item moves you to the My Domains page:
Create a new domain and name it “My Test Project”. At the time of creation, it contains two built-in users and no sites:
Finally, you need to create a new site. Clicking the new site
button takes you to the New Site page:
You can leave the description empty and click Create
. This moves you to the Site Configuration page:
At this point, if you followed the instructions, you have created an account in Softnet MS, a new domain called "My Test Project" and a site in this domain, which is currently in the status "site blank". The <service> block on the site panel contains a service entity named "Service1". This is where you can setup a device utilizing Softnet Free. On the site, your device will be presented as a service. You can also host multiple identical services (i.e., devices) on a site if you turn it into a multi-service site. But here we don't need this. For now, we leave the site as is and move on to coding.
2. Creating a demo service applicationFor demo purposes, we need two applications: a service and a client. In this section, we'll create a service application with minimal code that will be used in the following sections. Open your favorite Java IDE and create a console application with a name like "ServiceTestApp". Then setup the endpoint library files. Add a class named, for example, “ServiceApp” and insert the method main into the class. Then, import the following packages:
import softnet.*;
import softnet.service.*;
import softnet.exceptions.*;
import softnet.asn.*;
A Softnet application, whether a service or a client, communicates with counterparts through the endpoint object. So for the service application, we need to create the service endpoint first. The platform provides the softnet.service.ServiceEndpoint class for this purpose. It has a static method to create an instance of the class:
public static ServiceEndpoint create(
SiteStructure siteStructure,
String version,
ServiceURI serviceURI,
String password) throws HostErrorSoftnetException
The first parameter of the method is an object that implements the SiteStructure interface. This object is used to specify the information required to construct the site on which the service will be hosted. The platform constructs the site automatically when the service connects to the site for the first time. If you remember, the site we created is currently in the status "site blank" (see Pic. 10). ServiceEndpoint has a static method to create the SiteStructure object:
public static SiteStructure createStructure(
String serviceType,
String contractAuthor)
The site structure requires at least two parameters – the Service Type name and the name of the Contract Author. These parameters compose the name of the service's interface contract, and serve as an identifier to refer to the specific functionality and behavior of the device hosting this service application. If you are a DIY developer, you can specify your project name and your own name as the service type and contract author respectively. On the site management panel, these names will be displayed as follows: <Your Project Name> (<Your Name>), for example: Home Thermostat (John Doe). In another case, the service type could be the device’s model name plus generation number, and the contract author could be the manufacturer’s name as follows: T-800 (Cyberdyne Systems).
Let's continue exploring the create method. The second parameter, version, is the version of the service’s primary API, which we'll set to “1.0”. Its role is described in section 9. And we have the last two parameters, serviceURI and password, which accept service account data. The device owner takes this data from the site configuration page in Softnet MS. So, we return to the site we created earlier and click the account
button:
Clicking the generate password
button generates a new password. In my case, I had the following service URI and password:
softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org 5Ur3pLBXSw
In real applications, this data is stored once, for example, in the application configuration file, and read each time the application needs it.
The service URI string is used to instantiate the ServiceURI class:
String service_uri =
"softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org";
ServiceURI serviceURI = new ServiceURI(service_uri);
At this point, we have everything to create the service endpoint. Your application might look like the following:
package serviceTestApp;
import softnet.*;
import softnet.exceptions.*;
import softnet.service.*;
import softnet.asn.*;
public class ServiceApp {
static final String apiVersion = "1.0";
private static ServiceEndpoint serviceEndpoint = null;
public static void main(String[] args) {
String service_uri = "softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org";
String password = "5Ur3pLBXSw";
try {
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
serviceEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(serviceEndpoint != null)
serviceEndpoint.close();
System.out.println("The service app is closed!");
}
}
}
After the endpoint is created, call the connect method. Important note! Do not forget to close the service endpoint in the end! I've done it in the 'finally' block.
There is a set of the service's state parameters controlled by the platform that an application may be interested in monitoring. For each of them, the platform generates an event when its value changes. Two of them - endpoint connectivity status and service status - can be useful to monitor in almost any service application. To set up an event listener, ServiceEndpoint has a method called addEventListener:
public void addEventListener(ServiceEventListener listener)
This method accepts an object that implements the ServiceEventListener interface with appropriate event handlers. Instead of implementing all the methods of ServiceEventListener, you can extend an abstract class ServiceEventAdapter which has empty implementations of these methods. In this case, you can just override the methods you need.
The following code snippet demonstrates intercepting events related to changes in endpoint connectivity status and service status:
serviceEndpoint.addEventListener(new ServiceEventAdapter()
{
@Override
public void onConnectivityChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
EndpointConnectivity connectivity = serviceEndpoint.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
System.out.println(String.format("Service status: %s", serviceEndpoint.getStatus()));
}
});
The following is the entire code of the service application:
package serviceTestApp;
import softnet.*;
import softnet.exceptions.*;
import softnet.service.*;
import softnet.asn.*;
public class ServiceApp {
static final String apiVersion = "1.0";
private static ServiceEndpoint serviceEndpoint = null;
public static void main(String[] args) {
String service_uri = "softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org";
String password = "5Ur3pLBXSw";
try {
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
serviceEndpoint.addEventListener(new ServiceEventAdapter()
{
@Override
public void onConnectivityChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
EndpointConnectivity connectivity = serviceEndpoint.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
System.out.println(String.format("Service status: %s", serviceEndpoint.getStatus()));
}
});
serviceEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(serviceEndpoint != null)
serviceEndpoint.close();
System.out.println("The service app is closed!");
}
}
}
Let's run the application and see if it can connect to the server. We also do it to provide the platform with the site structure which is attached to the endpoint. The platform will use it to construct the site which is currently in the status 'site blank' (see Pic. 11). While the endpoint is establishing the connection, if everything is fine, you application could print something like this:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Endpoint connectivity status: Disconnected, Error: RestartDemanded, Message: The softnet server demanded to restart the endpoint.
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
The meaning of these messages is as follows: on the first connection of the service endpoint, the server constructed the site, and then the server demanded the endpoint to reestablish the connection. When the endpoint connected for the second time, the service was assigned the status Online.
Now click the refresh
button on the site page, and you will see that the site's appearance has changed:
The platform automatically adds the Owner user to the site. This is shown in the <explicit users> block. Now we can create clients associated with this user.
3. Creating a demo client applicationIn your Java IDE create a console application. Add a class named, for example, "ClientApp" and insert the main method into the class. Then, import the following packages:
import softnet.*;
import softnet.client.*;
import softnet.exceptions.*;
import softnet.asn.*;
The softnet.client.ClientSEndpoint class provides the network functionality to a client application to communicate with a single remote service. It has a static method to create an instance of the class, i.e. a client endpoint:
public static ClientSEndpoint create(
String serviceType,
String contractAuthor,
ClientURI clientURI,
String password,
String clientDescription)
We set the first two parameters, serviceType and contractAuthor, as "Test Service" and "John Doe". This ensures that the client will be able to communicate only with a service which Service Type and Contract Author are the same. The next two parameters are clientURI and password. We get them from the <account> section of the client registration. But, first, we have to create this registration. On the site page shown in Pic. 12, clicking the clients
button in the upper left corner of the panel opens the Client Management page of the site:
Clicking the add client
button located to the right of Owner creates a client entity:
Clicking the generate password
button generates a new password:
In my case, I had the following values for the client URI and password:
softnet-s://hj1syian@ts.softnet-iot.org
ZW81jj8NQd
In real applications, this data is stored once, for example, in the application configuration file, and read each time the application needs it.
The client URI string is used to instantiate the ClientURI class:
String client_uri = "softnet-s://hj1syian@ts.softnet-iot.org";
ClientURI clientURI = new ClientURI(client_uri);
Now we have everything to create the client endpoint. Your application might look like the following:
package clientTestApp;
import softnet.*;
import softnet.asn.*;
import softnet.exceptions.*;
import softnet.client.*;
public class ClientApp {
static ClientSEndpoint clientSEndpoint = null;
public static void main(String[] args)
{
String client_uri = "softnet-s://hj1syian@ts.softnet-iot.org";
String password = "ZW81jj8NQd";
try {
ClientURI clientURI = new ClientURI(client_uri);
clientSEndpoint = ClientSEndpoint.create(
"Test Service",
"John Doe",
clientURI,
password,
"Test Client");
clientSEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(clientSEndpoint != null)
clientSEndpoint.close();
System.out.println("The client app is closed!");
}
}
}
As with the service application, we want to monitor endpoint connectivity status and client status. To set up an event listener, ClientSEndpoint has a method addEventListener:
public void addEventListener(ClientEventListener listener)
This method accepts an object that implements the ClientEventListener interface with appropriate event handlers. As with the service application, instead of implementing all the ClientEventListener methods, you can extend an abstract class ClientEventAdapter which has empty implementations of these methods. This allows you just to override the methods you want to implement.
Along with two events related to the status parameters described above, we need to intercept the ServiceOnline event, which notifies the client when the remote service comes online. To do so, we'll override the onServiceOnline handler of ClientEventAdapter. The code snippet for the event handlers might look like this:
clientSEndpoint.addEventListener(new ClientEventAdapter()
{
@Override
public void onConnectivityChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
EndpointConnectivity connectivity = client.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
System.out.println(String.format("Client status: %s", client.getStatus()));
}
@Override
public void onServiceOnline(RemoteServiceEvent e) {
System.out.println(String.format("Remote service online! Hostname: %s, API version: %s", e.service.getHostname(), e.service.getVersion()));
}
});
The following is the entire code of the client application:
package clientTestApp;
import softnet.*;
import softnet.asn.*;
import softnet.exceptions.*;
import softnet.client.*;
public class ClientApp {
static ClientSEndpoint clientSEndpoint = null;
public static void main(String[] args)
{
String client_uri = "softnet-s://hj1syian@ts.softnet-iot.org";
String password = "ZW81jj8NQd";
try {
ClientURI clientURI = new ClientURI(client_uri);
clientSEndpoint = ClientSEndpoint.create(
"Test Service",
"John Doe",
clientURI,
password,
"Test Client");
clientSEndpoint.addEventListener(new ClientEventAdapter()
{
@Override
public void onConnectivityChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
EndpointConnectivity connectivity = client.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
System.out.println(String.format("Client status: %s", client.getStatus()));
}
@Override
public void onServiceOnline(RemoteServiceEvent e) {
System.out.println(String.format("Remote service online! Hostname: %s, API version: %s", e.service.getHostname(), e.service.getVersion()));
}
});
clientSEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(clientSEndpoint != null)
clientSEndpoint.close();
System.out.println("The client app is closed!");
}
}
}
Let's run the application and see if it can connect to the server. While the endpoint is establishing the connection, if everything is fine, you application could print something like this:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Remote service online! Hostname: Service1, API version: 1.0
Client status: Online
Now click the refresh button on the Client Management page, and you will see that the appearance of the client entity has changed - the client description "Test Client" has been added:
At this moment, we have the necessary resources to begin exploring the key features of the platform.
4. TCP in dynamic IP environmentsTCP is the most widely used transport layer protocol, and in many cases the ability to establish direct TCP connections between devices can be crucial. In this section, the client app we created earlier establishes a TCP connection with the service app and exchanges hello messages.
A detailed guide to working with TCP is presented in Handling TCP connection requests and Making TCP connection requests.
4.1. The service codebase
In Softnet Free, service endpoints accept TCP requests on virtual ports, which are not associated with physical ports. A service application first creates a binding to a virtual port, then accepts incoming requests as computing resources become available.
To create a virtual port binding, ServiceEndpoint has three tcpListen method overloads that differ in the way they define access rules. For simplicity, we use one that does not impose any access restrictions:
public void tcpListen(int virtualPort, TCPOptions tcpOptions, int backlog)
The first parameter is a virtual port. We set it to 5. As for TCP options, we let them default and provide null. The last parameter, backlog, plays the same role as in classical sockets (see for details here). We set its value to 2.
serviceEndpoint.tcpListen(5, null, 2);
To accept established connections, ServiceEndpoint has a method tcpAccept:
public void tcpAccept(int virtualPort, TCPAcceptHandler acceptHandler)
The first parameter, virtualPort, must have the same value as you specified for the tcpListen. In our case, this is 5. The second parameter takes an implementation of the TCPAcceptHandler interface. The tcpAccept method is called whenever the application is ready to handle the next established TCP connection. If you want to have only one accepted connection at a time, you can implement the next call to tcpAccept just after closing the current connection. In our case, we do this before leaving the accept method of MyTCPAcceptHandler:
class MyTCPAcceptHandler implements TCPAcceptHandler
{
public void accept(RequestContext context, SocketChannel socketChannel, ConnectionMode mode) {
System.out.println(String.format("The TCP connection is established in '%s' mode", mode));
try {
int buffer_size = 100;
ByteBuffer buffer = ByteBuffer.allocate(buffer_size);
while(socketChannel.read(buffer) != -1) {
if(buffer.position() == buffer_size)
break;
}
SequenceDecoder asnInput = ASNDecoder.Sequence(buffer.array());
System.out.println(String.format("The client message: '%s'", asnInput.UTF8String()));
ASNEncoder asnEncoder = new ASNEncoder();
SequenceEncoder asnOutput = asnEncoder.Sequence();
asnOutput.UTF8String("Hello! I'm a service.");
buffer.clear();
buffer.put(asnEncoder.getEncoding());
buffer.flip();
socketChannel.write(buffer);
socketChannel.shutdownOutput();
}
catch(IOException e) {
System.out.println(String.format("TCP socket error: %s", e.getMessage()));
}
catch(AsnException e) {
System.out.println(String.format("Input data format error: %s", e.getMessage()));
}
finally {
try {
socketChannel.close();
} catch (IOException e) {}
context.serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler()); /* The tcpAccept method is called again right after closing the current connection */
}
}
}
accept is the only method of TCPAcceptHandler to implement. Its first parameter of type RequestContext has a serviceEndpoint field, which is the service endpoint we created earlier. After processing the message exchange, we use this field to call tcpAccept again. This is not a recursive call because tcpAccept is a method called asynchronously. The second parameter, socketChannel, of the accept method is a TCP socket channel for an established connection. And the third parameter indicates the connection mode: peer-to-peer or proxy. We use the ASN.1 codec to encode/decode messages for transmission over a network.
Now in the main method, we add a call to tcpListen and the first call to tcpAccept:
serviceEndpoint.tcpListen(5, null, 2);
serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler());
Subsequent calls to tcpAccept occur in the accept method of MyTCPAcceptHandler. Finally, our service application looks like this:
package serviceTestApp;
import softnet.*;
import softnet.exceptions.*;
import softnet.service.*;
import softnet.asn.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
class MyTCPAcceptHandler implements TCPAcceptHandler
{
public void accept(RequestContext context, SocketChannel socketChannel, ConnectionMode mode) {
System.out.println(String.format("The TCP connection is established in '%s' mode", mode));
try {
int buffer_size = 100;
ByteBuffer buffer = ByteBuffer.allocate(buffer_size);
while(socketChannel.read(buffer) != -1) {
if(buffer.position() == buffer_size)
break;
}
SequenceDecoder asnInput = ASNDecoder.Sequence(buffer.array());
System.out.println(String.format("The client message: '%s'", asnInput.UTF8String()));
ASNEncoder asnEncoder = new ASNEncoder();
SequenceEncoder asnOutput = asnEncoder.Sequence();
asnOutput.UTF8String("Hello! I'm a service.");
buffer.clear();
buffer.put(asnEncoder.getEncoding());
buffer.flip();
socketChannel.write(buffer);
socketChannel.shutdownOutput();
}
catch(IOException e) {
System.out.println(String.format("TCP socket error: %s", e.getMessage()));
}
catch(AsnException e) {
System.out.println(String.format("Input data format error: %s", e.getMessage()));
}
finally {
try {
socketChannel.close();
} catch (IOException e) {}
context.serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler()); /* The tcpAccept method is called again right after closing the current connection */
}
}
}
public class ServiceApp {
static final String apiVersion = "1.0";
private static ServiceEndpoint serviceEndpoint = null;
public static void main(String[] args) {
String service_uri = "softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org";
String password = "5Ur3pLBXSw";
try {
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
serviceEndpoint.addEventListener(new ServiceEventAdapter()
{
@Override
public void onConnectivityChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
EndpointConnectivity connectivity = serviceEndpoint.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
System.out.println(String.format("Service status: %s", serviceEndpoint.getStatus()));
}
});
serviceEndpoint.tcpListen(5, null, 2);
serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler());
serviceEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(serviceEndpoint != null)
serviceEndpoint.close();
System.out.println("The service app is closed!");
}
}
}
If you launch the application and everything is fine, it could print something like this:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
The application is now listening for TCP connection requests.
4.2. The client codebase
Let's write the code for establishing a TCP connection to the virtual port that the service app is listening on. ClientSEndpoint has a method tcpConnect for this:
public void tcpConnect(
int virtualPort,
TCPResponseHandler responseHandler,
TCPOptions tcpOptions)
There are also overloads of this method, discussed in the "Advanced features" section. For the first parameter of tcpConnect, we specify 5. And we leave the TCP options by default, specifying null for the last parameter. Finally, the second parameter, responseHandler, takes an implementation of TCPResponseHandler. This interface looks like this:
public interface TCPResponseHandler {
void onSuccess(
ResponseContext context,
SocketChannel socketChannel,
ConnectionMode mode);
void onError(ResponseContext context, SoftnetException exception);
}
The interface has two methods to implement. Each request ends with a callback to one of these methods. In case of success, the onSuccess method is called back. Its first parameter, context, is provided by the platform. Any response handler has this type of parameter. The second parameter, socketChannel, is a TCP socket channel for an established connection. And the third parameter indicates the connection mode: peer-to-peer or proxy.
The onError method is called back when an error is detected by the platform. It provides an error in the second parameter, exception, of type SoftnetException.
So, in the client application, we create a method executeTcpDemo to put the code for TCP-related operations in there. For simplicity, we implement the TCPResponseHandler by anonymous class and place the message exchange code in the onSuccess method. Now the question is when to call the tcpConnect method. It makes sense to send a connection request only if the remote service is online. Earlier, in section 3, it was discussed that the platform raises a ServiceOnline event for each client when the remote service comes online. To intercept this event, we implemented the onServiceOnline handler. So, If we place calling executeTcpDemo in that handler, it will be executed each time both the client and the service come online.
Finally, our client application looks like this:
package clientTestApp;
import softnet.*;
import softnet.asn.*;
import softnet.exceptions.*;
import softnet.client.*;
public class ClientApp {
static ClientSEndpoint clientSEndpoint = null;
public static void executeTcpDemo()
{
clientSEndpoint.tcpConnect(5, new TCPResponseHandler()
{
public void onSuccess(ResponseContext context, SocketChannel socketChannel, ConnectionMode mode) {
System.out.println(String.format("The TCP connection is established in '%s' mode", mode));
try {
ASNEncoder asnEncoder = new ASNEncoder();
SequenceEncoder asnOutput = asnEncoder.Sequence();
asnOutput.UTF8String("Hello! I'm a client.");
int buffer_size = 100;
ByteBuffer buffer = ByteBuffer.allocate(buffer_size);
buffer.put(asnEncoder.getEncoding());
buffer.flip();
socketChannel.write(buffer);
socketChannel.shutdownOutput();
System.out.println("The hello message is sent to the service!");
buffer.clear();
while(socketChannel.read(buffer) != -1) {
if(buffer.position() == buffer_size)
break;
}
buffer.flip();
SequenceDecoder asnInput = ASNDecoder.Sequence(buffer.array());
System.out.println(String.format("The service's response: '%s'", asnInput.UTF8String()));
socketChannel.close();
}
catch(IOException e) {
System.out.println(String.format("TCP socket error: %s", e.getMessage()));
}
catch(AsnException e) {
System.out.println(String.format("Input data format error: %s", e.getMessage()));
}
finally {
try {
socketChannel.close();
} catch (IOException e) {}
}
}
public void onError(ResponseContext context, SoftnetException exception) {
System.out.println(String.format("The TCP connection request failed with an error: %s", exception.getMessage()));
}
}, null);
}
public static void main(String[] args)
{
String client_uri = "softnet-s://hj1syian@ts.softnet-iot.org";
String password = "ZW81jj8NQd";
try {
ClientURI clientURI = new ClientURI(client_uri);
clientSEndpoint = ClientSEndpoint.create(
"Test Service",
"John Doe",
clientURI,
password,
"Test Client");
clientSEndpoint.addEventListener(new ClientEventAdapter()
{
@Override
public void onConnectivityChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
EndpointConnectivity connectivity = client.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
System.out.println(String.format("Client status: %s", client.getStatus()));
}
@Override
public void onServiceOnline(RemoteServiceEvent e) {
System.out.println(String.format("Remote service online! Hostname: %s, API version: %s", e.service.getHostname(), e.service.getVersion()));
executeTcpDemo();
}
});
clientSEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(clientSEndpoint != null)
clientSEndpoint.close();
System.out.println("The client app is closed!");
}
}
}
If you run this application while the service is online and everything is fine, it could print the following:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Remote service online! Hostname: Service1, API version: 1.0
Client status: Online
The TCP connection is established in 'P2P' mode
The hello message is sent to the service!
Then, the service application prints the following and sends the response back:
The TCP connection is established in 'P2P' mode
The client message: 'Hello! I'm a client.'
Right after that, the client application prints the final message:
The service's response: 'Hello! I'm a service.'
5. UDP in dynamic IP environmentsUDP is widely used for real-time data transmission. This kind of data can include live audio/video or, for example, real-time digital samples of analog signals. In this section, the client application establishes a UDP connection to the service and sends 100 UDP packets at 200 milliseconds interval, with each packet containing a sequence number. The service application, in turn, prints a sequence number of each received packet. This scenario simulates the transmission of analog signal samples in real time.
A detailed guide to working with UDP is presented in Handling UDP connection requests and Making UDP connection requests. This technique is very similar to the one used with TCP, so much of this section is similar to what was in the previous section.
5.1. The service codebase
As with TCP, service endpoints accept UDP requests on virtual ports, which are not associated with physical ports. A service application first creates a binding to a virtual port, then accepts incoming requests as computing resources become available.
To create a virtual port binding, ServiceEndpoint has three udpListen method overloads that differ in the way they define access rules. For simplicity, we use one that does not impose any access restrictions:
public void udpListen(int virtualPort, int backlog)
We set the first parameter to 10. The second parameter, backlog, plays the same role as in classical sockets. We set its value to 2.
serviceEndpoint.udpListen(10, 2);
To accept established connections, ServiceEndpoint has a method udpAccept:
public void udpAccept(int virtualPort, UDPAcceptHandler acceptHandler)
The first parameter, virtualPort, must have the same value as specified for the udpListen. In our case, this is 10. The second parameter takes an implementation of the UDPAcceptHandler interface. The udpAccept method is called whenever the application is ready to handle the next established UDP connection. If you want to have only one accepted connection at a time, you can implement the next call to udpAccept just after closing the socket currently receiving data. In our case, we do this before leaving the accept method of MyUDPAcceptHandler:
class MyUDPAcceptHandler implements UDPAcceptHandler
{
public void accept(RequestContext context, DatagramSocket datagramSocket, InetSocketAddress remoteSocketAddress, ConnectionMode mode) {
System.out.println(String.format("The UDP connection is established in '%s' mode", mode));
try {
byte[] buffer = new byte[15];
DatagramPacket packet = new DatagramPacket(buffer, 15);
datagramSocket.setSoTimeout(5000);
int rawNumber = 0;
System.out.println("Sequence numbers of received packets:");
for(int i=0; i<100; i++) {
packet.setLength(15);
datagramSocket.receive(packet);
SequenceDecoder asnInput = ASNDecoder.Sequence(packet.getData());
int number = asnInput.Int32();
if(number / 10 != rawNumber) {
rawNumber = number / 10;
System.out.println();
}
System.out.print(String.format("%d,\t", number));
}
System.out.println();
}
catch(SocketTimeoutException e) {
System.out.println();
}
catch(IOException e) {
System.out.println(String.format("UDP socket error: %s", e.getMessage()));
}
catch(AsnException e) {
System.out.println(String.format("Input data format error: %s", e.getMessage()));
}
finally {
datagramSocket.close();
/* The udpAccept method is called again right after closing the current connection */
context.serviceEndpoint.udpAccept(10, new MyUDPAcceptHandler());
}
}
}
accept is the only method of UDPAcceptHandler to implement. Its first parameter of type RequestContext has a serviceEndpoint field, which is the service endpoint we created earlier. After receiving the set of UDP packets and closing the socket, we use this field to call udpAccept again. The second parameter, datagramSocket, of the accept method is a UDP socket for an established connection. The third parameter, remoteSocketAddress, is an IP address and port of the remote endpoint with which the connection is established. And the fourth parameter indicates the connection mode: peer-to-peer or proxy. We use the ASN.1 DER codec to encode/decode the UDP packet sequence numbers.
Now in the main method, we add a call to udpListen and the first call to udpAccept:
serviceEndpoint.udpListen(10, 2);
serviceEndpoint.udpAccept(10, new MyUDPAcceptHandler());
Subsequent calls to udpAccept occur in the accept method of MyUDPAcceptHandler. Let's put everything together, and now the client application looks like this:
package serviceTestApp;
import softnet.*;
import softnet.exceptions.*;
import softnet.service.*;
import softnet.asn.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.SocketTimeoutException;
/* MyTCPAcceptHandler is not shown to avoid cluttering the code */
class MyUDPAcceptHandler implements UDPAcceptHandler
{
public void accept(RequestContext context, DatagramSocket datagramSocket, InetSocketAddress remoteSocketAddress, ConnectionMode mode) {
System.out.println(String.format("The UDP connection is established in '%s' mode", mode));
try {
byte[] buffer = new byte[15];
DatagramPacket packet = new DatagramPacket(buffer, 15);
datagramSocket.setSoTimeout(5000);
int rawNumber = 0;
System.out.println("Sequence numbers of received packets:");
for(int i=0; i<100; i++) {
packet.setLength(15);
datagramSocket.receive(packet);
SequenceDecoder asnInput = ASNDecoder.Sequence(packet.getData());
int number = asnInput.Int32();
if(number / 10 != rawNumber) {
rawNumber = number / 10;
System.out.println();
}
System.out.print(String.format("%d,\t", number));
}
System.out.println();
}
catch(SocketTimeoutException e) {
System.out.println();
}
catch(IOException e) {
System.out.println(String.format("UDP socket error: %s", e.getMessage()));
}
catch(AsnException e) {
System.out.println(String.format("Input data format error: %s", e.getMessage()));
}
finally {
datagramSocket.close();
/* The udpAccept method is called again right after closing the current connection */
context.serviceEndpoint.udpAccept(10, new MyUDPAcceptHandler());
}
}
}
public class ServiceApp {
static final String apiVersion = "1.0";
private static ServiceEndpoint serviceEndpoint = null;
public static void main(String[] args) {
String service_uri = "softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org";
String password = "5Ur3pLBXSw";
try {
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
serviceEndpoint.addEventListener(new ServiceEventAdapter()
{
@Override
public void onConnectivityChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
EndpointConnectivity connectivity = serviceEndpoint.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
System.out.println(String.format("Service status: %s", serviceEndpoint.getStatus()));
}
});
/* this code is commented out as MyTCPAcceptHandler is not shown
serviceEndpoint.tcpListen(5, null, 2);
serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler());
*/
serviceEndpoint.udpListen(10, 2);
serviceEndpoint.udpAccept(10, new MyUDPAcceptHandler());
serviceEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(serviceEndpoint != null)
serviceEndpoint.close();
System.out.println("The service app is closed!");
}
}
}
If you launch the application and everything is fine, it could print something like this:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
The application is now listening for UDP connection requests.
5.2. The client codebase
ClientSEndpoint has a method udpConnect for establishing a UDP connection:
public void udpConnect(int virtualPort, UDPResponseHandler responseHandler)
There are also overloads of this method, discussed in the "Advanced features" section. for the first parameter of udpConnect we specify 10. The second parameter takes an implementation of UDPResponseHandler, an interface that looks like this:
public interface UDPResponseHandler {
void onSuccess(
ResponseContext context,
java.net.DatagramSocket datagramSocket,
java.net.InetSocketAddress remoteSocketAddress,
ConnectionMode mode);
void onError(ResponseContext context, SoftnetException exception);
}
The interface has two methods to implement. Each request ends with a callback to one of these methods. In case of success, the onSuccess method is called back. Its first parameter, context, is provided by the platform. The second parameter, datagramSocket, is a UDP socket for an established connection. The third parameter, remoteSocketAddress, is an IP address and port of the remote endpoint with which the connection is established. And the fourth parameter indicates the connection mode: peer-to-peer or proxy.
The onError method is called back when an error is detected by the platform. It provides an error in the second parameter, exception, of type SoftnetException.
As with TCP, in the client application, we create a method executeUdpDemo to put the code for UDP-related operations in there. For simplicity, we implement the UDPResponseHandler by anonymous class and place the code for sending UDP packets in the onSuccess method. Finally, we place the call to executeUdpDemo in the onServiceOnline handler, right after the call to executeTcpDemo which we comment out.
Our client application now looks like this:
package clientTestApp;
import softnet.*;
import softnet.asn.*;
import softnet.exceptions.*;
import softnet.client.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
public class ClientApp {
static ClientSEndpoint clientSEndpoint = null;
/* The executeTcpDemo method is not shown to avoid cluttering the code */
public static void executeUdpDemo()
{
clientSEndpoint.udpConnect(10, new UDPResponseHandler()
{
public void onSuccess(
ResponseContext context,
DatagramSocket datagramSocket,
InetSocketAddress remoteSocketAddress,
ConnectionMode mode)
{
System.out.println(String.format("The UDP connection is established in '%s' mode", mode));
try {
int rawNumber = 0;
System.out.println("Sequence numbers of sent packets:");
for(int i=0; i<100; i++) {
ASNEncoder asnEncoder = new ASNEncoder();
asnEncoder.Sequence().Int32(i);
byte[] buffer = asnEncoder.getEncoding();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, remoteSocketAddress);
datagramSocket.send(packet);
if(i / 10 != rawNumber) {
rawNumber = i / 10;
System.out.println();
}
System.out.print(String.format("%d,\t", i));
Thread.sleep(200);
}
System.out.println();
}
catch(IOException e) {
System.out.println(String.format("UDP socket error: %s", e.getMessage()));
}
catch(InterruptedException e) { }
finally {
datagramSocket.close();
}
}
public void onError(ResponseContext context, SoftnetException exception) {
System.out.println(String.format("The UDP connection request failed with an error: %s", exception.getMessage()));
}
});
}
public static void main(String[] args)
{
String client_uri = "softnet-s://hj1syian@ts.softnet-iot.org";
String password = "ZW81jj8NQd";
try {
ClientURI clientURI = new ClientURI(client_uri);
clientSEndpoint = ClientSEndpoint.create(
"Test Service",
"John Doe",
clientURI,
password,
"Test Client");
clientSEndpoint.addEventListener(new ClientEventAdapter()
{
@Override
public void onConnectivityChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
EndpointConnectivity connectivity = client.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
System.out.println(String.format("Client status: %s", client.getStatus()));
}
@Override
public void onServiceOnline(RemoteServiceEvent e) {
System.out.println(String.format("Remote service online! Hostname: %s, API version: %s", e.service.getHostname(), e.service.getVersion()));
/* executeTcpDemo(); */
executeUdpDemo();
}
});
clientSEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(clientSEndpoint != null)
clientSEndpoint.close();
System.out.println("The client app is closed!");
}
}
}
If you run the application while the service is online, and if everything is fine, it will likely print the following:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Remote service online! Hostname: Service1, API version: 1.0
Client status: Online
The UDP connection is established in 'P2P' mode
Sequence numbers of sent packets:
At the same time, the service app prints messages like this:
The UDP connection established in 'P2P' mode
Sequence numbers of received packets:
The client then starts sending UDP packets at intervals of 200 milliseconds on average and printing their numbers, forming a 10x10 matrix:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24,
Because UDP is a non-guaranteed delivery protocol, depending on the quality of the network route, some of the sent packets may be lost.
At the same time, the service prints the numbers of received packets, also forming a matrix:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24,
6. Remote Procedure CallsSoftnet RPC is useful for communication tasks that can be solved simply using a request-response pattern or when the underlying platform is unable to handle resource-intensive application layer protocols. Softnet RPC transmits messages in ASN.1 DER format, though you can use any other format, for example, JSON. Using built-in ASN.1 codec, a client can pass data organized in a complex hierarchical structure up to 64 kilobytes in size to the remote procedure. The service, in turn, can return the result also in ASN.1 DER format and up to 64 kilobytes in size. A detailed guide to RPC is presented in Remote Procedure Calls.
To demonstrate some of these features, I'll create a simple book store in the service application where a book entry consists of several fields of different types. Then, I'll create an RPC procedure that returns a list of book entries by a specific author.
6.1. The service codebase
First, create a book entry class:
class Book {
public String title;
public String author;
public Date pubDate;
public int pages;
public float price;
public Book(String title, String author, Date pubDate, int pages, float price) {
this.title = title;
this.author = author;
this.pubDate = pubDate;
this.pages = pages;
this.price = price;
}
}
Add a static member bookList of type ArrayList<Book> to the ServiceApp class, and a static method loadBookList to load a short list of book entries:
public class ServiceApp {
static final String apiVersion = "1.0";
private static ServiceEndpoint serviceEndpoint = null;
private static ArrayList<Book> bookList = new ArrayList<Book>();
private static void loadBookList() throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
dateFormat.setTimeZone(utcTimeZone);
bookList.add(new Book("The Secrets of Time", "Jane Smith", dateFormat.parse("2018-05-25"), 320, 19.99f));
bookList.add(new Book("Beyond the Stars", "Jane Smith", dateFormat.parse("2019-02-23"), 288, 15.99f));
bookList.add(new Book("Echoes of the Past", "John Doe", dateFormat.parse("2021-03-10"), 246, 14.99f));
bookList.add(new Book("The Mystery of the Hidden Treasure", "Sarah Johnson", dateFormat.parse("2020-11-05"), 368, 17.99f));
bookList.add(new Book("Lost in the Wilderness", "David Wilson", dateFormat.parse("2019-06-20"), 412, 21.99f));
}
/* The rest of the ServiceApp code is not shown */
}
Now create an RPC procedure. ServiceEndpoint has a method registerProcedure for registering procedures that clients call remotely:
public void registerProcedure(String procedureName, RPCRequestHandler requestHandler, int concurrencyLimit)
The first parameter is the procedure’s name that clients use to call it. The second one is a request handler, an object that implements the RPCRequestHandler interface. And, the third parameter limits the number of concurrent calls to the procedure. If the execution of a procedure is resource intensive, this number is important.
Note that you provide an RPCRequestHandler instance only once when registering the procedure, and this instance handles all RCP requests to that procedure. This technique is different from handling TCP or UDP connection requests, where for example, for each incoming TCP connection request, we call the tcpAccept method to process it, specifying either the same TCPRequestHandler instance or a new one for each request.
Let's implement an RPC procedure. Create a new class called, for example, GetBookListByAuthor that implements RPCRequestHandler. It will be convenient to have a bookList member in this class, initialized when the class is instantiated using the bookList you created in the ServiceApp:
class GetBookListByAuthor implements RPCRequestHandler
{
private ArrayList<Book> bookList;
public GetBookListByAuthor(ArrayList<Book> bookList) {
this.bookList = bookList;
}
public int execute(RequestContext context, SequenceDecoder parameters, SequenceEncoder result, SequenceEncoder error) {
try {
String author = parameters.UTF8String();
for (Book book : bookList) {
if(book.author.equalsIgnoreCase(author)) {
SequenceEncoder asnBook = result.Sequence();
asnBook.UTF8String(book.title);
asnBook.UTF8String(book.author);
asnBook.GndTime(book.pubDate);
asnBook.Int32(book.pages);
asnBook.Real32(book.price);
}
}
return 0;
}
catch(AsnException e) {
return -1;
}
}
}
RPCRequestHandler has the only method execute to implement. Let’s consider it in more detail. The first parameter, context, of type RequestContext contains info about the client, a user, etc. The second parameter, parameters, is an ASN.1 sequence decoder that contains structured data from the client. In our case, we expect it to contain an author name. The third parameter, result, is an ASN.1 sequence encoder that you use to return the result back. In our case, the procedure returns a list of book entries by a given author. Finally, the last parameter, error, allows you to send back structured information about an error that might occur while executing the procedure. In case of an error, don’t forget to return a non-zero value. If the procedure completes without an error, it must return 0.
From the code snippet above, you can see that using the built-in ASN.1 codec, it is easy to work with application data. The developer guide is published here. ASN.1 is a highly efficient format used in various fields, including mobile broadband networks and cryptography. For example, ASN.1 DER is used for representing complex PKCS structures, such as X.509 digital certificates, '.pfx' and '.p12' files, etc. The efficiency and compactness of ASN.1 encoding make it suitable for resource-constrained IoT devices, where memory, processing power, and network bandwidth may be limited. Its ability to represent complex data structures in a compact form can help reduce the overhead associated with data serialization and transmission.
Now in the main method, add a call to loadBookList and an RPC procedure registration under the name "getBookListByAuthor". Clients will specify this name to call the procedure:
loadBookList();
serviceEndpoint.registerProcedure(
"getBookListByAuthor",
new GetBookListByAuthor(bookList),
2);
Finally, your service application may look like this:
package serviceTestApp;
import softnet.*;
import softnet.exceptions.*;
import softnet.service.*;
import softnet.asn.*;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.TimeZone;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/* MyTCPAcceptHandler and MyUDPAcceptHandler are not shown
to avoid cluttering the code */
class GetBookListByAuthor implements RPCRequestHandler
{
private ArrayList<Book> bookList;
public GetBookListByAuthor(ArrayList<Book> bookList) {
this.bookList = bookList;
}
public int execute(RequestContext context, SequenceDecoder parameters, SequenceEncoder result, SequenceEncoder error) {
try {
String author = parameters.UTF8String();
for (Book book : bookList) {
if(book.author.equalsIgnoreCase(author)) {
SequenceEncoder asnBook = result.Sequence();
asnBook.UTF8String(book.title);
asnBook.UTF8String(book.author);
asnBook.GndTime(book.pubDate);
asnBook.Int32(book.pages);
asnBook.Real32(book.price);
}
}
return 0;
}
catch(AsnException e) {
return -1;
}
}
}
class Book {
public String title;
public String author;
public Date pubDate;
public int pages;
public float price;
public Book(String title, String author, Date pubDate, int pages, float price) {
this.title = title;
this.author = author;
this.pubDate = pubDate;
this.pages = pages;
this.price = price;
}
}
public class ServiceApp {
static final String apiVersion = "1.0";
private static ServiceEndpoint serviceEndpoint = null;
private static ArrayList<Book> bookList = new ArrayList<Book>();
private static void loadBookList() throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
dateFormat.setTimeZone(utcTimeZone);
bookList.add(new Book("The Secrets of Time", "Jane Smith", dateFormat.parse("2018-05-25"), 320, 19.99f));
bookList.add(new Book("Beyond the Stars", "Jane Smith", dateFormat.parse("2019-02-23"), 288, 15.99f));
bookList.add(new Book("Echoes of the Past", "John Doe", dateFormat.parse("2021-03-10"), 246, 14.99f));
bookList.add(new Book("The Mystery of the Hidden Treasure", "Sarah Johnson", dateFormat.parse("2020-11-05"), 368, 17.99f));
bookList.add(new Book("Lost in the Wilderness", "David Wilson", dateFormat.parse("2019-06-20"), 412, 21.99f));
}
public static void main(String[] args) {
String service_uri = "softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org";
String password = "5Ur3pLBXSw";
try {
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
serviceEndpoint.addEventListener(new ServiceEventAdapter()
{
@Override
public void onConnectivityChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
EndpointConnectivity connectivity = serviceEndpoint.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
System.out.println(String.format("Service status: %s", serviceEndpoint.getStatus()));
}
});
/* this code is commented out as MyTCPAcceptHandler is not shown
serviceEndpoint.tcpListen(5, null, 2);
serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler());
*/
/* this code is commented out as MyUDPAcceptHandler is not shown
serviceEndpoint.udpListen(10, 2);
serviceEndpoint.udpAccept(10, new MyUDPAcceptHandler());
*/
loadBookList();
serviceEndpoint.registerProcedure("getBookListByAuthor", new GetBookListByAuthor(bookList), 2);
serviceEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(serviceEndpoint != null)
serviceEndpoint.close();
System.out.println("The service app is closed!");
}
}
}
Run the application and it will go into the state of listening for RPC requests:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
6.2. The client codebase
This section shows how to request a list of book entries by a specific author. ClientSEndpoint has a methods call to make an RPC call:
public void call(RemoteProcedure remoteProcedure, RPCResponseHandler responseHandler)
Here're the parameter descriptions:
- remoteProcedure contains the name and arguments of the procedure;
- responseHandler is an implementation of the RPCResponseHandler interface that the client app provides to the method.
Let's consider the RemoteProcedure class:
public class RemoteProcedure {
public final String name;
public final SequenceEncoder arguments;
public RemoteProcedure(String name)
}
When instantiating this class, your application provides the remote procedure’s name to the constructor. In our case, it is "getBookListByAuthor". RemoteProcedure has an arguments field in which the client provides arguments to the method. It is of type softnet.asn.SequenceEncoderand its maximum data size is limited by the platform to 64 kilobytes.
Let's consider the second parameter of the ClientSEndpoint's method call. This is an implementation of the RPCResponseHandler interface:
public interface RPCResponseHandler {
void onSuccess(ResponseContext context, SequenceDecoder result);
void onError(ResponseContext context, int errorCode, SequenceDecoder error);
void onError(ResponseContext context, SoftnetException exception);
}
The interface has three methods to implement. Each request ends with a callback to one of these methods. In case of success, the onSuccess method is called back. Its first parameter, context, is provided by the platform. The second parameter, result, is an object of type softnet.asn.SequenceDecoder that contains the ASN.1 DER encoded data returned as a result by the remote procedure.
The interface has two overloaded onError methods to handle an error response. The first overload is called back if the error code returned by the remote procedure is not 0. The first parameter, as usual, is the context. The second parameter, errorCode, is the error code itself. And the third parameter, error, of type softnet.asn.SequenceDecoder is a structured description of an error provided by the remote procedure.
The second overload of onError is called back when an error is detected by the platform. It provides an error in the second parameter of type SoftnetException. The possible exceptions are described here.
To implement the client logic, an executeRpcDemo method is created. The list of books in the service app contains two books by the author "Jane Smith". So, her name is provided to the procedure call. For simplicity, RPCResponseHandler is implemented by anonymous class and the code printing the list of returned entries is placed in the onSuccess method:
public static void executeRpcDemo()
{
final String author = "Jane Smith";
RemoteProcedure remoteProcedure = new RemoteProcedure("getBookListByAuthor");
remoteProcedure.arguments.UTF8String(author);
clientSEndpoint.call(remoteProcedure, new RPCResponseHandler()
{
@Override
public void onSuccess(ResponseContext context, SequenceDecoder result)
{
System.out.println();
try {
if(result.count() > 0) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
dateFormat.setTimeZone(utcTimeZone);
System.out.println(String.format("Books found by %s:", author));
System.out.println();
while(result.hasNext()) {
SequenceDecoder asnBook = result.Sequence();
String title = asnBook.UTF8String();
asnBook.skip();
java.util.Date pubDate = asnBook.GndTimeToDate();
int pages = asnBook.Int32();
float price = asnBook.Real32();
System.out.println(String.format("Title: %s", title));
System.out.println(String.format("Pub.Date: %s", dateFormat.format(pubDate)));
System.out.println(String.format("Pages: %d", pages));
System.out.println(String.format("Price: %f", price));
System.out.println();
}
}
else {
System.out.println("No books found for your request");
}
}
catch(AsnException e) {
System.out.println("The respone from the service has an invalid format");
}
}
@Override
public void onError(ResponseContext context, int errorCode, SequenceDecoder error) {
System.out.println(String.format("The service returned an error code: %d", errorCode));
}
@Override
public void onError(ResponseContext context, SoftnetException exception) {
System.out.println(String.format("The RPC request failed with an error: %s", exception.getMessage()));
}
});
}
As in the previous sections, the method executeRpcDemo is called in the body of onServiceOnline. The following is the entire code of the application:
package clientTestApp;
import softnet.*;
import softnet.asn.*;
import softnet.exceptions.*;
import softnet.client.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
public class ClientApp {
static ClientSEndpoint clientSEndpoint = null;
/* The executeTcpDemo and executeUdpDemo methods are not shown
to avoid cluttering the code */
public static void executeRpcDemo()
{
final String author = "Jane Smith";
RemoteProcedure remoteProcedure = new RemoteProcedure("getBookListByAuthor");
remoteProcedure.arguments.UTF8String(author);
clientSEndpoint.call(remoteProcedure, new RPCResponseHandler()
{
@Override
public void onSuccess(ResponseContext context, SequenceDecoder result)
{
System.out.println();
try {
if(result.count() > 0) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
dateFormat.setTimeZone(utcTimeZone);
System.out.println(String.format("Books found by %s:", author));
System.out.println();
while(result.hasNext()) {
SequenceDecoder asnBook = result.Sequence();
String title = asnBook.UTF8String();
asnBook.skip();
java.util.Date pubDate = asnBook.GndTimeToDate();
int pages = asnBook.Int32();
float price = asnBook.Real32();
System.out.println(String.format("Title: %s", title));
System.out.println(String.format("Pub.Date: %s", dateFormat.format(pubDate)));
System.out.println(String.format("Pages: %d", pages));
System.out.println(String.format("Price: %f", price));
System.out.println();
}
}
else {
System.out.println("No books found for your request");
}
}
catch(AsnException e) {
System.out.println("The respone from the service has an invalid format");
}
}
@Override
public void onError(ResponseContext context, int errorCode, SequenceDecoder error) {
System.out.println(String.format("The service returned an error code: %d", errorCode));
}
@Override
public void onError(ResponseContext context, SoftnetException exception) {
System.out.println(String.format("The RPC request failed with an error: %s", exception.getMessage()));
}
});
}
public static void main(String[] args)
{
String client_uri = "softnet-s://hj1syian@ts.softnet-iot.org";
String password = "ZW81jj8NQd";
try {
ClientURI clientURI = new ClientURI(client_uri);
clientSEndpoint = ClientSEndpoint.create(
"Test Service",
"John Doe",
clientURI,
password,
"Test Client");
clientSEndpoint.addEventListener(new ClientEventAdapter()
{
@Override
public void onConnectivityChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
EndpointConnectivity connectivity = client.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
System.out.println(String.format("Client status: %s", client.getStatus()));
}
@Override
public void onServiceOnline(RemoteServiceEvent e) {
System.out.println(String.format("Remote service online! Hostname: %s, API version: %s", e.service.getHostname(), e.service.getVersion()));
/* executeTcpDemo(); */
/* executeUdpDemo(); */
executeRpcDemo();
}
});
clientSEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(clientSEndpoint != null)
clientSEndpoint.close();
System.out.println("The client app is closed!");
}
}
}
If you run the application while the service is online, and if everything is fine, it will likely print the following:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Remote service online! Hostname: Service1, API version: 1.0
Client status: Online
Books found by Jane Smith:
Title: The Secrets of Time
Pub.Date: 2018-05-25
Pages: 320
Price: 19.990000
Title: Beyond the Stars
Pub.Date: 2019-02-23
Pages: 288
Price: 15.990000
7. Pub/Sub communication patternInteraction with a remote device is often triggered by an event on that device. This is called event-driven interaction. Softnet implements a publish/subscribe event model. Services publish events to the site, while subscribed clients can receive these events if they have sufficient permissions. Softnet supports three categories of events: Replacing, Queueing, and Private. Events must be defined in the Site Structure, where, along with the category, you specify parameters such as name, lifetime, max queue size and access rule. It is important to note that the set of events that a given service can raise is part of its interface contract. Private events significantly differ from the first two categories. They play a key role in implementing the asynchronous communication pattern and are discussed in the next section. So this section covers Replacing and Queueing events.
Replacing events are convenient for representing changes in state parameters of a device over time. For example, such parameters can be the readings of temperature, humidity, pressure, etc. The device can raise a replacing event whenever the corresponding parameter reaches a certain value or changes by a certain delta. Each time an event is raised, the previous instance is replaced with a new one. Those clients that haven’t yet received the previous instance of the event will no longer be able to receive it, but only the last one.
Queuing events are used when clients need all event instances and in chronological order. Unlike Replacing events, an event of this category does not replace the previous instance, but joins a queue of existing instances.
To demonstrate the technique of raising and receiving events, I created an example in which the service app raises three instances of a Replacing event with some intervals between them and three instances of a Queuing event. The client app, in turn, subscribes to these events. Depending on when the client connects to the site, before or after events are raised, there may be different scenarios for receiving Replacing events. If the client comes online before the events are raised, it may receive all three of them, though not necessarily. If it connects to the site after the events are raised, it is expected to receive only the latest one. As for Queueing events, they all are expected to be received anyway.
7.1. The service codebase
In the main method of the service app, we have code that defines the site structure and then provides it to the create method of ServiceEndpoint:
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
The event definitions must be added to the site structure before calling the create method. And in order to raise the events, the persistence level must be set for the events after the service endpoint is created. In terms of Softnet, it is also called Service Persistence. Level 1 of the persistence allows to persist events only between reconnections to the site until the service endpoint is closed. The raised events remain cashed in memory. Level 2 of the persistence allows to persist events between application restarts or system reboots. But this requires that the underlying platform supports the file system.
The definition of a Replacing event requires at least the event name. Optionally, you can specify an access rule. But in this example we're not considering access control. The SiteStructure method for defining a Replacing event without access restrictions looks like this:
void addReplacingEvent(String eventName)
So, let's define a Replacing event called "CurrentTemperature". The code snippet might look like this:
siteStructure.addReplacingEvent("CurrentTemperature");
And now we have left to define a Queuing event. The SiteStructure method for defining a Queuing event without access restrictions looks like this:
void addQueueingEvent(String eventName, int lifetime, int maxQueueSize)
The first parameter takes the event name. The second parameter is the event lifetime in seconds. The third parameter is the maximum queue size. See details here. Let's define an event called "CriticalPressure" with a lifetime of 10 minutes and a maximum queue size of 3:
siteStructure.addQueueingEvent("CriticalPressure", 600, 3);
After adding the event definitions and setting up the service persistence to the level 1, the initial code looks like this:
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
siteStructure.addReplacingEvent("CurrentTemperature");
siteStructure.addQueueingEvent("CriticalPressure", 600, 3);
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
serviceEndpoint.setPersistenceL1();
To create a Replacing event, your app instantiates the ReplacingEventclass, passing the event name to the constructor. In our example, the name is "CurrentTemperature". If you want to attach data to an event, it has an arguments field of type SequenceEncoder where you can load up to 2 kilobytes of data. In our example, each event will contain a sequential event number. Similarly, for Queueing events, the platform provides the QueueingEvent class.
For clarity, we place the code for raising events into a separate executeEventRaisingDemomethod:
static void executeEventRaisingDemo() {
try {
ReplacingEvent replacingEvent = new ReplacingEvent("CurrentTemperature");
replacingEvent.arguments.Int32(1);
serviceEndpoint.raiseEvent(replacingEvent);
QueueingEvent queueingEvent = new QueueingEvent("CriticalPressure");
queueingEvent.arguments.Int32(1);
serviceEndpoint.raiseEvent(queueingEvent);
Thread.sleep(1000);
replacingEvent = new ReplacingEvent("CurrentTemperature");
replacingEvent.arguments.Int32(2);
serviceEndpoint.raiseEvent(replacingEvent);
queueingEvent = new QueueingEvent("CriticalPressure");
queueingEvent.arguments.Int32(2);
serviceEndpoint.raiseEvent(queueingEvent);
Thread.sleep(1000);
replacingEvent = new ReplacingEvent("CurrentTemperature");
replacingEvent.arguments.Int32(3);
serviceEndpoint.raiseEvent(replacingEvent);
queueingEvent = new QueueingEvent("CriticalPressure");
queueingEvent.arguments.Int32(3);
serviceEndpoint.raiseEvent(queueingEvent);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
A service application can raise events regardless of whether the service is online or offline. However, we want to see how the order in which the service and the client comes online affects the client receiving events. Therefore, the application will make a call to executeEventRaisingDemoin the onStatusChanged handler if it is assigned the Online status:
public void onStatusChanged(ServiceEndpointEvent e)
{
ServiceEndpoint serviceEndpoint = e.getEndpoint();
ServiceStatus serviceStatus = serviceEndpoint.getStatus();
System.out.println(String.format("Service status: %s", serviceStatus));
if(serviceStatus == ServiceStatus.Online) {
executeEventRaisingDemo();
}
}
Now the service application looks like this:
package serviceTestApp;
import softnet.*;
import softnet.exceptions.*;
import softnet.service.*;
import softnet.asn.*;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.TimeZone;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/* MyTCPAcceptHandler, MyUDPAcceptHandler and GetBookListByAuthor are not shown */
public class ServiceApp {
static final String apiVersion = "1.0";
private static ServiceEndpoint serviceEndpoint = null;
private static ArrayList<Book> bookList = new ArrayList<Book>();
/* loadBookList is not shown */
static void executeEventRaisingDemo() {
try {
ReplacingEvent replacingEvent = new ReplacingEvent("CurrentTemperature");
replacingEvent.arguments.Int32(1);
serviceEndpoint.raiseEvent(replacingEvent);
QueueingEvent queueingEvent = new QueueingEvent("CriticalPressure");
queueingEvent.arguments.Int32(1);
serviceEndpoint.raiseEvent(queueingEvent);
Thread.sleep(1000);
replacingEvent = new ReplacingEvent("CurrentTemperature");
replacingEvent.arguments.Int32(2);
serviceEndpoint.raiseEvent(replacingEvent);
queueingEvent = new QueueingEvent("CriticalPressure");
queueingEvent.arguments.Int32(2);
serviceEndpoint.raiseEvent(queueingEvent);
Thread.sleep(1000);
replacingEvent = new ReplacingEvent("CurrentTemperature");
replacingEvent.arguments.Int32(3);
serviceEndpoint.raiseEvent(replacingEvent);
queueingEvent = new QueueingEvent("CriticalPressure");
queueingEvent.arguments.Int32(3);
serviceEndpoint.raiseEvent(queueingEvent);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
String service_uri = "softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org";
String password = "5Ur3pLBXSw";
try {
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
siteStructure.addReplacingEvent("CurrentTemperature");
siteStructure.addQueueingEvent("CriticalPressure", 600, 3);
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
serviceEndpoint.setPersistenceL1();
serviceEndpoint.addEventListener(new ServiceEventAdapter()
{
@Override
public void onConnectivityChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
EndpointConnectivity connectivity = serviceEndpoint.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
ServiceStatus serviceStatus = serviceEndpoint.getStatus();
System.out.println(String.format("Service status: %s", serviceStatus));
if(serviceStatus == ServiceStatus.Online) {
executeEventRaisingDemo();
}
}
});
/* this code is commented out as MyTCPAcceptHandler is not shown
serviceEndpoint.tcpListen(5, null, 2);
serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler());
*/
/* this code is commented out as MyUDPAcceptHandler is not shown
serviceEndpoint.udpListen(10, 2);
serviceEndpoint.udpAccept(10, new MyUDPAcceptHandler());
*/
/* this code is commented out as GetBookListByAuthor is not shown
loadBookList();
serviceEndpoint.registerProcedure("getBookListByAuthor", new GetBookListByAuthor(bookList), 2);
*/
serviceEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(serviceEndpoint != null)
serviceEndpoint.close();
System.out.println("The service app is closed!");
}
}
}
If you launch the application and it connects to the site, you'll probably see the following messages:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: SiteStructureMismatch
The last message means that now the site structure attached to the endpoint differs from which the site has been constructed originally because we have added two event definitions. If you open the site management panel, you can see that the service status has changed there too:
This is a typical situation when you are developing a Softnet application, which is described here. To solve the problem, click the structure
button. This displays the action button apply structure to the site
:
Clicking this button rebuilds the structure of the site:
At first glance, nothing has changed compared to the original view of the site. However, behind the scene, an event broker has been launched on the site. At the same time, the application probably went online and printed the following additional messages:
Service status: Offline
Endpoint connectivity status: Disconnected, Error: RestartDemanded, Message: The softnet server demanded to restart the endpoint.
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
We still have to discuss a client codebase, so close the application and move on to the next section.
7.2. The client codebase
Similar to ServiceEndpoint, the client endpoint class also implements two levels of event persistence. But, in the case of a client endpoint, the persistence plays a different role — it protects clients from receiving the same events multiple times. Level 1 guarantees this protection only between reconnections to the site until the client endpoint is closed. While Level 2 provides this protection between application restarts or system reboots. But this requires that the underlying platform supports the file system. In our example, we'll use Level 2 for the client endpoint as the application will be restarted multiple times during the test.
To receive event instances of a specific event, defined by its category and name, a client must first subscribe to that event. For the Replacing event category, ClientEndpoint has the following method to create subscription:
public void subscribeToREvent(String eventName, RemoteEventListener listener)
The method has two parameters: event name and event listener of type RemoteEventListener:
public interface RemoteEventListener {
void accept(ClientEndpoint clientEndpoint, RemoteEvent remoteEvent);
void acceptError(ClientEndpoint clientEndpoint, SoftnetException exception);
}
The first method, accept, is an event handler. It is called by the endpoint when it receives an event. Note that the endpoint receives the next event only after the current call to the handler has completed. The method has two parameters:
- clientEndpoint is an endpoint that called the handler;
- remoteEvent of type RemoteEvent is the event itself.
The second method, acceptError,is called in case of an error. Currently, the only possible error can be caused by subscribing to a non-existent event.
So, the remoteEvent represents the received event. It has the following fields that can be useful in this example:
- name is the event name specified in the event subscription;
- category is of type EventCategory enumeration. Depending of the event category, Its value can be set to one of the following three enum constants: Replacing, Queueing, or Private;
- arguments is of type SequenceDecoder provided by Softnet ASN.1 Codec. It contains data attached to the event by the service app;
- age specifies the time in seconds elapsed since the event has been received by the broker. This value is zero if the event is sent to the client without delay as soon as it is received by the broker.
For the Queueing event category, ClientEndpoint has the following method to create subscription:
public void subscribeToQEvent(String eventName, RemoteEventListener listener)
As you can see, its parameters are similar to those of subscribeToREvent. Everything said about creating a subscription to a Replacing event is also true for this type.
Now, let's implement the client codebase. For clarity, we place the code for receiving events into a separate executeEventReceivingDemomethod:
public static void executeEventReceivingDemo()
{
clientSEndpoint.subscribeToREvent("CurrentTemperature", new RemoteEventListener()
{
public void accept(ClientEndpoint clientEndpoint, RemoteEvent remoteEvent) {
try {
System.out.println(String.format("Received event: '%s'. Event number: %d. Age: %d seconds", remoteEvent.name, remoteEvent.arguments.Int32(), remoteEvent.age));
}
catch(AsnException e) {
System.out.println("Data format error");
}
}
public void acceptError(ClientEndpoint clientEndpoint, SoftnetException exception) {
System.out.println(String.format("Event subscription error: %s", exception.getMessage()));
}
});
clientSEndpoint.subscribeToQEvent("CriticalPressure", new RemoteEventListener() {
public void accept(ClientEndpoint clientEndpoint, RemoteEvent remoteEvent) {
try {
System.out.println(String.format("Received event: '%s'. Event number: %d. Age: %d seconds.", remoteEvent.name, remoteEvent.arguments.Int32(), remoteEvent.age));
}
catch(AsnException e) {
System.out.println("Data format error");
}
}
public void acceptError(ClientEndpoint clientEndpoint, SoftnetException exception) {
System.out.println(String.format("Event subscription error: %s", exception.getMessage()));
}
});
}
A client can call the subscription creation methods at any time, even before calling the connect method. However, the persistence level must be set before the first call to the connect. If the application does not explicitly set the persistence level, it is considered to be set to level 1.
In our example, we set the event persistence and call the executeEventReceivingDemo method right before calling the connect method:
clientSEndpoint.setPersistenceL2();
executeEventReceivingDemo();
clientSEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
The following is the entire code of the client application:
package clientTestApp;
import softnet.*;
import softnet.asn.*;
import softnet.exceptions.*;
import softnet.client.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
public class ClientApp {
static ClientSEndpoint clientSEndpoint = null;
/* The executeTcpDemo, executeUdpDemo and executeRpcDemo
methods are not shown */
public static void executeEventReceivingDemo()
{
clientSEndpoint.subscribeToREvent("CurrentTemperature", new RemoteEventListener()
{
public void accept(ClientEndpoint clientEndpoint, RemoteEvent remoteEvent) {
try {
System.out.println(String.format("Received event: '%s'. Event number: %d. Age: %d seconds", remoteEvent.name, remoteEvent.arguments.Int32(), remoteEvent.age));
}
catch(AsnException e) {
System.out.println("Data format error");
}
}
public void acceptError(ClientEndpoint clientEndpoint, SoftnetException exception) {
System.out.println(String.format("Event subscription error: %s", exception.getMessage()));
}
});
clientSEndpoint.subscribeToQEvent("CriticalPressure", new RemoteEventListener() {
public void accept(ClientEndpoint clientEndpoint, RemoteEvent remoteEvent) {
try {
System.out.println(String.format("Received event: '%s'. Event number: %d. Age: %d seconds.", remoteEvent.name, remoteEvent.arguments.Int32(), remoteEvent.age));
}
catch(AsnException e) {
System.out.println("Data format error");
}
}
public void acceptError(ClientEndpoint clientEndpoint, SoftnetException exception) {
System.out.println(String.format("Event subscription error: %s", exception.getMessage()));
}
});
}
public static void main(String[] args)
{
String client_uri = "softnet-s://hj1syian@ts.softnet-iot.org";
String password = "ZW81jj8NQd";
try {
ClientURI clientURI = new ClientURI(client_uri);
clientSEndpoint = ClientSEndpoint.create(
"Test Service",
"John Doe",
clientURI,
password,
"Test Client");
clientSEndpoint.addEventListener(new ClientEventAdapter()
{
@Override
public void onConnectivityChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
EndpointConnectivity connectivity = client.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ClientEndpointEvent e) {
ClientEndpoint client = e.getEndpoint();
System.out.println(String.format("Client status: %s", client.getStatus()));
}
@Override
public void onServiceOnline(RemoteServiceEvent e) {
System.out.println(String.format("Remote service online! Hostname: %s, API version: %s", e.service.getHostname(), e.service.getVersion()));
/* executeTcpDemo(); */
/* executeUdpDemo(); */
/* executeRpcDemo(); */
}
});
clientSEndpoint.setPersistenceL2();
executeEventReceivingDemo();
clientSEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(clientSEndpoint != null)
clientSEndpoint.close();
System.out.println("The client app is closed!");
}
}
}
7.3. The first test scenario
Launch first the client application. If everything is fine, it prints the following:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Client status: Online
The client created subscriptions to the events and set up event handlers. Now launch the service application. If everything is fine, it prints the following:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
At the same time, the client app prints additional messages:
Remote service online! Hostname: Service1, API version: 1.0
Received event: 'CurrentTemperature'. Event number: 1. Age: 0 seconds
Received event: 'CriticalPressure'. Event number: 1. Age: 0 seconds.
Received event: 'CurrentTemperature'. Event number: 2. Age: 0 seconds
Received event: 'CriticalPressure'. Event number: 2. Age: 0 seconds.
Received event: 'CurrentTemperature'. Event number: 3. Age: 0 seconds
Received event: 'CriticalPressure'. Event number: 3. Age: 0 seconds.
The client received all three Replacing events and all three Queueing events. All events have an age of 0, meaning they were delivered to the client without delay as soon as they were received by the event broker.
Close both applications.
7.4. The second test scenario
Now launch first the service application. If everything is fine, it prints the following:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
The service app raised all events. We expect that the event broker has retained all three Queueing events and only the last Replacing event. Well let's check. Launch the client app. If everything is fine, it might print something like this:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Remote service online! Hostname: Service1, API version: 1.0
Client status: Online
Received event: 'CriticalPressure'. Event number: 1. Age: 479 seconds.
Received event: 'CurrentTemperature'. Event number: 3. Age: 478 seconds
Received event: 'CriticalPressure'. Event number: 2. Age: 479 seconds.
Received event: 'CriticalPressure'. Event number: 3. Age: 478 seconds.
All right! Note that this time the age is not 0. The received events have been stored in the broker for some time.
8. Asynchronous communication patternSome requests to devices may be of a nature that they cannot be immediately responded but instead require some indefinite period of time to prepare the response. If the communication platform supports notifications, the device can complete the request-response session by informing the client that it will be notified when the result is ready. And when the result is prepared, the device can notify the client about it or send the result itself back. Softnet Free implements this communication pattern through Private events.
In this section, I demonstrate a scenario in which a client makes an RPC call to a remote service to set the temperature of some object to a specific value. Changing the temperature of a physical object takes some time. And when this is done, the remote service sends a Private event back to the client notifying it.
8.1. The service codebase
Before using, a Private event must be defined in the Site Structure, where you specify the event's name and lifetime. The queue size is fixed at 1000. As for access rules, they are not used for Private events.
The SiteStructure method for defining a Private event looks like this:
void addPrivateEvent(String eventName, int lifeTime)
So, let's define a Private event called "TemperatureSet" with a lifetime of 30 minutes. It looks like this:
siteStructure.addPrivateEvent("TemperatureSetUp", TimeSpan.fromMinutes(30));
I add this code right after the previous two event definitions. Next, I create an RPC procedure called "setTemperature". In its body, I run a task in a different thread that simulates setting up the temperature. The procedure ends with sending back the ID of this task. After 3 seconds, the task sends back a Private event with the task ID included, confirming that the temperature has been changed:
class SetTemperature implements RPCRequestHandler
{
public int execute(final RequestContext context, SequenceDecoder parameters, SequenceEncoder result, SequenceEncoder error) {
final UUID taskID = UUID.randomUUID();
try {
System.out.println(String.format("The 'Set Temperature' task is created. Target temp: %s", parameters.Int32()));
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
PrivateEvent notification = new PrivateEvent("TemperatureSet", context.clientId);
notification.arguments.OctetString(taskID);
context.serviceEndpoint.raiseEvent(notification);
}
catch (InterruptedException e) {}
}});
thread.start();
result.OctetString(taskID);
return 0;
}
catch (AsnException e) {
return -1;
}
}
}
Look how the Private event is created:
PrivateEvent notification = new PrivateEvent("TemperatureSet", context.clientId);
Along with the event name, its constructor takes the client ID. Here're the PrivateEvent class members:
public class PrivateEvent {
public final String name;
public final long clientId;
public final SequenceEncoder arguments;
public PrivateEvent(String name, long clientId)
}
An application takes the client ID from the request context which the platform provides to the request handler when it passes a request to it. A request context is an instance of RequestContext which has the following members:
public class RequestContext {
public final MembershipUser user;
public final long clientId;
public final ServiceEndpoint serviceEndpoint;
public final SequenceDecoder sessionTag;
}
This is the first parameter of all built-in request handlers, i.e., TCP, UDP and RPC request handlers. Its other members are discussed in the next section.
The last thing we need to do is register the procedure:
serviceEndpoint.registerProcedure("setTemperature", new setTemperature(), 1);
The service application now looks like this:
package serviceTestApp;
import softnet.*;
import softnet.exceptions.*;
import softnet.service.*;
import softnet.asn.*;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.TimeZone;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/* MyTCPAcceptHandler, MyUDPAcceptHandler and GetBookListByAuthor are not shown */
class SetTemperature implements RPCRequestHandler
{
public int execute(final RequestContext context, SequenceDecoder parameters, SequenceEncoder result, SequenceEncoder error) {
final UUID taskID = UUID.randomUUID();
try {
System.out.println(String.format("The 'Set Temperature' task is created. Target temp: %s", parameters.Int32()));
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
PrivateEvent notification = new PrivateEvent("TemperatureSet", context.clientId);
notification.arguments.OctetString(taskID);
context.serviceEndpoint.raiseEvent(notification);
}
catch (InterruptedException e) {}
}});
thread.start();
result.OctetString(taskID);
return 0;
}
catch (AsnException e) {
return -1;
}
}
}
public class ServiceApp {
static final String apiVersion = "1.0";
private static ServiceEndpoint serviceEndpoint = null;
private static ArrayList<Book> bookList = new ArrayList<Book>();
/* loadBookList and executeEventRaisingDemo are not shown */
public static void main(String[] args) {
String service_uri = "softnet-srv://3cd69efa-70a5-4c4f-9dec-50c47afd5db0@ts.softnet-iot.org";
String password = "5Ur3pLBXSw";
try {
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Test Service",
"John Doe");
siteStructure.addReplacingEvent("CurrentTemperature");
siteStructure.addQueueingEvent("CriticalPressure", 600, 3);
siteStructure.addPrivateEvent("TemperatureSet", TimeSpan.fromMinutes(30));
ServiceURI serviceURI = new ServiceURI(service_uri);
serviceEndpoint = ServiceEndpoint.create(siteStructure, apiVersion, serviceURI, password);
serviceEndpoint.setPersistenceL1();
serviceEndpoint.addEventListener(new ServiceEventAdapter()
{
@Override
public void onConnectivityChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
EndpointConnectivity connectivity = serviceEndpoint.getConnectivity();
System.out.println(String.format("Endpoint connectivity status: %s, Error: %s, Message: %s", connectivity.status, connectivity.error, connectivity.message));
}
@Override
public void onStatusChanged(ServiceEndpointEvent e) {
ServiceEndpoint serviceEndpoint = e.getEndpoint();
ServiceStatus serviceStatus = serviceEndpoint.getStatus();
System.out.println(String.format("Service status: %s", serviceStatus));
/* this code is commented out as executeEventRaisingDemo
is not shown
if(serviceStatus == ServiceStatus.Online) {
executeEventRaisingDemo();
}
*/
}
});
/* this code is commented out as MyTCPAcceptHandler is not shown
serviceEndpoint.tcpListen(5, null, 2);
serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler());
*/
/* this code is commented out as MyUDPAcceptHandler is not shown
serviceEndpoint.udpListen(10, 2);
serviceEndpoint.udpAccept(10, new MyUDPAcceptHandler());
*/
/* this code is commented out as GetBookListByAuthor is not shown
loadBookList();
serviceEndpoint.registerProcedure("getBookListByAuthor", new GetBookListByAuthor(bookList), 2);
*/
serviceEndpoint.registerProcedure("setTemperature", new SetTemperature(), 1);
serviceEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
}
catch(java.lang.Throwable e) {
e.printStackTrace();
}
finally {
if(serviceEndpoint != null)
serviceEndpoint.close();
System.out.println("The service app is closed!");
}
}
}
We run the application now and see the following messages:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: SiteStructureMismatch
This is the same situation we had in the previous section. We added a Private event definition to the Site Structure and the platform detected that now it is different from the site's original structure. As in the previous section, we open the site management panel and click the structure
button:
Then, clicking the apply structure to the site
button rebuilds the site, and the service goes online:
Service status: Offline
Endpoint connectivity status: Disconnected, Error: RestartDemanded, Message: The softnet server demanded to restart the endpoint.
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
8.2. The client codebase
I implemented the client codebase in two methods. One method creates a subscription to the "TemperatureSet" event and specifies the event handler. Another method makes an RPC call to "setTemperature" to set the target temperature. Each method prints the action name, task ID and action time when the handler is called.
To receive event instances of a specific Private event, a client must first subscribe to that event. ClientEndpoint has the following method for this:
public void subscribeToPEvent(String eventName, RemoteEventListener listener)
Its parameters and the listener implementation are similar to those described for Replacing and Queueing events in the previous section.
The client code looks like this:
static void subscribeToTemperatureSet() {
clientSEndpoint.subscribeToPEvent("TemperatureSet", new RemoteEventListener() {
public void accept(ClientEndpoint clientEndpoint, RemoteEvent remoteEvent) {
try {
/* Print the task ID and current time */
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
System.out.println(String.format("The 'Set Temperature' task completed. Task ID: %s. Time: %s",
remoteEvent.arguments.OctetStringToUUID().toString(), timeFormat.format(new Date())));
}
catch(AsnException e) {
System.out.println("Data format error");
}
}
public void acceptError(ClientEndpoint clientEndpoint, SoftnetException exception) {
System.out.println(String.format("Event subscription error: %s", exception.getMessage()));
}
});
}
public static void executeAsyncCommDemo() {
RemoteProcedure remoteProcedure = new RemoteProcedure("setTemperature");
/* set the temperature to 30 degrees Celsius */
remoteProcedure.arguments.Int32(30);
clientSEndpoint.call(remoteProcedure, new RPCResponseHandler() {
@Override
public void onSuccess(ResponseContext context, SequenceDecoder result) {
try {
/* Print the task ID and current time */
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
System.out.println(String.format("The 'Set Temperature' task created. Task ID: %s. Time: %s",
result.OctetStringToUUID().toString(), timeFormat.format(new Date())));
}
catch (AsnException e) {
e.printStackTrace();
}
}
@Override
public void onError(ResponseContext context, int errorCode, SequenceDecoder error) {
System.out.println(String.format("The service returned an error code: %d", errorCode));
}
@Override
public void onError(ResponseContext context, SoftnetException exception) {
System.out.println(String.format("The RPC request failed with an error: %s", exception.getMessage()));
}
});
}
I placed a call to subscribeToTemperatureSet in the main method before connecting the endpoint:
subscribeToTemperatureSet();
clientSEndpoint.connect();
System.in.read(); /* waits until pressing the Enter key */
The second method, executeAsyncCommDemo, I call in onServiceOnline handler:
@Override
public void onServiceOnline(RemoteServiceEvent e) {
System.out.println(String.format("Remote service online! Hostname: %s, API version: %s", e.service.getHostname(), e.service.getVersion()));
/* executeTcpDemo(); */
/* executeUdpDemo(); */
/* executeRpcDemo(); */
executeAsyncCommDemo();
}
Let's launch the service app first:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Service status: Online
Then launch the client app:
Endpoint connectivity status: AttemptToConnect, Error: NoError, Message: null
Endpoint connectivity status: Connected, Error: NoError, Message: null
Remote service online! Hostname: Service1, API version: 1.0
Client status: Online
After that, the service app prints this message:
The 'Set Temperature' task is created. Target temp: 30
Then, the client prints the following two messages at 3 second intervals:
The 'Set Temperature' task created. Task ID: f82776bb-56f9-4fe8-ac2a-e0d99d95859e. Time: 21:13:50
The 'Set Temperature' task completed. Task ID: f82776bb-56f9-4fe8-ac2a-e0d99d95859e. Time: 21:13:53
9. Advanced featuresThis publication primarily focuses on communication models of the platform. However, many other features and capabilities remained unrevealed. In this section, I provide a brief overview of the most interesting of them.
9.1. Access Control
This is perhaps the most important feature of this platform and the most required in the IoT realm. Softnet offers developers two types of access control – Plain Access Control (PAC) and Role-Based Access Control (RBAC). The use of PAC does not imply any additions to the service’s source code. Any domain user added to the site, and therefore any client associated with it, will have a full access to the device. PAC can also be used if you're implementing your own mechanism of access control. At the same time, RBAC allows you to implement different levels of access to device resources. You simply add a list of roles to the Site Structure and then use them to declaratively create access rules for RPC procedures, events in the Site Structure, and TCP and UDP virtual port listeners. It is also possible to implement a granular access control programmatically. The list of roles you added to the Site Structure will then appear in the site management panel when registering the device on the site. The device owner can then assign them to domain users. Users, in turn, can be associated with different persons and organizations, which allows owners to share access to devices with them.
Let's consider a simple example. Assume our service application employs 3 user roles: Administrator, Operator, User. First, we need to add them to the Site Structure:
SiteStructure siteStructure = ServiceEndpoint.createStructure(
"Smart Home",
"John Doe");
siteStructure.setRoles("Administrator; Operator; User");
Then, we can use them in the event definitions:
siteStructure.addQueueingEvent("MovementDetected", TimeSpan.fromHours(2), 20,
"Administrator; Operator");
siteStructure.addReplacingEvent("ModeChanged", "Administrator");
The 'MovementDetected' event is only accessible to clients with at least one of two roles: Administrator or Operator. While the 'ModeChanged' event is only accessible to clients with the Administrator role.
Access rules based on user roles can also be applied to request handlers. The TCP virtual port listener might look like this:
serviceEndpoint.tcpListen(5, null, 2, "Administrator; Operator");
Then you can implement the request handling:
serviceEndpoint.tcpAccept(5, new MyTCPAcceptHandler());
What if I require more granular access control over device resources? Well, I can achieve this programmatically. When the platform passes a request to the handler, it also provides a context of type RequestContext as the first parameter. Here are the RequestContext members:
public class RequestContext {
public final ServiceEndpoint serviceEndpoint;
public final MembershipUser user;
public final long clientId;
public final SequenceDecoder sessionTag;
}
This class contains a field of type MembershipUser which represents the domain user associated with a given client. The MembershipUser object provides the user name, ID, category, roles in case of RBAC, as well as some methods. Let's see how it might look like with a TCP connection request handler:
class MyTCPAcceptHandler implements TCPAcceptHandler {
@Override
public void accept(RequestContext context, SocketChannel socketChannel, ConnectionMode mode) {
if(context.user.isInRole("Administrator")) {
// some code utilizing a TCP connection for administrators
}
else if (context.user.isInRole("Operator")) {
// some code utilizing a TCP connection for operators
}
else {
// Access Denied!
// We should not get here if a declarative
// access rule is "Administrator; Operator"
}
}
}
ServiceEndpoint provides a set of API methods for interacting with the User Membership. One of them, getUsers, returns all users with access to the service:
public MembershipUser[] getUsers()
9.2. User authority management
As it was mentioned, if a service application employs RBAC, the roles supported by the service are presented on the site management panel and the device owner can assign them to domain users. This is described here. Without going into details, let's see how this might look in the context of our example. The following is a view of the site where I registered the service from the previous section:
As you can see, three roles supported by the service are displayed in the <supported roles> block. The domain contains two users — Owner and Guest. Next, I add a new user Alex and assign him the role "Operator", and Owner the role "Administrator". It looks like this:
Now device users can create client registrations from these domain users and interact with the device. This is described here.
In Softnet MS, you can share devices with other individuals and organizations. This feature is implemented through the concept of contacts. The user guide on this is published here. In the following image, I created a contact Arthur Koifman, added a domain user Arthur for this contact and assigned him the role "User":
The next image is what Arthur Koifman sees when he opens the "Home Automation" domain shared by John Doe:
Here Arthur can create one or multiple client entities to register client applications.
9.3. Multi-Service sites
The client application we have used throughout this publication interacted with a single remote device. However, devices with identical interfaces, designated by the same Service Type and Contract Author, can be registered on the same site. In this case, a single client can interact with all of them. This type of client is called a multi-service client.
The following image is a site where 4 IP-cameras are registered:
An application designed to communicate with multiple services utilizes the endpoint instantiated from the ClientEndpoint class. This is the parent class for ClientSEndpoint, which is a single-service endpoint class that we used in all the examples. ClientEndpoint implements a structure called Service Group that represents all services registered on the site. For each service it has a RemoteService object that provides the service's hostname, ID, API version, and online/offline status. In case of ClientSEndpoint, the Service Group ever contains exactly one RemoteService object. RemoteService has the following members:
public interface RemoteService {
long getId();
String getHostname();
String getVersion();
boolean isOnline();
boolean isRemoved();
}
ClientEndpoint provides a set of API methods for interacting with the Service Group. One of them, getServices, returns all RemoteService objects corresponding to services hosted on the site:
public RemoteService[] getServices()
The Service Status Detection (SSD) mechanism provides clients with information about the status of remote services. Whenever a service goes online/offline the client is provided with the corresponding event that it can intercept with the onServiceOnline and onServiceOffline handlers. There is a set of additional events that SSD raises in response to changes to the services. See Client-specific platform events for details. The following is a peace of code implementing some of the handlers associated with the Service Group:
clientEndpoint.addEventListener(new ClientEventAdapter()
{
@Override
public void onServiceOnline(RemoteServiceEvent e) {
System.out.println("Remote service online.");
System.out.println("Hostname: " + e.service.getHostname());
System.out.println("API version: " + e.service.getVersion());
System.out.println();
}
@Override
public void onServiceOffline(RemoteServiceEvent e) {
System.out.println("Remote service offline.");
System.out.println("Hostname: " + e.service.getHostname());
System.out.println();
}
@Override
public void onServiceIncluded(RemoteServiceEvent e) {
System.out.println("Remote service Included.");
System.out.println("API version: " + e.service.getVersion());
System.out.println("Hostname: " + e.service.getHostname());
System.out.println();
}
@Override
public void onServiceRemoved(RemoteServiceEvent e) {
System.out.println("Remote service Removed.");
System.out.println("Hostname: " + e.service.getHostname());
System.out.println();
}
@Override
public void onServiceUpdated(RemoteServiceEvent e) {
System.out.println("Remote service updated.");
System.out.println("API version: " + e.service.getVersion());
System.out.println("Hostname: " + e.service.getHostname());
System.out.println();
}
});
9.4. Public services and guest access
Some service applications may support guest access. This is convenient for users as it does not require creating a guest client account. In Softnet, services with guest access are called public services. Softnet users can find public services they are interested in on the “Public Services” page. This subject is described here.
Traditionally, a guest client has no state on the server as it has no account. However, some client-server interaction scenarios require that clients preserve the state on the server between reconnections. This is necessary for asynchronous communication, which Softnet implements through Private events. However, Private events can only be sent to stateful (registered) clients. So that guest clients could also receive Private events, Softnet implements stateful guest clients along with conventional stateless clients. Each stateful guest client has an account on the server and unique URI. Any anonymous person can find any public service in a Softnet network and create a stateful guest client using their email. This doesn't require being logged into the Softnet MS.
Some service applications may not necessarily require for guest clients to be able to receive Private events. Such services can support stateless guest clients, i.e. conventional guest clients. These clients connect to the site using a guest shared URI. Users can find it on the site page.
If you want you service app to support guest access, simply specify this in the Site Structure:
siteStructure.setGuestSupport();
To indicate support for stateless guests, make the following method call:
siteStructure.setStatelessGuestSupport();
For such a service, you will see three additional blocks on the site panel:
The <guest status> block shows whether the guest allowed and a button to change this status. The second block, <guest page>, shows a URL of the page where users can view the service info and create stateful guest accounts. And the third block, <guest shared uri>, shows a URI that stateless clients use to connect to the site.
Anonymous users see a public service like this:
In the "Guest Clients by Email" section of the Softnet MS, a non-registered user can manage stateful guest clients that he/she created in a given Softnet network:
Comments
Please log in or sign up to comment.