0

Handle viewer counts outside of websocket connections

This commit is contained in:
Gabe Kangas 2020-06-11 13:33:20 -07:00
parent ac6ee31833
commit 344da52e3d
5 changed files with 88 additions and 34 deletions

View File

@ -10,11 +10,9 @@ import (
const channelBufSize = 100 const channelBufSize = 100
var maxId int = 0
// Chat client. // Chat client.
type Client struct { type Client struct {
id int id string
ws *websocket.Conn ws *websocket.Conn
server *Server server *Server
ch chan *Message ch chan *Message
@ -32,11 +30,10 @@ func NewClient(ws *websocket.Conn, server *Server) *Client {
panic("server cannot be nil") panic("server cannot be nil")
} }
maxId++
ch := make(chan *Message, channelBufSize) ch := make(chan *Message, channelBufSize)
doneCh := make(chan bool) doneCh := make(chan bool)
clientID := getClientIDFromRequest(ws.Request())
return &Client{maxId, ws, server, ch, doneCh} return &Client{clientID, ws, server, ch, doneCh}
} }
func (c *Client) Conn() *websocket.Conn { func (c *Client) Conn() *websocket.Conn {

19
main.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"path"
"strconv" "strconv"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -49,7 +50,15 @@ func startChatServer() {
go server.Listen() go server.Listen()
// static files // static files
http.Handle("/", http.FileServer(http.Dir("webroot"))) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, path.Join("webroot", r.URL.Path))
if path.Ext(r.URL.Path) == ".m3u8" {
clientID := getClientIDFromRequest(r)
stats.SetClientActive(clientID)
}
})
http.HandleFunc("/status", getStatus) http.HandleFunc("/status", getStatus)
log.Printf("Starting public web server on port %d", configuration.WebServerPort) log.Printf("Starting public web server on port %d", configuration.WebServerPort)
@ -81,10 +90,10 @@ func streamDisconnected() {
stats.StreamDisconnected() stats.StreamDisconnected()
} }
func viewerAdded() { func viewerAdded(clientID string) {
stats.SetViewerCount(server.ClientCount()) stats.SetClientActive(clientID)
} }
func viewerRemoved() { func viewerRemoved(clientID string) {
stats.SetViewerCount(server.ClientCount()) stats.ViewerDisconnected(clientID)
} }

View File

