Init
This commit is contained in:
commit
e4523df602
42
.gitea/workflows/ci.yml
Normal file
42
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,42 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
28
.gitea/workflows/release.yml
Normal file
28
.gitea/workflows/release.yml
Normal file
@ -0,0 +1,28 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Build release binaries
|
||||
run: VERSION=${{ github.ref_name }} make release
|
||||
|
||||
- name: Upload Release Assets
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: websocket-relay
|
||||
path: build/*
|
||||
overwrite: true
|
||||
|
||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# Binaries
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
websocket-relay
|
||||
build/
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# SSL certificates
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Other
|
||||
config.yaml
|
||||
25
Makefile
Normal file
25
Makefile
Normal file
@ -0,0 +1,25 @@
|
||||
.PHONY: build test release clean
|
||||
|
||||
BINARY_NAME=websocket-relay
|
||||
VERSION?=1.0.0
|
||||
|
||||
build:
|
||||
mkdir -p build
|
||||
go build -o build/$(BINARY_NAME) .
|
||||
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
release:
|
||||
mkdir -p build
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s -X main.version=$(VERSION)" -o build/$(BINARY_NAME)-linux-amd64 .
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-w -s -X main.version=$(VERSION)" -o build/$(BINARY_NAME)-darwin-arm64 .
|
||||
|
||||
clean:
|
||||
rm -rf build/$(BINARY_NAME)*
|
||||
|
||||
run:
|
||||
go run .
|
||||
|
||||
deps:
|
||||
go mod tidy
|
||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# WebSocket Relay Server
|
||||
|
||||
A minimal Go WebSocket relay server with SSL support for P2P connections.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
# Configure via config.yaml (see config.yaml for options)
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config.yaml` to configure:
|
||||
- **Server port and TLS settings**
|
||||
- **SSL certificate paths**
|
||||
|
||||
## Usage
|
||||
|
||||
- WebSocket endpoint: `/ws`
|
||||
- All WebSocket messages are relayed to all connected clients
|
||||
|
||||
## Testing
|
||||
|
||||
```javascript
|
||||
// For TLS enabled (default config)
|
||||
const ws = new WebSocket('wss://localhost:8443/ws');
|
||||
// For HTTP only
|
||||
// const ws = new WebSocket('ws://localhost:8443/ws');
|
||||
ws.onmessage = (event) => console.log('Received:', event.data);
|
||||
ws.send('Hello from client!');
|
||||
```
|
||||
6
config.example.yaml
Normal file
6
config.example.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
server:
|
||||
port: 8443
|
||||
tls:
|
||||
enabled: true
|
||||
cert_file: cert.pem
|
||||
key_file: key.pem
|
||||
86
example/index.html
Normal file
86
example/index.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Websocket relay example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<style>
|
||||
.chat {
|
||||
width: 600px;
|
||||
height: 500px;
|
||||
border: 1px solid black;
|
||||
overflow-y: scroll;
|
||||
background-color: beige;
|
||||
padding: 15px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.chat .bubble {
|
||||
width: calc(100% - 30px);
|
||||
border-radius: 10px;
|
||||
background-color: bisque;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.chat .error {
|
||||
color: red;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<h1>P2P Chat</h1>
|
||||
<div class="chat" id="box"></div>
|
||||
<textarea id="message"></textarea>
|
||||
<br />
|
||||
<button id="send" disabled>Send</button>
|
||||
</div>
|
||||
<script>
|
||||
const chat = document.getElementById("box");
|
||||
const message = document.getElementById("message");
|
||||
const btn = document.getElementById("send");
|
||||
const name = Date.now().toString(36);
|
||||
let retry = 1000;
|
||||
let ws;
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket('ws://localhost:8000/');
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
console.log('Received:', event.data);
|
||||
chat.innerHTML += `<div class="bubble">${event.data}</div>`;
|
||||
};
|
||||
ws.onopen = () => {
|
||||
btn.removeAttribute("disabled");
|
||||
ws.send(`${name} joined the chat`);
|
||||
};
|
||||
ws.onerror = (ev) => {
|
||||
btn.setAttribute("disabled", "disabled");
|
||||
chat.innerHTML += `<div class="error">Failed to connect to websocket, retrying...</div>`;
|
||||
console.error(ev);
|
||||
delete ws.onmessage;
|
||||
delete ws.onopen;
|
||||
setTimeout(connect, retry);
|
||||
retry *= 2;
|
||||
};
|
||||
}
|
||||
btn.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
const msg = message.value.trim();
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
const data = `${name}<br>${msg}`;
|
||||
ws.send(data);
|
||||
message.value = "";
|
||||
});
|
||||
connect();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
23
go.mod
Normal file
23
go.mod
Normal file
@ -0,0 +1,23 @@
|
||||
module websocket-relay
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
47
go.sum
Normal file
47
go.sum
Normal file
@ -0,0 +1,47 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
32
internal/config/config.go
Normal file
32
internal/config/config.go
Normal file
@ -0,0 +1,32 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Port int `yaml:"port"`
|
||||
TLS struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
CertFile string `yaml:"cert_file"`
|
||||
KeyFile string `yaml:"key_file"`
|
||||
} `yaml:"tls"`
|
||||
} `yaml:"server"`
|
||||
Metrics struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Port int `yaml:"port"`
|
||||
} `yaml:"metrics"`
|
||||
}
|
||||
|
||||
func Load(filename string) (*Config, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config Config
|
||||
err = yaml.Unmarshal(data, &config)
|
||||
return &config, err
|
||||
}
|
||||
45
internal/config/config_test.go
Normal file
45
internal/config/config_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
testConfig := `server:
|
||||
port: 9000
|
||||
tls:
|
||||
enabled: false
|
||||
cert_file: test.pem
|
||||
key_file: test.key`
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "config_test_*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
if _, err := tmpFile.WriteString(testConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
config, err := Load(tmpFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Load failed: %v", err)
|
||||
}
|
||||
|
||||
if config.Server.Port != 9000 {
|
||||
t.Errorf("Expected port 9000, got %d", config.Server.Port)
|
||||
}
|
||||
if config.Server.TLS.Enabled != false {
|
||||
t.Errorf("Expected TLS disabled, got %v", config.Server.TLS.Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileNotFound(t *testing.T) {
|
||||
_, err := Load("nonexistent.yaml")
|
||||
if err == nil {
|
||||
t.Error("Expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
97
internal/hub/hub.go
Normal file
97
internal/hub/hub.go
Normal file
@ -0,0 +1,97 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"websocket-relay/internal/metrics"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
clients map[*websocket.Conn]bool
|
||||
broadcast chan []byte
|
||||
register chan *websocket.Conn
|
||||
unregister chan *websocket.Conn
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func New() *Hub {
|
||||
return &Hub{
|
||||
clients: make(map[*websocket.Conn]bool),
|
||||
broadcast: make(chan []byte),
|
||||
register: make(chan *websocket.Conn),
|
||||
unregister: make(chan *websocket.Conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) Run() {
|
||||
for {
|
||||
select {
|
||||
case conn := <-h.register:
|
||||
h.mu.Lock()
|
||||
h.clients[conn] = true
|
||||
h.mu.Unlock()
|
||||
metrics.ConnectedClients.Set(float64(len(h.clients)))
|
||||
metrics.ConnectionsTotal.Inc()
|
||||
log.Printf("Client connected. Total: %d", len(h.clients))
|
||||
|
||||
case conn := <-h.unregister:
|
||||
h.mu.Lock()
|
||||
if _, ok := h.clients[conn]; ok {
|
||||
delete(h.clients, conn)
|
||||
conn.Close()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
metrics.ConnectedClients.Set(float64(len(h.clients)))
|
||||
metrics.DisconnectionsTotal.Inc()
|
||||
log.Printf("Client disconnected. Total: %d", len(h.clients))
|
||||
|
||||
case message := <-h.broadcast:
|
||||
metrics.MessagesTotal.Inc()
|
||||
h.mu.RLock()
|
||||
for conn := range h.clients {
|
||||
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
|
||||
delete(h.clients, conn)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("WebSocket upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.register <- conn
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
h.unregister <- conn
|
||||
}()
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
h.broadcast <- message
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *Hub) ClientCount() int {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return len(h.clients)
|
||||
}
|
||||
40
internal/hub/hub_test.go
Normal file
40
internal/hub/hub_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
h := New()
|
||||
if h == nil {
|
||||
t.Fatal("New returned nil")
|
||||
}
|
||||
if h.clients == nil {
|
||||
t.Error("clients map not initialized")
|
||||
}
|
||||
if h.broadcast == nil {
|
||||
t.Error("broadcast channel not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCount(t *testing.T) {
|
||||
h := New()
|
||||
go h.Run()
|
||||
|
||||
if count := h.ClientCount(); count != 0 {
|
||||
t.Errorf("Expected 0 clients, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastChannel(t *testing.T) {
|
||||
h := New()
|
||||
go h.Run()
|
||||
|
||||
select {
|
||||
case h.broadcast <- []byte("test"):
|
||||
// Channel is working
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("broadcast channel blocked")
|
||||
}
|
||||
}
|
||||
28
internal/metrics/metrics.go
Normal file
28
internal/metrics/metrics.go
Normal file
@ -0,0 +1,28 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
ConnectedClients = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "websocket_connected_clients",
|
||||
Help: "Number of currently connected WebSocket clients",
|
||||
})
|
||||
|
||||
MessagesTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "websocket_messages_total",
|
||||
Help: "Total number of WebSocket messages processed",
|
||||
})
|
||||
|
||||
ConnectionsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "websocket_connections_total",
|
||||
Help: "Total number of WebSocket connections established",
|
||||
})
|
||||
|
||||
DisconnectionsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "websocket_disconnections_total",
|
||||
Help: "Total number of WebSocket disconnections",
|
||||
})
|
||||
)
|
||||
43
main.go
Normal file
43
main.go
Normal file
@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"websocket-relay/internal/config"
|
||||
"websocket-relay/internal/hub"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load("config.yaml")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load config:", err)
|
||||
}
|
||||
|
||||
h := hub.New()
|
||||
go h.Run()
|
||||
|
||||
// Start metrics server if enabled
|
||||
if cfg.Metrics.Enabled {
|
||||
go func() {
|
||||
metricsAddr := fmt.Sprintf(":%d", cfg.Metrics.Port)
|
||||
log.Printf("Metrics server starting on %s", metricsAddr)
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
log.Fatal(http.ListenAndServe(metricsAddr, nil))
|
||||
}()
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/ws", h.HandleWebSocket)
|
||||
|
||||
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||||
if cfg.Server.TLS.Enabled {
|
||||
log.Printf("WebSocket relay server starting on %s (TLS)", addr)
|
||||
log.Fatal(http.ListenAndServeTLS(addr, cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile, mux))
|
||||
} else {
|
||||
log.Printf("WebSocket relay server starting on %s (HTTP)", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, mux))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user