mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
165 lines
4.9 KiB
Go
165 lines
4.9 KiB
Go
// Copyright 2014 Team 254. All Rights Reserved.
|
|
// Author: pat@patfairbank.com (Patrick Fairbank)
|
|
//
|
|
// Functions for the server side of handling websockets.
|
|
|
|
package websocket
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/gorilla/websocket"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const pingInterval = time.Second * 10
|
|
|
|
// Wraps the Gorilla Websocket module so that we can define additional functions on it.
|
|
type Websocket struct {
|
|
conn *websocket.Conn
|
|
writeMutex *sync.Mutex
|
|
}
|
|
|
|
type Message struct {
|
|
Type string `json:"type"`
|
|
Data interface{} `json:"data"`
|
|
}
|
|
|
|
var websocketUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 2014}
|
|
|
|
// Upgrades the given HTTP request to a websocket connection.
|
|
func NewWebsocket(w http.ResponseWriter, r *http.Request) (*Websocket, error) {
|
|
conn, err := websocketUpgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Websocket{conn, new(sync.Mutex)}, nil
|
|
}
|
|
|
|
func NewTestWebsocket(conn *websocket.Conn) *Websocket {
|
|
return &Websocket{conn, new(sync.Mutex)}
|
|
}
|
|
|
|
func (ws *Websocket) Close() error {
|
|
return ws.conn.Close()
|
|
}
|
|
|
|
func (ws *Websocket) Read() (string, interface{}, error) {
|
|
var message Message
|
|
err := ws.conn.ReadJSON(&message)
|
|
if websocket.IsCloseError(err, websocket.CloseAbnormalClosure, websocket.CloseGoingAway,
|
|
websocket.CloseNoStatusReceived) {
|
|
// This error indicates that the browser terminated the connection normally; rewrite it so that clients don't
|
|
// log it.
|
|
return "", nil, io.EOF
|
|
}
|
|
if err != nil {
|
|
// Include the caller of this method in the error message.
|
|
_, file, line, _ := runtime.Caller(1)
|
|
filePathParts := strings.Split(file, "/")
|
|
return "", nil, fmt.Errorf("[%s:%d] Websocket read error: %v", filePathParts[len(filePathParts)-1], line, err)
|
|
}
|
|
return message.Type, message.Data, nil
|
|
}
|
|
|
|
func (ws *Websocket) ReadWithTimeout(timeout time.Duration) (string, interface{}, error) {
|
|
type wsReadResult struct {
|
|
messageType string
|
|
message interface{}
|
|
err error
|
|
}
|
|
readChan := make(chan wsReadResult, 1)
|
|
go func() {
|
|
messageType, message, err := ws.Read()
|
|
readChan <- wsReadResult{messageType, message, err}
|
|
}()
|
|
|
|
select {
|
|
case result := <-readChan:
|
|
return result.messageType, result.message, result.err
|
|
case <-time.After(timeout):
|
|
return "", nil, fmt.Errorf("Websocket read timed out after waiting for %v", timeout)
|
|
}
|
|
}
|
|
|
|
func (ws *Websocket) Write(messageType string, data interface{}) error {
|
|
ws.writeMutex.Lock()
|
|
defer ws.writeMutex.Unlock()
|
|
err := ws.conn.WriteJSON(Message{messageType, data})
|
|
if err != nil {
|
|
// Include the caller of this method in the error message.
|
|
_, file, line, _ := runtime.Caller(1)
|
|
filePathParts := strings.Split(file, "/")
|
|
return fmt.Errorf("[%s:%d] Websocket write error: %v", filePathParts[len(filePathParts)-1], line, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ws *Websocket) WriteNotifier(notifier *Notifier) error {
|
|
return ws.Write(notifier.messageType, notifier.getMessageBody())
|
|
}
|
|
|
|
func (ws *Websocket) WriteError(errorMessage string) error {
|
|
return ws.Write("error", errorMessage)
|
|
}
|
|
|
|
// Creates listeners for the given notifiers and loops forever to pass their output directly through to the websocket.
|
|
func (ws *Websocket) HandleNotifiers(notifiers ...*Notifier) {
|
|
// Use reflection to dynamically build a select/case structure for all the notifiers.
|
|
listeners := make([]reflect.SelectCase, len(notifiers))
|
|
for i, notifier := range notifiers {
|
|
listener := notifier.listen()
|
|
defer close(listener)
|
|
listeners[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(listener)}
|
|
|
|
// Send each notifier's respective data immediately upon connection to bootstrap the client state.
|
|
if notifier.messageProducer != nil {
|
|
err := ws.WriteNotifier(notifier)
|
|
if err != nil {
|
|
log.Printf("Websocket error writing inital value for notifier %v: %v", notifier, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add an additional case to periodically ping the websocket to detect whether the client has closed it.
|
|
pingCase := reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(time.Tick(pingInterval))}
|
|
pingIndex := len(listeners)
|
|
listeners = append(listeners, pingCase)
|
|
|
|
for {
|
|
// Block until a message is available on any of the channels.
|
|
chosenIndex, value, ok := reflect.Select(listeners)
|
|
if ok && chosenIndex == pingIndex {
|
|
err := ws.Write("ping", nil)
|
|
if err != nil {
|
|
// The client has probably closed the connection; bail out of the loop.
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
if !ok {
|
|
log.Printf("Channel for notifier %v closed unexpectedly.", notifiers[chosenIndex])
|
|
return
|
|
}
|
|
message, ok := value.Interface().(messageEnvelope)
|
|
if !ok {
|
|
log.Printf("Channel for notifier %v sent unexpected value %v.", notifiers[chosenIndex], value)
|
|
continue
|
|
}
|
|
|
|
// Forward the message verbatim on to the websocket.
|
|
err := ws.Write(message.messageType, message.messageBody)
|
|
if err != nil {
|
|
// The client has probably closed the connection; bail out of the loop.
|
|
return
|
|
}
|
|
}
|
|
}
|