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

Adding WebSocket Go example #6109

Merged
merged 1 commit into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions code-samples/serving/websockets-go/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Use the official Golang image to create a build artifact.
# This is based on Debian and sets the GOPATH to /go.
FROM golang:latest as builder

ARG TARGETOS
Copy link
Contributor

@skonto skonto Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally works as expected:

$ wscat --connect 127.0.0.1:8080/ws
Connected (press CTRL+C to quit)
> hello
< hello
> hi
< hi
> 

Copy link
Contributor

@skonto skonto Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent with the other samples eg. grpc we need to add a section for running with docker, something like in the example for grpc

docker run --rm {username}/grpc-ping-go \
  /client \
  -server_addr="grpc-ping.default.1.2.3.4.sslip.io:80" \
  -insecure

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never done that before. I mostly do never use docker, I just run things w/ ko

feel free to add things to this post.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand but the docs have a common pattern to follow for now see my comment above about ko.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skonto

docker run --rm docker.io/matzew/ws-goss \      
  /server \                                                 
  -server_addr="websockets-go.default.1.2.3.4.sslip.io:80" \
  -insecure

I am not getting this to work, please feel free to add the correct content. Otherwise we merge this and deal with this later?

The section above shows how to the binary w/in kube already. Not really adding too much value

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could do go run cmd/server/main.go ...

and localhost - that is simple enought - and has similar value

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is ok let's fix this in #6110

ARG TARGETARCH

# Create and change to the app directory.
WORKDIR /app

# Initialize the Go module inside the Dockerfile.
RUN go mod init mymodule

# Copy local code to the container image.
COPY . ./

# Install dependencies and tidy up the go.mod and go.sum files.
RUN go mod tidy

# Build the binary.
# -mod=readonly ensures immutable go.mod and go.sum in container builds.
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -mod=readonly -v -o server ./cmd/server

# Use the official Alpine image for a lean production container.
# https://hub.docker.com/_/alpine
# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
FROM alpine:3
RUN apk add --no-cache ca-certificates

# Copy the binary to the production image from the builder stage.
COPY --from=builder /app/server /server

# Run the web service on container startup.
CMD ["/server"]
138 changes: 138 additions & 0 deletions code-samples/serving/websockets-go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# WebSocket - Go

