Websocket Chat with Lambda (but all in Java)

 

I don't know if I am not able to perform search on Internet (I doubt it) but every examples of websocket chat created with Lambda was all created using Node.js. And I can cheat on Java, that's my love.

So I decided to transform the usual chat into a Java project, by of course performing some changes in order to be easy to check if it works. 


THE IDEA

The idea behind the project is simple: a simple WebSocket API exposed with api gateway with 4 different route and a unique Lambda who is in charge to serve each request.

The change I made is about the name registration: instead of just propagate the message to other connected users, I want to have the way to register a name for chat users so every time a message is published, the receiver can see who sent the message.

As I said in other project, this is to be intended for testing purpose, so there are really infinite ways to do have the same behavior maybe in a better way.


THE PROJECT

The entire project is based on cloud formation, where I have declared:

  • the gateway
  • the stage
  • and all the routes
The gateway

SimpleWebSocket:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: !Sub ${AWS::StackName}-SimpleWebSocket
ProtocolType: WEBSOCKET
RouteSelectionExpression: "$request.body.action"
ApiKeySelectionExpression: $request.header.x-api-key

The stage

SimpleWebSocketStage:
Type: AWS::ApiGatewayV2::Stage
Properties:
ApiId: !Ref SimpleWebSocket
AutoDeploy: true
StageName: production

As you see in the first piece, the Route selection is attempted using the field action in the json object of body.

There are 4 Route and four integration, I will show just the one related to one route action

ConnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref SimpleWebSocket
RouteKey: $connect
AuthorizationType: NONE
OperationName: ConnectRoute
Target: !Join
- '/'
- - 'integrations'
- !Ref ConnectInteg
ConnectInteg:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref SimpleWebSocket
Description: Connect Integration
IntegrationType: AWS_PROXY
IntegrationUri:
Fn::Sub:
arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SimpleWebSocketFunction.Arn}/invocations

The code is pretty simple as we are defining the route key and performing the integration to the function (which is unique and will be the same for each Integration)


It is important of course to give permission to apigateway to perform an invokefunction to our function

SimpleWebSocketFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt SimpleWebSocketFunction.Arn
Principal: apigateway.amazonaws.com


THE TABLE


The DynamoTable involved is really simple:
  • the key is the connectionId received during the connect routing
  • the second attribute is the name


THE JAVA CODE

Java code is of course simple but let me explain the algorithm first.
When a user send a message which action is "sendmessage" the application expects to receive a Json Object which is composed of two element:
  • the action, a string representation. In this case the value is of course sendmessage. This is the field that drives the routing
  • the detail, which is an element of type Action composed of
    • the type, which is an ActionType element, an enum that can have two value: ADD_MESSAGE and SET_NAME
    • the value which is a string

Based on that object, the application will add the message received in value or can save the name in table if set_name is received.

During connect operation, the application save the connectionId in table, during disconnect the application delete the value

During the SET_NAME phase, the application performs an update on table to save the value as name to related connectionID

During ADD_MESSAGE phase, the application perform a getItem to search the name from the connectionId, scans all the table to find the active connections and send a message to each user. The message has a structure with two element
  •  the content which is a string representing the message
  • the sender which is a SendInfo element made by
    • connectionId - the receiver
    • name - the name of the sender
I know that the variable could have better names, sorry for that


The db operations are made using DynamoDbClient, the send message is performed using ApiGatewayManagementApiClient and postToConnection

I show the code for SEND_MESSAGE route management, i tried to use as much as functional i can

else if (routeKey.equals(ROUTE_KEY_SEND_MESSAGE)){
try {
Action detail = new ObjectMapper().readValue(event.getBody(), MessageReceived.class).detail();
log.debug("Received send message with type {}", detail.type());
if (detail.type() == ActionType.SET_NAME){
log.debug("Updating {} with name {}", connectionId, detail.value());
dynamoDbClient.updateItem(new UpdateItem(connectionId, detail.value()).buildUpdateItem());
}
else if (detail.type() == ActionType.ADD_MESSAGE){
log.debug("Send message {} to others", detail.value());
SendInfo sender = this.fromMapAttributeValue(dynamoDbClient
.getItem(new ItemKeyBuilder(connectionId).buildGetItem()).item());
log.debug("Message is sent by {}", sender.name());
dynamoDbClient.scan(buildScanRequest()).items().stream()
.map(this::fromMapAttributeValue).filter(s -> !s.connectionId().equals(connectionId))
.map(senderFromDb -> new MessageToSend(detail.value(),
new SendInfo(senderFromDb.connectionId(), sender.name())))
.map(messageSent -> buildPostToConnectionRequest(messageSent))
.forEach(apiGatewayManagementApiClient::postToConnection);
}

} catch(Exception e){
log.error("Unable to send messages " + e.getMessage(), e);
sendMessageToMyself(connectionId, apiGatewayManagementApiClient, "Error sending message");
}


Every time an exception occurs, a message is sent back to the user who originates the request.

THE HTML CODE


The application is quite simple again: page has an input for name and a button to send, another input to write message and a related button to send

When page is open everything is disabled. It tries to connect to websocket: when connection is ok the name input and button are unlocked. Then the user can write name and press the button: the request is sent to api and then the application unlock the message input and button and disabled the name

When the user press the send button, the text is sent and it is shown to a div with a prefix "YOU"

When the user receive a message, the text is shown to a div with a prefix obtained from the name of the sender

The onmessage

webSocket.onmessage = (event) => {
var data = JSON.parse(event.data);
if (data.content && data.sender){
document.getElementById('receivedContent').innerHTML += "</br>" + data.sender.name + " --- " + data.content;
} else {
document.getElementById('receivedContent').innerHTML += "</br>" + event.data;
}
};


the name is extracted from sender.name

THE RESULT


Here an example of interaction, you can see it from that screen





CONCLUSION


There are really many ways to create application like that, they are easy to create and to maintain. Exchange data in this way is very powerful and can be used for many purpose
Right now of course there is no persistence and the interaction is among different users

But let's suppose in the future to have another application, which generate notification to be sent to connected user: we can reuse this infrastructure of course.

Let's study it




Commenti

Post popolari in questo blog

HTML and AI - A simple request, and a beautiful response for ElasticSearch without Kibana

A simple CD using AWS Pipeline