From 848224d6d8d423514c76f67d52ec41decf4c301e Mon Sep 17 00:00:00 2001 From: Rak Siva Date: Mon, 19 Aug 2024 12:56:39 -0600 Subject: [PATCH 1/3] add guide --- src/nav.config.ts | 4 + src/pages/guides/go.mdx | 5 + src/pages/guides/go/realtime-messaging.mdx | 231 +++++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 src/pages/guides/go/realtime-messaging.mdx diff --git a/src/nav.config.ts b/src/nav.config.ts index cd8a72c0c..5fca3bed6 100644 --- a/src/nav.config.ts +++ b/src/nav.config.ts @@ -568,6 +568,10 @@ const fullNav: FullNav = { title: 'REST API', href: '/guides/go/serverless-rest-api-example', }, + { + title: 'Realtime Messaging', + href: '/guides/go/realtime-messaging', + }, ], }, ], diff --git a/src/pages/guides/go.mdx b/src/pages/guides/go.mdx index 24009f904..82934b7e4 100644 --- a/src/pages/guides/go.mdx +++ b/src/pages/guides/go.mdx @@ -8,4 +8,9 @@ export const description = 'How to work with existing databases' name="REST API" description="Use Nitric to build and deploy REST APIs for AWS, Azure or GCP" /> + diff --git a/src/pages/guides/go/realtime-messaging.mdx b/src/pages/guides/go/realtime-messaging.mdx new file mode 100644 index 000000000..3137ab44e --- /dev/null +++ b/src/pages/guides/go/realtime-messaging.mdx @@ -0,0 +1,231 @@ +export const description = + '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' + +# Building your first WebSocket Application with Nitric + +## What we'll be doing + +1. Use Nitric to create a WebSocket endpoint +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 + +## 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_) + +## 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 +| ... ++--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 +package main + +import ( + "context" + "fmt" + + "github.com/nitrictech/go-sdk/handler" + "github.com/nitrictech/go-sdk/nitric" +) + +func main() { + ws, err := nitric.NewWebsocket("public") + if err != nil { + fmt.Println("Error creating WebSocket:", err) + return + } + + connections, err := nitric.NewKv("connections").Allow(nitric.KvStoreGet, nitric.KvStoreSet, nitric.KvStoreDelete) + if err != nil { + fmt.Println("Error creating KV store:", err) + return + } + + // Add event handlers here + + if err := nitric.Run(); err != nil { + fmt.Println("Error running Nitric service:", err) + } +} +``` + +Here we're creating: + +- A WebSocket endpoint named `public` +- A Key-Value store named `connections` to track WebSocket connections + +From here, let's add some features to that function that allow us to manage connections and broadcast messages. + + + 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. + + +### Register connections on connect + +```go +ws.On(handler.WebsocketConnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { + err := connections.Set(context.TODO(), ctx.Request.ConnectionID(), map[string]interface{}{ + "connectionId": ctx.Request.ConnectionID(), + }) + if err != nil { + return ctx, err + } + + return next(ctx) +}) +``` + +### Remove connections on disconnect + +```go +ws.On(handler.WebsocketDisconnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { + err := connections.Delete(context.TODO(), ctx.Request.ConnectionID()) + if err != nil { + return ctx, err + } + + return next(ctx) +}) +``` + +### Broadcast messages to all connected clients + +```go +ws.On(handler.WebsocketMessage, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { + connectionStream, err := connections.Keys(context.TODO()) + if err != nil { + return ctx, err + } + + 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 ctx, err + } + } + + return next(ctx) +}) +``` + +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. + +```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. + +We will keep it running for our tests. If you want to update your services, just save them, and they'll be reloaded automatically. + +## 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) + +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. + +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`. + +### AWS + +Note: You are responsible for staying within the limits of the free tier or any costs associated with deployment. + +We called our stack `dev`. Let's try deploying it with the `up` command: + +```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. + +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). From 715e936dad319e5d5dd47c55c5f2687c1dae96da Mon Sep 17 00:00:00 2001 From: Rak Siva Date: Fri, 30 Aug 2024 07:43:19 -0600 Subject: [PATCH 2/3] Add full code snippet as suggested. --- src/pages/guides/go/realtime-messaging.mdx | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/pages/guides/go/realtime-messaging.mdx b/src/pages/guides/go/realtime-messaging.mdx index 3137ab44e..91dba3a88 100644 --- a/src/pages/guides/go/realtime-messaging.mdx +++ b/src/pages/guides/go/realtime-messaging.mdx @@ -174,6 +174,91 @@ ws.On(handler.WebsocketMessage, func(ctx *handler.WebsocketContext, next handler }) ``` +### Bringing it all together + +
+Your code should look like this: + +```go +package main + +import ( + "context" + "fmt" + + "github.com/nitrictech/go-sdk/handler" + "github.com/nitrictech/go-sdk/nitric" +) + +func main() { + ws, err := nitric.NewWebsocket("public") + if err != nil { + fmt.Println("Error creating WebSocket:", err) + return + } + + connections, err := nitric.NewKv("connections").Allow(nitric.KvStoreGet, nitric.KvStoreSet, nitric.KvStoreDelete) + if err != nil { + fmt.Println("Error creating KV store:", err) + return + } + + ws.On(handler.WebsocketConnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { + err := connections.Set(context.TODO(), ctx.Request.ConnectionID(), map[string]interface{}{ + "connectionId": ctx.Request.ConnectionID(), + }) + if err != nil { + return ctx, err + } + + return next(ctx) + }) + + ws.On(handler.WebsocketDisconnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { + err := connections.Delete(context.TODO(), ctx.Request.ConnectionID()) + if err != nil { + return ctx, err + } + + return next(ctx) + }) + + ws.On(handler.WebsocketMessage, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { + connectionStream, err := connections.Keys(context.TODO()) + if err != nil { + return ctx, err + } + + 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 ctx, err + } + } + + return next(ctx) + }) + + if err := nitric.Run(); err != nil { + fmt.Println("Error running Nitric service:", err) + } +} +``` + +
+ Do a quick `go mod tidy` to make sure all new dependencies are resolved. ## Ok, let's run this thing! From f59c5ed11af4748d62fee629eb3238dd9aafde85 Mon Sep 17 00:00:00 2001 From: Rak Siva Date: Thu, 26 Sep 2024 13:04:02 -0600 Subject: [PATCH 3/3] fix: update code for new go SDK --- src/pages/guides/go/realtime-messaging.mdx | 183 ++++++++++----------- 1 file changed, 88 insertions(+), 95 deletions(-) diff --git a/src/pages/guides/go/realtime-messaging.mdx b/src/pages/guides/go/realtime-messaging.mdx index 91dba3a88..7e31eb25b 100644 --- a/src/pages/guides/go/realtime-messaging.mdx +++ b/src/pages/guides/go/realtime-messaging.mdx @@ -77,28 +77,31 @@ import ( "context" "fmt" - "github.com/nitrictech/go-sdk/handler" "github.com/nitrictech/go-sdk/nitric" + "github.com/nitrictech/go-sdk/nitric/keyvalue" + "github.com/nitrictech/go-sdk/nitric/websockets" ) func main() { - ws, err := nitric.NewWebsocket("public") - if err != nil { - fmt.Println("Error creating WebSocket:", err) - return - } + // Create a WebSocket endpoint named "public". + ws := nitric.NewWebsocket("public") - connections, err := nitric.NewKv("connections").Allow(nitric.KvStoreGet, nitric.KvStoreSet, nitric.KvStoreDelete) - if err != nil { - fmt.Println("Error creating KV store:", err) - return - } + // 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 + } + }) // Add event handlers here - if err := nitric.Run(); err != nil { - fmt.Println("Error running Nitric service:", err) - } + nitric.Run() } ``` @@ -117,40 +120,35 @@ From here, let's add some features to that function that allow us to manage conn ### Register connections on connect ```go -ws.On(handler.WebsocketConnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { +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 ctx, err + return } - - return next(ctx) }) ``` ### Remove connections on disconnect ```go -ws.On(handler.WebsocketDisconnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { +ws.On(websockets.EventType_Disconnect, func(ctx *websockets.Ctx) { err := connections.Delete(context.TODO(), ctx.Request.ConnectionID()) if err != nil { - return ctx, err + return } - - return next(ctx) }) ``` ### Broadcast messages to all connected clients ```go -ws.On(handler.WebsocketMessage, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { +ws.On(websockets.EventType_Message, func(ctx *websockets.Ctx) { connectionStream, err := connections.Keys(context.TODO()) if err != nil { - return ctx, err + return } - senderId := ctx.Request.ConnectionID() for { @@ -166,11 +164,9 @@ ws.On(handler.WebsocketMessage, func(ctx *handler.WebsocketContext, next handler message := fmt.Sprintf("%s: %s", senderId, ctx.Request.Message()) err = ws.Send(context.TODO(), connectionId, []byte(message)) if err != nil { - return ctx, err + return } } - - return next(ctx) }) ``` @@ -183,77 +179,74 @@ ws.On(handler.WebsocketMessage, func(ctx *handler.WebsocketContext, next handler package main import ( - "context" - "fmt" + "context" + "fmt" - "github.com/nitrictech/go-sdk/handler" - "github.com/nitrictech/go-sdk/nitric" + "github.com/nitrictech/go-sdk/nitric" + "github.com/nitrictech/go-sdk/nitric/keyvalue" + "github.com/nitrictech/go-sdk/nitric/websockets" ) func main() { - ws, err := nitric.NewWebsocket("public") - if err != nil { - fmt.Println("Error creating WebSocket:", err) - return - } - - connections, err := nitric.NewKv("connections").Allow(nitric.KvStoreGet, nitric.KvStoreSet, nitric.KvStoreDelete) - if err != nil { - fmt.Println("Error creating KV store:", err) - return - } - - ws.On(handler.WebsocketConnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { - err := connections.Set(context.TODO(), ctx.Request.ConnectionID(), map[string]interface{}{ - "connectionId": ctx.Request.ConnectionID(), - }) - if err != nil { - return ctx, err - } - - return next(ctx) - }) - - ws.On(handler.WebsocketDisconnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { - err := connections.Delete(context.TODO(), ctx.Request.ConnectionID()) - if err != nil { - return ctx, err - } - - return next(ctx) - }) - - ws.On(handler.WebsocketMessage, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) { - connectionStream, err := connections.Keys(context.TODO()) - if err != nil { - return ctx, err - } - - 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 ctx, err - } - } - - return next(ctx) - }) - - if err := nitric.Run(); err != nil { - fmt.Println("Error running Nitric service:", err) - } + // 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 + } + }) + + 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() } ```