-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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"] |
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. |
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) | ||
} | ||
} |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But than the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
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 |
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= |
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) | ||
} |
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@skonto
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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