@ -11,7 +11,7 @@ import (
type Server struct { type Server struct {
pattern string pattern string
messages []*Message messages []*Message
clients map[int]*Client clients map[string]*Client
addCh chan *Client addCh chan *Client
delCh chan *Client delCh chan *Client
sendAllCh chan *Message sendAllCh chan *Message
@ -22,7 +22,7 @@ type Server struct {
// Create new chat server. // Create new chat server.
func NewServer(pattern string) *Server { func NewServer(pattern string) *Server {
messages := []*Message{} messages := []*Message{}
clients := make(map[int]*Client) clients := make(map[string]*Client)
addCh := make(chan *Client) addCh := make(chan *Client)
delCh := make(chan *Client) delCh := make(chan *Client)
sendAllCh := make(chan *Message) sendAllCh := make(chan *Message)
@ -100,17 +100,14 @@ func (s *Server) Listen() {
// Add new a client // Add new a client
case c := <-s.addCh: case c := <-s.addCh:
log.Println("Added new client")
s.clients[c.id] = c s.clients[c.id] = c
log.Println("Now", len(s.clients), "clients connected.") viewerAdded(c.id)
viewerAdded()
s.sendPastMessages(c) s.sendPastMessages(c)
// del a client // del a client
case c := <-s.delCh: case c := <-s.delCh:
log.Println("Delete client")
delete(s.clients, c.id) delete(s.clients, c.id)
viewerRemoved() viewerRemoved(c.id)
// broadcast message for all clients // broadcast message for all clients
case msg := <-s.sendAllCh: case msg := <-s.sendAllCh:

View File

@ -1,8 +1,18 @@
/*
Viewer counting doesn't just count the number of websocket clients that are currently connected,
because people may be watching the stream outside of the web browser via any HLS video client.
Instead we keep track of requests and consider each unique IP as a "viewer".
As a signal, however, we do use the websocket disconnect from a client as a signal that a viewer
dropped and we call ViewerDisconnected().
*/
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"log"
"math" "math"
"os" "os"
"time" "time"
@ -10,40 +20,53 @@ import (
type Stats struct { type Stats struct {
streamConnected bool `json:"-"` streamConnected bool `json:"-"`
ViewerCount int `json:"viewerCount"`
SessionMaxViewerCount int `json:"sessionMaxViewerCount"` SessionMaxViewerCount int `json:"sessionMaxViewerCount"`
OverallMaxViewerCount int `json:"overallMaxViewerCount"` OverallMaxViewerCount int `json:"overallMaxViewerCount"`
LastDisconnectTime time.Time `json:"lastDisconnectTime"` LastDisconnectTime time.Time `json:"lastDisconnectTime"`
clients map[string]time.Time
} }
func (s *Stats) Setup() { func (s *Stats) Setup() {
ticker := time.NewTicker(2 * time.Minute) s.clients = make(map[string]time.Time)
quit := make(chan struct{})
statsSaveTimer := time.NewTicker(2 * time.Minute)
go func() { go func() {
for { for {
select { select {
case <-ticker.C: case <-statsSaveTimer.C:
s.save() s.save()
case <-quit:
ticker.Stop()
return
} }
} }
}() }()
staleViewerPurgeTimer := time.NewTicker(5 * time.Second)
go func() {
for {
select {
case <-staleViewerPurgeTimer.C:
s.purgeStaleViewers()
}
}
}()
}
func (s *Stats) purgeStaleViewers() {
for clientID, lastConnectedtime := range s.clients {
timeSinceLastActive := time.Since(lastConnectedtime).Minutes()
if timeSinceLastActive > 2 {
s.ViewerDisconnected(clientID)
}
}
} }
func (s *Stats) IsStreamConnected() bool { func (s *Stats) IsStreamConnected() bool {
return s.streamConnected return s.streamConnected
} }
func (s *Stats) SetViewerCount(count int) {
s.ViewerCount = count
s.SessionMaxViewerCount = int(math.Max(float64(s.ViewerCount), float64(s.SessionMaxViewerCount)))
s.OverallMaxViewerCount = int(math.Max(float64(s.SessionMaxViewerCount), float64(s.OverallMaxViewerCount)))
}
func (s *Stats) GetViewerCount() int { func (s *Stats) GetViewerCount() int {
return s.ViewerCount return len(s.clients)
} }
func (s *Stats) GetSessionMaxViewerCount() int { func (s *Stats) GetSessionMaxViewerCount() int {
@ -54,10 +77,20 @@ func (s *Stats) GetOverallMaxViewerCount() int {
return s.OverallMaxViewerCount return s.OverallMaxViewerCount
} }
func (s *Stats) ViewerConnected() { func (s *Stats) SetClientActive(clientID string) {
fmt.Println("Marking client active:", clientID)
s.clients[clientID] = time.Now()
s.SessionMaxViewerCount = int(math.Max(float64(s.GetViewerCount()), float64(s.SessionMaxViewerCount)))
s.OverallMaxViewerCount = int(math.Max(float64(s.SessionMaxViewerCount), float64(s.OverallMaxViewerCount)))
fmt.Println("Now", s.GetViewerCount(), "clients connected.")
} }
func (s *Stats) ViewerDisconnected() { func (s *Stats) ViewerDisconnected(clientID string) {
log.Println("Removed client", clientID)
delete(s.clients, clientID)
} }
func (s *Stats) StreamConnected() { func (s *Stats) StreamConnected() {

View File

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -65,3 +66,20 @@ func resetDirectories(configuration Config) {
os.MkdirAll(path.Join(configuration.PublicHLSPath, strconv.Itoa(index)), 0777) os.MkdirAll(path.Join(configuration.PublicHLSPath, strconv.Itoa(index)), 0777)
} }
} }
func getClientIDFromRequest(req *http.Request) string {
var ipAddress string
xForwardedFor := req.Header.Get("X-FORWARDED-FOR")
if xForwardedFor != "" {
ipAddress = xForwardedFor
} else {
ipAddressString := req.RemoteAddr
ipAddressComponents := strings.Split(ipAddressString, ":")
ipAddressComponents[len(ipAddressComponents)-1] = ""
ipAddress = strings.Join(ipAddressComponents, ":")
}
// fmt.Println("IP address determined to be", ipAddress)
return ipAddress
}