Переглянути джерело

refactor: split HTTP and websocket handlers from main

master
Jan Svabenik 2 дні тому
джерело
коміт
6d99631a4b
3 змінених файлів з 332 додано та 311 видалено
  1. +262
    -0
      cmd/sdrd/http_handlers.go
  2. +4
    -311
      cmd/sdrd/main.go
  3. +66
    -0
      cmd/sdrd/ws_handlers.go

+ 262
- 0
cmd/sdrd/http_handlers.go Переглянути файл

@@ -0,0 +1,262 @@
package main

import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"

"sdr-visual-suite/internal/config"
"sdr-visual-suite/internal/detector"
"sdr-visual-suite/internal/events"
fftutil "sdr-visual-suite/internal/fft"
"sdr-visual-suite/internal/recorder"
"sdr-visual-suite/internal/runtime"
)

func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime.Manager, srcMgr *sourceManager, dspUpdates chan dspUpdate, gpuState *gpuStatus, recMgr *recorder.Manager, sigSnap *signalSnapshot, eventMu *sync.RWMutex) {
mux.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
_ = json.NewEncoder(w).Encode(cfgManager.Snapshot())
case http.MethodPost:
var update runtime.ConfigUpdate
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
prev := cfgManager.Snapshot()
next, err := cfgManager.ApplyConfig(update)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sourceChanged := prev.CenterHz != next.CenterHz || prev.SampleRate != next.SampleRate || prev.GainDb != next.GainDb || prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz
if sourceChanged {
if err := srcMgr.ApplyConfig(next); err != nil {
cfgManager.Replace(prev)
http.Error(w, "failed to apply source config", http.StatusInternalServerError)
return
}
}
if err := config.Save(cfgPath, next); err != nil {
log.Printf("config save failed: %v", err)
}
detChanged := prev.Detector.ThresholdDb != next.Detector.ThresholdDb ||
prev.Detector.MinDurationMs != next.Detector.MinDurationMs ||
prev.Detector.HoldMs != next.Detector.HoldMs ||
prev.Detector.EmaAlpha != next.Detector.EmaAlpha ||
prev.Detector.HysteresisDb != next.Detector.HysteresisDb ||
prev.Detector.MinStableFrames != next.Detector.MinStableFrames ||
prev.Detector.GapToleranceMs != next.Detector.GapToleranceMs ||
prev.Detector.CFARMode != next.Detector.CFARMode ||
prev.Detector.CFARGuardCells != next.Detector.CFARGuardCells ||
prev.Detector.CFARTrainCells != next.Detector.CFARTrainCells ||
prev.Detector.CFARRank != next.Detector.CFARRank ||
prev.Detector.CFARScaleDb != next.Detector.CFARScaleDb ||
prev.Detector.CFARWrapAround != next.Detector.CFARWrapAround ||
prev.SampleRate != next.SampleRate ||
prev.FFTSize != next.FFTSize
windowChanged := prev.FFTSize != next.FFTSize
var newDet *detector.Detector
var newWindow []float64
if detChanged {
newDet = detector.New(next.Detector, next.SampleRate, next.FFTSize)
}
if windowChanged {
newWindow = fftutil.Hann(next.FFTSize)
}
pushDSPUpdate(dspUpdates, dspUpdate{cfg: next, det: newDet, window: newWindow, dcBlock: next.DCBlock, iqBalance: next.IQBalance, useGPUFFT: next.UseGPUFFT})
_ = json.NewEncoder(w).Encode(next)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})

mux.HandleFunc("/api/sdr/settings", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var update runtime.SettingsUpdate
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
prev := cfgManager.Snapshot()
next, err := cfgManager.ApplySettings(update)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz {
if err := srcMgr.ApplyConfig(next); err != nil {
cfgManager.Replace(prev)
http.Error(w, "failed to apply sdr settings", http.StatusInternalServerError)
return
}
}
if prev.DCBlock != next.DCBlock || prev.IQBalance != next.IQBalance {
pushDSPUpdate(dspUpdates, dspUpdate{cfg: next, dcBlock: next.DCBlock, iqBalance: next.IQBalance})
}
if err := config.Save(cfgPath, next); err != nil {
log.Printf("config save failed: %v", err)
}
_ = json.NewEncoder(w).Encode(next)
})