A simple [WebSocket](https://datatracker.ietf.org/doc/html/rfc6455) server that performs the HTTP upgrade and prints log messages on all standardized WebSocket events, such as `open`, `message`, `close` and `error`. The server is written in Golang and uses the [Gorilla WebSocket](github.com/gorilla/websocket) library.


## Before you begin

- A Kubernetes cluster with Knative installed and DNS configured. See
[Install Knative Serving](https://knative.dev/docs/install/serving/install-serving-with-yaml).
- [ko](https://github.com/ko-build/ko) or [Docker](https://www.docker.com) installed and running on your local machine,
and a Docker Hub account configured (we'll use it for a container registry).

## The sample code.

1. If you look in `cmd/server/main.go`, you will the `main` function setting a `handleWebSocket` function and starting the web server on the `/ws` context:

```go
func main() {
http.HandleFunc("/ws", handleWebSocket)
fmt.Println("Starting server on :8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Server error: %v", err)
}
}
```

2. The `handleWebSocket` performs the protocol upgrade and assigns various websocket handler functions, such as `OnOpen` or `OnMessage`:

```go
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Error upgrading to websocket: %v", err)
return
}
handlers.OnOpen(conn)

go func() {
defer handlers.OnClose(conn)
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
handlers.OnError(conn, err)
break
}
handlers.OnMessage(conn, messageType, message)
}
}()
}
```

3. The WebSocket application logic is located in the `pkg/handlers/handlers.go` file and contains callbacks for each WebSocket event:

```go
func OnOpen(conn *websocket.Conn) {
log.Printf("WebSocket connection opened: %v", conn.RemoteAddr())
}

func OnMessage(conn *websocket.Conn, messageType int, message []byte) {
log.Printf("Received message from %v: %s", conn.RemoteAddr(), string(message))

if err := conn.WriteMessage(messageType, message); err != nil {
log.Printf("Error sending message: %v", err)
}
}

func OnClose(conn *websocket.Conn) {
log.Printf("WebSocket connection closed: %v", conn.RemoteAddr())
conn.Close()
}

func OnError(conn *websocket.Conn, err error) {
log.Printf("WebSocket error from %v: %v", conn.RemoteAddr(), err)
}
```

## Build the application

### Dockerfile

* If you look in `Dockerfile`, you will see a method for pulling in the dependencies and building a small Go container based on Alpine. You can build and push this to your registry of choice via:
```bash
# Build and push the container on your local machine.
docker buildx build --platform linux/arm64,linux/amd64 -t "<image>" --push .
```

### ko

* You can use `ko` to build and push just the image with:
```bash
ko publish github.com/knative/docs/code-samples/serving/websockets-go
```
However, if you use `ko` for the next step, this is not necessary.

## Deploy the application

### yaml (with Dockerfile)
* If you look in `service.yaml`, take the `<image>` name you used earlier and insert it into the `image:` field, then run:
```bash
kubectl apply -f config/service.yaml
```

### yaml (with ko)
* If using `ko` to build and push:
```bash
ko apply -f config/service.yaml
```

## Testing the WebSocket server

Get the URL for your Service with:

```bash
kubectl get ksvc
NAME URL LATESTCREATED LATESTREADY READY REASON
websocket-server http://websocket-server.default.svc.cluster.local websocket-server-00001 websocket-server-00001 True
```

Now run a container with the [wscat](https://github.com/websockets/wscat) CLI and point it to the WebSocket application `ws://websocket-server.default.svc.cluster.local/ws`, like:


```bash
kubectl run --rm -i --tty wscat --image=monotykamary/wscat --restart=Never -- -c ws://websocket-server.default.svc.cluster.local/ws
```

Afterward you can chat with the WebSocket server like:

```bash
```If you don't see a command prompt, try pressing enter.
```connected (press CTRL+C to quit)
```> Hello
```< Hello
```>
```

The above is scaling to exactly one pod, since only one client was connected. Since Knative Serving allows you a dynamic scalling, a certain number of concurrent connections lead to a number of pods.

>> **NOTE:** Depending on the target annotation you have ([`autoscaling.knative.dev/target`](https://knative.dev/docs/serving/autoscaling/autoscaling-targets/)) you can scale based on num of connections.
45 changes: 45 additions & 0 deletions code-samples/serving/websockets-go/cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"fmt"
"log"
"net/http"

"github.com/gorilla/websocket"
"github.com/knative/docs/code-samples/serving/websockets-go/pkg/handlers"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Error upgrading to websocket: %v", err)
return
}
handlers.OnOpen(conn)

go func() {
defer handlers.OnClose(conn)
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
handlers.OnError(conn, err)
break
}
handlers.OnMessage(conn, messageType, message)
}
}()
}

func main() {
http.HandleFunc("/ws", handleWebSocket)
fmt.Println("Starting server on :8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Server error: %v", err)
}
}
10 changes: 10 additions & 0 deletions code-samples/serving/websockets-go/config/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: websockets-go
namespace: default
spec:
template:
spec:
containers:
- image: ko://github.com/knative/docs/code-samples/serving/websockets-go/cmd/server
Copy link
Contributor

@skonto skonto Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other samples have docker.io/{username}/grpc-ping-go, so let's keep consistency.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But than the ko does not work. (also the docker buildx does not directly work for me w/ podman, b/c of the --push. that's why I want to keep it.

Copy link
Contributor

@skonto skonto Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we remove that part for now, we don't use ko in the other examples. I am not saying we should or not use it, just suggesting that we keep things as is for now. Updating the samples with a different build approach is the topic for another PR.

Copy link
Contributor

@skonto skonto Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the past we added buildx and since it touches many files any other addition should be more visible and get proper reviews. On top of that, if something does not work we can open another issue and fix it (migrate to ko etc).
Btw I am in favor of adding more options as suggested here #5273 (comment).

Copy link
Contributor

@skonto skonto Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rhuss @pierDipi wdyth? Any objections to stamp?

5 changes: 5 additions & 0 deletions code-samples/serving/websockets-go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/knative/docs/code-samples/serving/websockets-go

go 1.22

require github.com/gorilla/websocket v1.5.0
2 changes: 2 additions & 0 deletions code-samples/serving/websockets-go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
28 changes: 28 additions & 0 deletions code-samples/serving/websockets-go/pkg/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package handlers

import (
"log"

"github.com/gorilla/websocket"
)

func OnOpen(conn *websocket.Conn) {
log.Printf("WebSocket connection opened: %v", conn.RemoteAddr())
}

func OnMessage(conn *websocket.Conn, messageType int, message []byte) {
log.Printf("Received message from %v: %s", conn.RemoteAddr(), string(message))

if err := conn.WriteMessage(messageType, message); err != nil {
log.Printf("Error sending message: %v", err)
}
}

func OnClose(conn *websocket.Conn) {
log.Printf("WebSocket connection closed: %v", conn.RemoteAddr())
conn.Close()
}

func OnError(conn *websocket.Conn, err error) {
log.Printf("WebSocket error from %v: %v", conn.RemoteAddr(), err)
}
1 change: 1 addition & 0 deletions docs/samples/serving.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ See [all Knative code samples](https://github.com/knative/docs/tree/main/code-sa
| Kong Routing | An example of mapping multiple Knative services to different paths under a single domain name using the Kong API gateway. | [Go](https://github.com/knative/docs/tree/main/code-samples/serving/kong-routing-go) |
| Knative Secrets | A simple app that demonstrates how to use a Kubernetes secret as a Volume in Knative. | [Go](https://github.com/knative/docs/tree/main/code-samples/serving/secrets-go) |
| Multi Container | A quick introduction that highlights how to build and deploy an app using Knative Serving for multiple containers. | [Go](https://github.com/knative/docs/tree/main/code-samples/serving/multi-container) |
| WebSocket Server | A simple WebSocket server. | [Go](https://github.com/knative/docs/tree/main/code-samples/serving/websocket-go) |
Loading