Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add go guide for real time chat and a guide to use terratest with the same project #639

Merged
merged 15 commits into from
Oct 16, 2024
309 changes: 309 additions & 0 deletions docs/guides/go/realtime-messaging.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
export const description =
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved
'Use the Nitric framework to easily build and deploy Go WebSocket applications for AWS, Azure or GCP'

export const title_meta =
'Building your first WebSocket Application with Go and Nitric'
HomelessDinosaur marked this conversation as resolved.
Show resolved Hide resolved

# Building your first WebSocket Application with Nitric
raksiv marked this conversation as resolved.
Show resolved Hide resolved

## What we'll be doing

1. Use Nitric to create a WebSocket endpoint
raksiv marked this conversation as resolved.
Show resolved Hide resolved
2. Manage WebSocket connections using a Key-Value store
3. Handle WebSocket events:
- Register connections on connect
- Remove connections on disconnect
- Broadcast messages to all connected clients
4. Run locally for testing
5. Deploy to a cloud of your choice
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

## Prerequisites

- [Go](https://go.dev/dl/)
- The [Nitric CLI](https://nitric.io/docs/installation)
- An [AWS](https://aws.amazon.com), [GCP](https://cloud.google.com), or [Azure](https://azure.microsoft.com) account (_your choice_)
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

## Getting started

We'll start by creating a new project for our WebSocket application.

```bash
nitric new my-websocket-app go-starter
```

Next, open the project in your editor of choice.

```bash
cd my-websocket-app
```

Make sure all dependencies are resolved:

```bash
go mod tidy
```

The scaffolded project should have the following structure:

```text
+--services/
| +-- hello/
| +-- main.go
| ...
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved
+--nitric.yaml
+--go.mod
+--go.sum
+--golang.dockerfile
+--.gitignore
+--README.md
```

You can test the project to verify everything is working as expected:

```bash
nitric start
```

If everything is working as expected, you can now delete all files/folders in the `services/` folder. We'll create new services in this guide.

## Building the WebSocket Application

Let's begin by setting up the WebSocket application. First, create a new folder called `websockets` within the services directory. Inside this folder, add a file named `main.go`, and include the following code:

```go
raksiv marked this conversation as resolved.
Show resolved Hide resolved
package main

import (
"context"
"fmt"

"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/keyvalue"
"github.com/nitrictech/go-sdk/nitric/websockets"
)

func main() {
// Create a WebSocket endpoint named "public".
raksiv marked this conversation as resolved.
Show resolved Hide resolved
ws := nitric.NewWebsocket("public")

// Initialize a KV store named "connections" with Get, Set, and Delete permissions.
connections := nitric.NewKv("connections").Allow(keyvalue.KvStoreGet, keyvalue.KvStoreSet, keyvalue.KvStoreDelete)

// Handle new WebSocket connections by storing the connection ID in the KV store.
ws.On(websockets.EventType_Connect, func(ctx *websockets.Ctx) {
err := connections.Set(context.TODO(), ctx.Request.ConnectionID(), map[string]interface{}{
"connectionId": ctx.Request.ConnectionID(),
})
if err != nil {
return
}
})
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

// Add event handlers here

nitric.Run()
}
```

Here we're creating:

- A WebSocket endpoint named `public`
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved
- A Key-Value store named `connections` to track WebSocket connections
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

From here, let's add some features to that function that allow us to manage connections and broadcast messages.

<Note>
You could separate some or all of these event handlers into their own services
if you prefer. For simplicity, we'll group them together in this guide.
</Note>

### Register connections on connect

```go
ws.On(websockets.EventType_Connect, func(ctx *websockets.Ctx) {
err := connections.Set(context.TODO(), ctx.Request.ConnectionID(), map[string]interface{}{
"connectionId": ctx.Request.ConnectionID(),
})
if err != nil {
return
jyecusch marked this conversation as resolved.
Show resolved Hide resolved
}
raksiv marked this conversation as resolved.
Show resolved Hide resolved
})
```

### Remove connections on disconnect

```go
ws.On(websockets.EventType_Disconnect, func(ctx *websockets.Ctx) {
err := connections.Delete(context.TODO(), ctx.Request.ConnectionID())
raksiv marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return
}
raksiv marked this conversation as resolved.
Show resolved Hide resolved
})
```

### Broadcast messages to all connected clients

```go
ws.On(websockets.EventType_Message, func(ctx *websockets.Ctx) {
connectionStream, err := connections.Keys(context.TODO())
if err != nil {
return
}
raksiv marked this conversation as resolved.
Show resolved Hide resolved
senderId := ctx.Request.ConnectionID()
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

for {
connectionId, err := connectionStream.Recv()
if err != nil {
break
}
raksiv marked this conversation as resolved.
Show resolved Hide resolved

if connectionId == senderId {
continue
}

message := fmt.Sprintf("%s: %s", senderId, ctx.Request.Message())
err = ws.Send(context.TODO(), connectionId, []byte(message))
raksiv marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return
}
raksiv marked this conversation as resolved.
Show resolved Hide resolved
}
})
```

### Bringing it all together

<details>
<summary>Your code should look like this:</summary>

```go
raksiv marked this conversation as resolved.
Show resolved Hide resolved
package main

import (
"context"
"fmt"

"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/keyvalue"
"github.com/nitrictech/go-sdk/nitric/websockets"
)

func main() {
// Create a WebSocket endpoint named "public".
ws := nitric.NewWebsocket("public")

// Initialize a KV store named "connections" with Get, Set, and Delete permissions.
connections := nitric.NewKv("connections").Allow(keyvalue.KvStoreGet, keyvalue.KvStoreSet, keyvalue.KvStoreDelete)

// Handle new WebSocket connections by storing the connection ID in the KV store.
ws.On(websockets.EventType_Connect, func(ctx *websockets.Ctx) {
err := connections.Set(context.TODO(), ctx.Request.ConnectionID(), map[string]interface{}{
"connectionId": ctx.Request.ConnectionID(),
})
if err != nil {
return
}
})

ws.On(websockets.EventType_Connect, func(ctx *websockets.Ctx) {
err := connections.Set(context.TODO(), ctx.Request.ConnectionID(), map[string]interface{}{
"connectionId": ctx.Request.ConnectionID(),
})
if err != nil {
return
jyecusch marked this conversation as resolved.
Show resolved Hide resolved
}
})
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

ws.On(websockets.EventType_Disconnect, func(ctx *websockets.Ctx) {
err := connections.Delete(context.TODO(), ctx.Request.ConnectionID())
if err != nil {
return
}
})

ws.On(websockets.EventType_Message, func(ctx *websockets.Ctx) {
connectionStream, err := connections.Keys(context.TODO())
if err != nil {
return
}

senderId := ctx.Request.ConnectionID()

for {
connectionId, err := connectionStream.Recv()
if err != nil {
break
}

if connectionId == senderId {
continue
}

message := fmt.Sprintf("%s: %s", senderId, ctx.Request.Message())
err = ws.Send(context.TODO(), connectionId, []byte(message))
if err != nil {
return
}
}
})

nitric.Run()
}
```

</details>

Do a quick `go mod tidy` to make sure all new dependencies are resolved.

## Ok, let's run this thing!

Now that you have your WebSocket application defined with handlers for each event, it's time to test it locally.
HomelessDinosaur marked this conversation as resolved.
Show resolved Hide resolved

```bash
nitric start
```

Once it starts, the application will be ready to accept WebSocket connections. You can use a WebSocket client like Postman or any other WebSocket tool to test the application.
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

We will keep it running for our tests. If you want to update your services, just save them, and they'll be reloaded automatically.
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

## Deploy to the cloud

At this point, you can deploy what you've built to any of the supported cloud providers. To do this, start by setting up your credentials and any configuration for the cloud you prefer:

- [AWS](/reference/providers/aws)
- [Azure](/reference/providers/azure)
- [GCP](/reference/providers/gcp)
davemooreuws marked this conversation as resolved.
Show resolved Hide resolved

Next, we'll need to create a `stack`. A stack represents a deployed instance of an application, which is a key value store of resources defined in your project. You might want separate stacks for each environment, such as stacks for `dev`, `test`, and `prod`. For now, let's start by creating a `dev` stack.
raksiv marked this conversation as resolved.
Show resolved Hide resolved

The `stack new` command below will create a stack named `dev` that uses the `aws` provider.

```bash
nitric stack new dev aws
```

Continue by checking your stack file `nitric.dev.yaml` and adding in your preferred region. Let's use `us-east-1`.
raksiv marked this conversation as resolved.
Show resolved Hide resolved

### AWS

Note: You are responsible for staying within the limits of the free tier or any costs associated with deployment.
HomelessDinosaur marked this conversation as resolved.
Show resolved Hide resolved

We called our stack `dev`. Let's try deploying it with the `up` command:
raksiv marked this conversation as resolved.
Show resolved Hide resolved

```bash
nitric up
```

When the deployment is complete, go to the relevant cloud console and you'll be able to see and interact with your WebSocket application.
raksiv marked this conversation as resolved.
Show resolved Hide resolved

To tear down your application from the cloud, use the `down` command:

```bash
nitric down
```

## Summary

In this guide, we've created a serverless WebSocket application using Go and Nitric. We've demonstrated how to set up WebSocket connections, track clients using a Key-Value store, and broadcast messages to all connected clients. This application can be easily deployed to the cloud, allowing you to build scalable, real-time communication systems.

For more information and advanced usage, refer to the [Nitric documentation](https://nitric.io/docs).
Loading
Loading