Ioto provides a simple and flexible method for efficiently generating dynamic responses that contain live device data for client HTTP requests.
This post is the third of a series on the Ioto embedded web server component and it discusses generating dynamic responses for Ioto requests.
The posts in the series are:
Using frameworks such as VueJS or React to implement embedded management applications is the preferred design pattern when creating embedded device management applications. In this approach, the device management task is divided into two parts - a UI/UX portion that runs in the browser, and a device-resident portion that provides data to the browser or mobile UI. As a result, the device agent with the embedded web server does not generate HTML pages; instead, the pages are dynamically generated in the browser, freeing the device agent to focus on providing pure data as needed to the management app or requestor. This approach is called the Single Page Application (SPA) design pattern.
When using Single Page Application design, it is necessary for the web server to render live data in order to feed the UI with device information. Typically, this is accomplished by accessing device APIs and dynamically rendering the information in a JSON formatted response.
The Ioto web server can generate dynamic responses via a Action Handlers that bind specific URLs to corresponding C functions. The action handler pattern is ideal for situations when you want to generate a dynamic response based on current device data or state.
Historically, web servers have used the CGI protocol and web frameworks like PHP or ESP to generate dynamic content. But these solutions are a poor fit for modern management applications that use SPA techniques. These frameworks consume considerable CPU and memory resources and are not the best approach for embedded devices. Further, they expose larger attack surfaces due to excess features that are not essential for embedded devices.
Ioto employs Action Routines that implement a direct binding from URLs to C functions, along with a flexible, non-blocking API that supports streaming. By utilizing a JSON parser engine and Fiber Coroutines, a significantly more straightforward and efficient solution is achieved. Compared to other web servers for embedded tasks, Ioto is both faster and more compact.
Action routines are registered by calling webAddAction in your extension code. This API takes a function to run and a corresponding URI prefix to bind to.
static void hello(Web *web)
{
webWrite(web, "Hello World\n", -1);
webFinalize(web);
}
// Register the action
webAddAction(ioto->webHost, "/action/hello", hello);
The webAddAction call registers the hello C function to be invoked whenever the request URL matches the given prefix. In this case, if the URL begins with the string /action/hello, the request will invoke this action routine.
When invoked, request data is passed to the action routine and the action can generate or stream a response.
Read the previous post Extending Ioto with Custom Code to learn how to integrate your custom code and dynamic actions into Ioto.
Ioto provides a flexible, comprehensive API so you can fully tailor dynamic request responses.
For example:
To generate a simple one line response:
webResponse(web, 200, "Hello World\n");
To set an output HTTP header with the response:
webAddHeader(web, "X-Custom", "%d", 42);
To set the HTTP response status:
webSetStatus(web, 404);
To respond to a request with an error:
webError(web, 404, "Cannot serve request");
To write a database item (record) to the response:
DbItem *item = dbGet(db, "User", DB_PROPS("id", "u23877112"), 0);
webWriteItem(web, item);
To write an array (grid) of items to the response:
DbGrid *users = dbFind(db, "User", 0, 0);
webWriteItems(web, users);
To redirect to a new page:
webRedirect(web, 302, "/login");
To get the value of a request form field:
const char *user = webGetVar(web, "username", 0);
To set a variable in the authenticated session store:
webSetSessionVar(web, "username", username);
It is essential for a device agent to be able to generate dynamic responses without blocking, even if that response may cause the network to become saturated while the data drains. i.e. a device agent must be able to wait for such activities to complete without blocking the entire agent.
Similarly, you may have a need to access an external process or service to gather device data to use in a response, and that may entail waiting for the data to become available.
A device agent must be able to perform these long running tasks, without blocking the entire agent.
Ioto fully supports parallelism and overlapped I/O. But it is not multithreaded and so does not have the complexity of threads or event-based callbacks.
Programming with threads can be appealing at first, however a multithreaded design can be problematic. Subtle programming errors due to timing related issues, multithread lock deadlocks and race conditions can be extraordinarily difficult to detect and diagnose. All too often, they appear in production deployments.
The other design pattern used to achieve parallelism is event-based programming. Unfortunately, event based paradigms require callbacks that quickly become nested, complex and hard to visualize. They are thus sometimes termed callback-hell for good reason.
Ioto does not suffer from these issues. Rather, Ioto uses Fiber Coroutines so you can wait for I/O inside without blocking the entire agent and Ioto will transparently switch to another fiber coroutine when the current fiber is waiting for I/O. Importantly: only one fiber is ever running at a time. The result is simple, fast, straight-line procedural code that is easier to debug and maintain.
When writing a large response, the Ioto webWrite and webWriteSocket primitives will automatically yield the CPU if the underlying socket is full. Similarly webRead and webReadSocket will yield if not data is yet available. Provided you are using the fiber-safe blocking routines provided by Ioto, you can rely on the transparent yielding and do not need to take explicit steps to yield.
If you need to wait for data from an external API or service, you have two options:
Use the Ioto Portable Runtime non-blocking socket I/O routines such as rReadSocket and rWriteSocket, or
Create a thread to wait for the data and then call rYieldFiber in your action routine. When your thread has the required data, call rResumeFiber from the thread to resume the Ioto fiber.
The next post will cover Streaming Responses.
To learn more about EmbedThis Ioto, please read:
{{comment.name}} said ...
{{comment.message}}