mux.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(srcMgr.Stats())
})
mux.HandleFunc("/api/gpu", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(gpuState.snapshot())
})
mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
limit := 200
if v := r.URL.Query().Get("limit"); v != "" {
if parsed, err := strconv.Atoi(v); err == nil {
limit = parsed
}
}
var since time.Time
if v := r.URL.Query().Get("since"); v != "" {
if parsed, err := parseSince(v); err == nil {
since = parsed
} else {
http.Error(w, "invalid since", http.StatusBadRequest)
return
}
}
snap := cfgManager.Snapshot()
eventMu.RLock()
evs, err := events.ReadRecent(snap.EventPath, limit, since)
eventMu.RUnlock()
if err != nil {
http.Error(w, "failed to read events", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(evs)
})
mux.HandleFunc("/api/signals", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if sigSnap == nil {
_ = json.NewEncoder(w).Encode([]detector.Signal{})
return
}
_ = json.NewEncoder(w).Encode(sigSnap.get())
})
mux.HandleFunc("/api/decoders", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(decoderKeys(cfgManager.Snapshot()))
})
mux.HandleFunc("/api/recordings", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
snap := cfgManager.Snapshot()
list, err := recorder.ListRecordings(snap.Recorder.OutputDir)
if err != nil {
http.Error(w, "failed to list recordings", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(list)
})
mux.HandleFunc("/api/recordings/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
id := strings.TrimPrefix(r.URL.Path, "/api/recordings/")
if id == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
snap := cfgManager.Snapshot()
base := filepath.Clean(filepath.Join(snap.Recorder.OutputDir, id))
if !strings.HasPrefix(base, filepath.Clean(snap.Recorder.OutputDir)) {
http.Error(w, "invalid path", http.StatusBadRequest)
return
}
if r.URL.Path == "/api/recordings/"+id+"/audio" {
http.ServeFile(w, r, filepath.Join(base, "audio.wav"))
return
}
if r.URL.Path == "/api/recordings/"+id+"/iq" {
http.ServeFile(w, r, filepath.Join(base, "signal.cf32"))
return
}
if r.URL.Path == "/api/recordings/"+id+"/decode" {
mode := r.URL.Query().Get("mode")
cmd := buildDecoderMap(cfgManager.Snapshot())[mode]
if cmd == "" {
http.Error(w, "decoder not configured", http.StatusBadRequest)
return
}
meta, err := recorder.ReadMeta(filepath.Join(base, "meta.json"))
if err != nil {
http.Error(w, "meta read failed", http.StatusInternalServerError)
return
}
audioPath := filepath.Join(base, "audio.wav")
if _, errStat := os.Stat(audioPath); errStat != nil {
audioPath = ""
}
res, err := recorder.DecodeOnDemand(cmd, filepath.Join(base, "signal.cf32"), meta.SampleRate, audioPath)
if err != nil {
http.Error(w, res.Stderr, http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(res)
return
}
http.ServeFile(w, r, filepath.Join(base, "meta.json"))
})
mux.HandleFunc("/api/demod", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
q := r.URL.Query()
freq, _ := strconv.ParseFloat(q.Get("freq"), 64)
bw, _ := strconv.ParseFloat(q.Get("bw"), 64)
sec, _ := strconv.Atoi(q.Get("sec"))
if sec < 1 {
sec = 1
}
if sec > 10 {
sec = 10
}
mode := q.Get("mode")
data, _, err := recMgr.DemodLive(freq, bw, mode, sec)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "audio/wav")
_, _ = w.Write(data)
})
}

func newHTTPServer(addr string, webRoot string, h *hub, cfgPath string, cfgManager *runtime.Manager, srcMgr *sourceManager, dspUpdates chan dspUpdate, gpuState *gpuStatus, recMgr *recorder.Manager, sigSnap *signalSnapshot, eventMu *sync.RWMutex) *http.Server {
mux := http.NewServeMux()
registerWSHandlers(mux, h)
registerAPIHandlers(mux, cfgPath, cfgManager, srcMgr, dspUpdates, gpuState, recMgr, sigSnap, eventMu)
mux.Handle("/", http.FileServer(http.Dir(webRoot)))
return &http.Server{Addr: addr, Handler: mux}
}

func shutdownServer(server *http.Server) {
ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelTimeout()
_ = server.Shutdown(ctxTimeout)
}

+ 4
- 311
cmd/sdrd/main.go Переглянути файл

@@ -2,24 +2,18 @@ package main

