Having rejected RPC as a technology back in the 1980s, the whole request/response nature of HTTP and the web seemed a backward step to me when I first started doing Web Development in the 1990s. Consequently, I've been a keen enthusiast of WebSockets as they have slowly permeated the web.
One of my reasons for not investigating API Gateway before now was its lack of support for WebSockets. Part of my reason for pursuing it now is that the support is there (admittedly, it has been for a year, but I've been busy).
In this post, we are going to extend the work of the last post to support a WebSocket client. Like most such examples to be found on the web, it is going to be trivial; but for variety not a chat room. Instead, you can send lines of text (one at a time) to the server and it will send back a character count. Trivial logic, complex plumbing.
All of this code is in the repository, tagged API_GATEWAY_WEBSOCKET.
First, a Client
One of the things about using WebSockets is that fairly quickly curl stops working for almost anything you want to do. wscurl and wscat exist, but I've had a lot of issues getting them to do what I want to do.
On the other hand, writing websocket clients with grizzly is "fairly" trivial, particularly when so much of the code is boilerplate. Consequently, I've written a quick-and-dirty client that sends and receives messages as pure text without any processing and placed it in the blog.ignorance.wsclient directory.
Now AWS
There are a number of online resources that I've followed in understanding websockets on AWS.
- This blog post seems to be the top hit that I always come back to and gives a basic outline of the approach but requires manual setup.
- This announcement says that CloudFormation now supports configuration of API Gateway V2 (with websockets) but doesn't give any direct links.
- It would seem that to support this, CloudFormation added a completely new gateway resource type, helpfully called APIGatewayV2.
- There is a developer guide for Websocket APIs.
Once again, we are required to create a number of resources:
- A new (V2) gateway
- An integration between this and our existing lambda
- A "route" that says that we want to send all our messages to this lambda as they are
- A "deployment" and a "stage" to make it exist in the outside world
As an aside, for this operation, I created an updateGateway.sh script, which avoids tearing down and rebuilding all the components continually. You may use this if you wish, but from time to time there appear to be issues with "updating" things in CloudFormation, at which point you may have to go back and drop and recreate everything anyway.
If you have tried running the script, it will probably work ... but we have yet to implement the code in the lambda.
I'm not sure how much is V1 vs V2, but obviously what comes across in the request is significantly different for WebSockets than it is for REST API calls - for a start, there is no path parameter. Instead, we need to dig into the "request context".
This is a map which is passed across in the integration proxy JSON (for both V1 and V2), just like query parameters or headers. Unlike those, it seems the values can plausibly be non-String values, such as nested maps. In the V2 request context for websockets, there is a field called "eventType" which appears as "MESSAGE", a "routeKey" which contains the route key determined by the gateway and a "connectionId" which is an abstract string describing each connection uniquely.
To avoid the kind of duplication we dealt with in the API integration before, I've opted to configure the gateway to pass everything as "$default" - the default route. It would not be unreasonable to have a number of "large scale" lambdas, each dedicated to handling a significant part of the logic of the websocket flow. As always, what you decide on depends on the constraints of the system that you are building.
Either way, I claim that the knowledge of the route is not interesting: either all the routes are "$default" or else there is one route per lambda: whatever the value, we want to take the same action (I am deliberately ignoring $connect and $disconnect events here, along with the connectionId, but will return to them in the next post).
So we can add to our Initialization class a simple request that we want to have a handler which processes all the WebSocket messages, and have our "implementation detail" handler observe if the eventType is MESSAGE and if so, invoke this handler instead.
public void initialize(Central central) {
...
central.websocket(() -> new CounterSocket());
}
public void handleIt(TDACentralConfiguration central, ServerLogger logger,
Responder response, Context cx) throws Exception {
if (context != null && "MESSAGE".equals(context.get("eventType"))) {
WSProcessor wsproc = central.websocketHandler();
if (wsproc instanceof DesiresLogger) {
((DesiresLogger)wsproc).provideLogger(logger);
}
wsproc.onText(body);
return;
}
...
}
The CounterSocket class is then an implementation of WSProcessor which, for now, just simply logs out what it would like to do. It turns out that responding is trickier than I had imagined and involves tangling with that connectionId. So time to cut our losses and move on to the next post which deals with that.
Conclusion
In this post, we have managed to set up a second gateway, which is using the AWS V2 model, and which can handle websockets. We have successfully managed to configure our lambda to receive and log the incoming messages, but we can't respond yet because we need to first deal with connections.
Next: Responding to Websockets
Next: Responding to Websockets
No comments:
Post a Comment