import (
"context"
"encoding/json"
"flag"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"

"github.com/gorilla/websocket"

"sdr-visual-suite/internal/config"
"sdr-visual-suite/internal/detector"
"sdr-visual-suite/internal/events"
fftutil "sdr-visual-suite/internal/fft"
"sdr-visual-suite/internal/fft/gpufft"
"sdr-visual-suite/internal/mock"
@@ -113,308 +107,7 @@ func main() {

go runDSP(ctx, srcMgr, cfg, det, window, h, eventFile, eventMu, dspUpdates, gpuState, recMgr, sigSnap)

upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" || origin == "null" {
return true
}
// allow same-host or any local IP
return true
}}
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("ws upgrade failed: %v (origin: %s)", err, r.Header.Get("Origin"))
return
}
c := &client{conn: conn, send: make(chan []byte, 32), done: make(chan struct{})}
h.add(c)
defer func() {
h.remove(c)
_ = conn.Close()
}()
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
go func() {
ping := time.NewTicker(30 * time.Second)
defer ping.Stop()
for {
select {
case msg, ok := <-c.send:
if !ok {
return
}
_ = conn.SetWriteDeadline(time.Now().Add(200 * time.Millisecond))
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-ping.C:
_ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("ws ping error: %v", err)
return
}
}
}
}()
for {
_, _, err := conn.ReadMessage()
if err != nil {
return
}
}
})

http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
_ = json.NewEncoder(w).Encode(cfgManager.Snapshot())
case http.MethodPost:
var update runtime.ConfigUpdate
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
prev := cfgManager.Snapshot()
next, err := cfgManager.ApplyConfig(update)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sourceChanged := prev.CenterHz != next.CenterHz || prev.SampleRate != next.SampleRate || prev.GainDb != next.GainDb || prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz
if sourceChanged {
if err := srcMgr.ApplyConfig(next); err != nil {
cfgManager.Replace(prev)
http.Error(w, "failed to apply source config", http.StatusInternalServerError)
return
}
}
if err := config.Save(cfgPath, next); err != nil {
log.Printf("config save failed: %v", err)
}
detChanged := prev.Detector.ThresholdDb != next.Detector.ThresholdDb ||
prev.Detector.MinDurationMs != next.Detector.MinDurationMs ||
prev.Detector.HoldMs != next.Detector.HoldMs ||
prev.Detector.EmaAlpha != next.Detector.EmaAlpha ||
prev.Detector.HysteresisDb != next.Detector.HysteresisDb ||
prev.Detector.MinStableFrames != next.Detector.MinStableFrames ||
prev.Detector.GapToleranceMs != next.Detector.GapToleranceMs ||
prev.Detector.CFARMode != next.Detector.CFARMode ||
prev.Detector.CFARGuardCells != next.Detector.CFARGuardCells ||
prev.Detector.CFARTrainCells != next.Detector.CFARTrainCells ||
prev.Detector.CFARRank != next.Detector.CFARRank ||
prev.Detector.CFARScaleDb != next.Detector.CFARScaleDb ||
prev.Detector.CFARWrapAround != next.Detector.CFARWrapAround ||
prev.SampleRate != next.SampleRate ||
prev.FFTSize != next.FFTSize
windowChanged := prev.FFTSize != next.FFTSize
var newDet *detector.Detector
var newWindow []float64
if detChanged {
newDet = detector.New(next.Detector, next.SampleRate, next.FFTSize)
}
if windowChanged {
newWindow = fftutil.Hann(next.FFTSize)
}
pushDSPUpdate(dspUpdates, dspUpdate{
cfg: next,
det: newDet,
window: newWindow,
dcBlock: next.DCBlock,
iqBalance: next.IQBalance,
useGPUFFT: next.UseGPUFFT,
})
_ = json.NewEncoder(w).Encode(next)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})

http.HandleFunc("/api/sdr/settings", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var update runtime.SettingsUpdate
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
prev := cfgManager.Snapshot()
next, err := cfgManager.ApplySettings(update)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz {
if err := srcMgr.ApplyConfig(next); err != nil {
cfgManager.Replace(prev)
http.Error(w, "failed to apply sdr settings", http.StatusInternalServerError)
return
}
}
if prev.DCBlock != next.DCBlock || prev.IQBalance != next.IQBalance {
pushDSPUpdate(dspUpdates, dspUpdate{
cfg: next,
dcBlock: next.DCBlock,
iqBalance: next.IQBalance,
})
}
if err := config.Save(cfgPath, next); err != nil {
log.Printf("config save failed: %v", err)
}
_ = json.NewEncoder(w).Encode(next)
})

http.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(srcMgr.Stats())
})

http.HandleFunc("/api/gpu", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(gpuState.snapshot())
})

http.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
limit := 200
if v := r.URL.Query().Get("limit"); v != "" {
if parsed, err := strconv.Atoi(v); err == nil {
limit = parsed
}
}
var since time.Time
if v := r.URL.Query().Get("since"); v != "" {
if parsed, err := parseSince(v); err == nil {
since = parsed
} else {
http.Error(w, "invalid since", http.StatusBadRequest)
return
}
}
snap := cfgManager.Snapshot()
eventMu.RLock()
evs, err := events.ReadRecent(snap.EventPath, limit, since)
eventMu.RUnlock()
if err != nil {
http.Error(w, "failed to read events", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(evs)
})

http.HandleFunc("/api/signals", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if sigSnap == nil {
_ = json.NewEncoder(w).Encode([]detector.Signal{})
return
}
_ = json.NewEncoder(w).Encode(sigSnap.get())
})

http.HandleFunc("/api/decoders", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(decoderKeys(cfgManager.Snapshot()))
})

http.HandleFunc("/api/recordings", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
snap := cfgManager.Snapshot()
list, err := recorder.ListRecordings(snap.Recorder.OutputDir)
if err != nil {
http.Error(w, "failed to list recordings", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(list)
})

http.HandleFunc("/api/recordings/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
id := strings.TrimPrefix(r.URL.Path, "/api/recordings/")
if id == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
snap := cfgManager.Snapshot()
base := filepath.Clean(filepath.Join(snap.Recorder.OutputDir, id))
if !strings.HasPrefix(base, filepath.Clean(snap.Recorder.OutputDir)) {
http.Error(w, "invalid path", http.StatusBadRequest)
return
}
if r.URL.Path == "/api/recordings/"+id+"/audio" {
http.ServeFile(w, r, filepath.Join(base, "audio.wav"))
return
}
if r.URL.Path == "/api/recordings/"+id+"/iq" {
http.ServeFile(w, r, filepath.Join(base, "signal.cf32"))
return
}
if r.URL.Path == "/api/recordings/"+id+"/decode" {
mode := r.URL.Query().Get("mode")
cmd := buildDecoderMap(cfgManager.Snapshot())[mode]
if cmd == "" {
http.Error(w, "decoder not configured", http.StatusBadRequest)
return
}
meta, err := recorder.ReadMeta(filepath.Join(base, "meta.json"))
if err != nil {
http.Error(w, "meta read failed", http.StatusInternalServerError)
return
}
audioPath := filepath.Join(base, "audio.wav")
if _, errStat := os.Stat(audioPath); errStat != nil {
audioPath = ""
}
res, err := recorder.DecodeOnDemand(cmd, filepath.Join(base, "signal.cf32"), meta.SampleRate, audioPath)
if err != nil {
http.Error(w, res.Stderr, http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(res)
return
}
// default: meta.json
http.ServeFile(w, r, filepath.Join(base, "meta.json"))
})

http.HandleFunc("/api/demod", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
q := r.URL.Query()
freq, _ := strconv.ParseFloat(q.Get("freq"), 64)
bw, _ := strconv.ParseFloat(q.Get("bw"), 64)
sec, _ := strconv.Atoi(q.Get("sec"))
if sec < 1 {
sec = 1
}
if sec > 10 {
sec = 10
}
mode := q.Get("mode")
data, _, err := recMgr.DemodLive(freq, bw, mode, sec)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "audio/wav")
_, _ = w.Write(data)
})

http.Handle("/", http.FileServer(http.Dir(cfg.WebRoot)))

server := &http.Server{Addr: cfg.WebAddr}
server := newHTTPServer(cfg.WebAddr, cfg.WebRoot, h, cfgPath, cfgManager, srcMgr, dspUpdates, gpuState, recMgr, sigSnap, eventMu)
go func() {
log.Printf("web listening on %s", cfg.WebAddr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
@@ -425,9 +118,9 @@ func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelTimeout()
_ = server.Shutdown(ctxTimeout)
shutdownServer(server)
}





+ 66
- 0
cmd/sdrd/ws_handlers.go Переглянути файл

@@ -0,0 +1,66 @@
package main

import (
"log"
"net/http"
"time"

"github.com/gorilla/websocket"
)

func registerWSHandlers(mux *http.ServeMux, h *hub) {
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" || origin == "null" {
return true
}
return true
}}

mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("ws upgrade failed: %v (origin: %s)", err, r.Header.Get("Origin"))
return
}
c := &client{conn: conn, send: make(chan []byte, 32), done: make(chan struct{})}
h.add(c)
defer func() {
h.remove(c)
_ = conn.Close()
}()
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
go func() {
ping := time.NewTicker(30 * time.Second)
defer ping.Stop()
for {
select {
case msg, ok := <-c.send:
if !ok {
return
}
_ = conn.SetWriteDeadline(time.Now().Add(200 * time.Millisecond))
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-ping.C:
_ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("ws ping error: %v", err)
return
}
}
}
}()
for {
_, _, err := conn.ReadMessage()
if err != nil {
return
}
}
})
}

Завантаження…
Відмінити
Зберегти