init: 1.0.0
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Operator int
|
||||
|
||||
const (
|
||||
MaxDevices Operator = iota
|
||||
Admin
|
||||
SubscribeUpdate = "subscribe_update"
|
||||
)
|
||||
|
||||
// Device represents a device structure
|
||||
type Device struct {
|
||||
Session string
|
||||
DeviceID string
|
||||
Conn *websocket.Conn
|
||||
CreatedAt time.Time
|
||||
LastPingTime time.Time
|
||||
}
|
||||
|
||||
// WebSocket upgrader
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// DeviceManager manages devices
|
||||
type DeviceManager struct {
|
||||
userDevices sync.Map // userID -> []*Device
|
||||
totalOnline int32 // total online devices
|
||||
userMutexes sync.Map // userID level locks
|
||||
heartbeatTimeout int // heartbeat timeout (seconds)
|
||||
checkInterval int // heartbeat check interval (seconds)
|
||||
|
||||
// event callbacks
|
||||
OnDeviceOnline func(userID int64, deviceID, session string)
|
||||
OnDeviceOffline func(userID int64, deviceID, session string, createAt time.Time)
|
||||
OnDeviceKicked func(userID int64, deviceID, session string, operator Operator)
|
||||
OnMessage func(userID int64, deviceID, session string, message string)
|
||||
}
|
||||
|
||||
// Get user-level mutex
|
||||
func (dm *DeviceManager) getUserMutex(userID int64) *sync.Mutex {
|
||||
mu, _ := dm.userMutexes.LoadOrStore(userID, &sync.Mutex{})
|
||||
return mu.(*sync.Mutex)
|
||||
}
|
||||
|
||||
// Listen to WebSocket data
|
||||
func (dm *DeviceManager) listenToDevice(userID int64, device *Device) {
|
||||
defer func() {
|
||||
dm.removeDevice(userID, device.DeviceID) // remove device when disconnected
|
||||
}()
|
||||
|
||||
for {
|
||||
_, msg, err := device.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
zap.S().Infof("Device %s (User %d) disconnected: %v", device.DeviceID, userID, err)
|
||||
break
|
||||
}
|
||||
|
||||
message := string(msg)
|
||||
if message == "ping" || message == "heartbeat" {
|
||||
dm.UpdateHeartbeat(userID, device.DeviceID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Trigger message callback
|
||||
if dm.OnMessage != nil {
|
||||
go dm.OnMessage(userID, device.DeviceID, device.Session, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateHeartbeat updates device heartbeat
|
||||
func (dm *DeviceManager) UpdateHeartbeat(userID int64, deviceID string) {
|
||||
mu := dm.getUserMutex(userID)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if val, ok := dm.userDevices.Load(userID); ok {
|
||||
devices := val.([]*Device)
|
||||
for _, d := range devices {
|
||||
if d.DeviceID == deviceID {
|
||||
d.LastPingTime = time.Now()
|
||||
if err := d.Conn.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
|
||||
zap.S().Infof("✅ Heartbeat updated: Device %s (User %d) err: %s", deviceID, userID, err.Error())
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddDevice **Add: Device connects WebSocket and is added to the manager**
|
||||
func (dm *DeviceManager) AddDevice(w http.ResponseWriter, r *http.Request, session string, userID int64, deviceID string, maxDevices int) {
|
||||
// **Upgrade WebSocket connection**
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
zap.S().Infof("WebSocket upgrade failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
mu := dm.getUserMutex(userID)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
newDevice := &Device{
|
||||
Session: session,
|
||||
DeviceID: deviceID,
|
||||
Conn: conn,
|
||||
CreatedAt: time.Now(),
|
||||
LastPingTime: time.Now(),
|
||||
}
|
||||
|
||||
//不限制设备数量
|
||||
if maxDevices < 1 {
|
||||
maxDevices = 99
|
||||
}
|
||||
|
||||
// Get user's device list
|
||||
var restConnection bool
|
||||
var devices []*Device
|
||||
if val, ok := dm.userDevices.Load(userID); ok {
|
||||
devices = val.([]*Device)
|
||||
var tempDevice []*Device
|
||||
for _, d := range devices {
|
||||
if d.DeviceID == deviceID {
|
||||
restConnection = true
|
||||
} else {
|
||||
tempDevice = append(tempDevice, d)
|
||||
}
|
||||
}
|
||||
devices = tempDevice
|
||||
}
|
||||
|
||||
// **If exceeding the limit, kick out the earliest device**
|
||||
if !restConnection && len(devices) >= maxDevices {
|
||||
oldestDevice := devices[0]
|
||||
devices = devices[1:]
|
||||
|
||||
if dm.OnDeviceKicked != nil {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
dm.OnDeviceKicked(userID, oldestDevice.DeviceID, oldestDevice.Session, MaxDevices)
|
||||
}()
|
||||
<-done // block and wait for callback to complete
|
||||
}
|
||||
oldestDevice.Conn.Close()
|
||||
atomic.AddInt32(&dm.totalOnline, -1)
|
||||
}
|
||||
|
||||
// Add new device
|
||||
devices = append(devices, newDevice)
|
||||
dm.userDevices.Store(userID, devices)
|
||||
atomic.AddInt32(&dm.totalOnline, 1)
|
||||
|
||||
// Trigger online event
|
||||
if dm.OnDeviceOnline != nil {
|
||||
go dm.OnDeviceOnline(userID, deviceID, session)
|
||||
}
|
||||
|
||||
// Start listening
|
||||
go dm.listenToDevice(userID, newDevice)
|
||||
}
|
||||
|
||||
// removeDevice removes a device
|
||||
func (dm *DeviceManager) removeDevice(userID int64, deviceID string) {
|
||||
mu := dm.getUserMutex(userID)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if val, ok := dm.userDevices.Load(userID); ok {
|
||||
devices := val.([]*Device)
|
||||
for i, d := range devices {
|
||||
if d.DeviceID == deviceID {
|
||||
devices = append(devices[:i], devices[i+1:]...)
|
||||
d.Conn.Close()
|
||||
atomic.AddInt32(&dm.totalOnline, -1)
|
||||
|
||||
if dm.OnDeviceOffline != nil {
|
||||
go dm.OnDeviceOffline(userID, deviceID, d.Session, d.CreatedAt)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
dm.userDevices.Delete(userID)
|
||||
} else {
|
||||
dm.userDevices.Store(userID, devices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KickDevice kicks a device (supports individual device or entire user)
|
||||
func (dm *DeviceManager) KickDevice(userID int64, deviceID string) {
|
||||
mu := dm.getUserMutex(userID)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Get user's device list
|
||||
val, ok := dm.userDevices.Load(userID)
|
||||
if !ok {
|
||||
zap.S().Infof("⚠️ User %d has no online devices, unable to kick out", userID)
|
||||
return
|
||||
}
|
||||
|
||||
devices := val.([]*Device)
|
||||
var activeDevices []*Device
|
||||
|
||||
for _, d := range devices {
|
||||
if deviceID == "" || d.DeviceID == deviceID {
|
||||
// Trigger kick event callback
|
||||
if dm.OnDeviceKicked != nil {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
dm.OnDeviceKicked(userID, d.DeviceID, d.Session, Admin)
|
||||
}()
|
||||
<-done // block and wait for callback to complete
|
||||
}
|
||||
// Close WebSocket connection
|
||||
d.Conn.Close()
|
||||
atomic.AddInt32(&dm.totalOnline, -1)
|
||||
zap.S().Infof("❌ Device %s (User %d) kicked out", d.DeviceID, userID)
|
||||
} else {
|
||||
activeDevices = append(activeDevices, d)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user's device mapping
|
||||
if len(activeDevices) == 0 {
|
||||
dm.userDevices.Delete(userID)
|
||||
} else {
|
||||
dm.userDevices.Store(userID, activeDevices)
|
||||
}
|
||||
}
|
||||
|
||||
// StartHeartbeatCheck periodically checks for heartbeat timeout devices
|
||||
func (dm *DeviceManager) StartHeartbeatCheck() {
|
||||
ticker := time.NewTicker(time.Duration(dm.checkInterval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
now := time.Now()
|
||||
|
||||
dm.userDevices.Range(func(userID, val interface{}) bool {
|
||||
uid := userID.(int64)
|
||||
devices := val.([]*Device)
|
||||
|
||||
mu := dm.getUserMutex(uid)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
var activeDevices []*Device
|
||||
for _, d := range devices {
|
||||
if now.Sub(d.LastPingTime) > time.Duration(dm.heartbeatTimeout)*time.Second {
|
||||
zap.S().Infof("⚠️ Device %s (User %d) heartbeat timeout, removed", d.DeviceID, uid)
|
||||
d.Conn.Close()
|
||||
atomic.AddInt32(&dm.totalOnline, -1)
|
||||
|
||||
if dm.OnDeviceOffline != nil {
|
||||
go dm.OnDeviceOffline(uid, d.DeviceID, d.Session, d.CreatedAt)
|
||||
}
|
||||
} else {
|
||||
activeDevices = append(activeDevices, d)
|
||||
}
|
||||
}
|
||||
|
||||
if len(activeDevices) == 0 {
|
||||
dm.userDevices.Delete(uid)
|
||||
} else {
|
||||
dm.userDevices.Store(uid, activeDevices)
|
||||
}
|
||||
return true
|
||||
})
|
||||
//zap.S().Infof("Total online devices: %d\n", dm.totalOnline)
|
||||
}
|
||||
}
|
||||
|
||||
// NewDeviceManager creates a new device manager
|
||||
func NewDeviceManager(heartbeatTimeout, checkInterval int) *DeviceManager {
|
||||
dm := &DeviceManager{
|
||||
heartbeatTimeout: heartbeatTimeout,
|
||||
checkInterval: checkInterval,
|
||||
}
|
||||
go dm.StartHeartbeatCheck()
|
||||
return dm
|
||||
}
|
||||
|
||||
// SendToDevice sends a message to a specific device
|
||||
func (dm *DeviceManager) SendToDevice(userID int64, deviceID string, message string) error {
|
||||
if val, ok := dm.userDevices.Load(userID); ok {
|
||||
devices := val.([]*Device)
|
||||
if deviceID == "" {
|
||||
for _, d := range devices {
|
||||
err := d.Conn.WriteMessage(websocket.TextMessage, []byte(message))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
for _, d := range devices {
|
||||
if d.DeviceID == deviceID {
|
||||
return d.Conn.WriteMessage(websocket.TextMessage, []byte(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return fmt.Errorf("device %s (User %d) is offline", deviceID, userID)
|
||||
}
|
||||
|
||||
// Broadcast sends a message to all devices
|
||||
func (dm *DeviceManager) Broadcast(message string) {
|
||||
go func(message string) {
|
||||
dm.userDevices.Range(func(_, val interface{}) bool {
|
||||
devices := val.([]*Device)
|
||||
for _, d := range devices {
|
||||
_ = d.Conn.WriteMessage(websocket.TextMessage, []byte(message))
|
||||
}
|
||||
return true
|
||||
})
|
||||
}(message)
|
||||
|
||||
}
|
||||
|
||||
// Gracefully shut down all WebSocket connections
|
||||
func (dm *DeviceManager) Shutdown(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
zap.S().Infof("🔴 Shutting down all WebSocket connections...")
|
||||
|
||||
dm.userDevices.Range(func(userID, val interface{}) bool {
|
||||
uid := userID.(int64)
|
||||
devices := val.([]*Device)
|
||||
|
||||
for _, d := range devices {
|
||||
d.Conn.Close()
|
||||
zap.S().Infof("✅ Closed device %s (User %d)", d.DeviceID, uid)
|
||||
}
|
||||
dm.userDevices.Delete(uid)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func TestDevice(t *testing.T) {
|
||||
t.Skip("skip test")
|
||||
/* deviceManager := NewDeviceManager(10, 3)
|
||||
|
||||
deviceManager.OnDeviceOnline = func(userID int64, deviceID, session string) {
|
||||
fmt.Printf("✅ 设备 %s (用户 %d) 上线\n", deviceID, userID)
|
||||
}
|
||||
|
||||
deviceManager.OnDeviceOffline = func(userID int64, deviceID, session string) {
|
||||
fmt.Printf("❌ 设备 %s (用户 %d) 下线\n", deviceID, userID)
|
||||
}
|
||||
|
||||
deviceManager.OnDeviceKicked = func(userID int64, deviceID, session string, operator Operator) {
|
||||
fmt.Printf("⚠️ 设备 %s (用户 %d) 被踢下线\n", deviceID, userID)
|
||||
}
|
||||
deviceManager.OnMessage = func(userID int64, deviceID, session string, message string) {
|
||||
log.Printf("✅收到消息: 设备 %s (用户 %d) 内容: %s,sesion: %s\n", deviceID, userID, message, session)
|
||||
}
|
||||
engine := gin.Default()
|
||||
engine.GET("/ws/:userid/:device_number", func(c *gin.Context) {
|
||||
//根据Authorization获取session
|
||||
authorization := c.GetHeader("Authorization")
|
||||
userid, err := strconv.ParseInt(c.Param("userid"), 10, 64)
|
||||
if err != nil {
|
||||
t.Errorf("get user id err:%v", err)
|
||||
return
|
||||
}
|
||||
deviceNumber := c.Param("device_number")
|
||||
deviceManager.AddDevice(c, authorization, userid, deviceNumber, 3)
|
||||
return
|
||||
})
|
||||
go func() {
|
||||
err := http.ListenAndServe(":8081", engine)
|
||||
if err != nil {
|
||||
t.Fatalf("engine start failed: %v", err)
|
||||
}
|
||||
}()
|
||||
*/
|
||||
h := http.Header{}
|
||||
h.Add("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJTZXNzaW9uSWQiOiIwMTk0Y2ZiNy1hYjY0LTdjYjMtODUzYi03ZGU5YTAzNWRlZTgiLCJVc2VySWQiOjI5LCJleHAiOjE3MzkyNTY1MDgsImlhdCI6MTczODY1MTcwOH0.BGKT5-hongJPZrA_yAb6cf6go5iDR8T9uu1ZxUg8HDw")
|
||||
|
||||
mutex := sync.Mutex{}
|
||||
serverURL := fmt.Sprintf("ws://localhost:8080/v1/app/ws/%d/%s", 29, "15502502051") // 假设 userID 为 1001,设备ID 为 deviceA
|
||||
|
||||
// 建立 WebSocket 连接
|
||||
conn, resp, err := websocket.DefaultDialer.Dial(serverURL, h)
|
||||
if err != nil {
|
||||
all, err := io.ReadAll(resp.Body)
|
||||
t.Fatalf("websocket dial failed: %v:%s", err, string(all))
|
||||
}
|
||||
// 启动一个 goroutine 来读取服务器消息
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) || strings.Contains(err.Error(), "use of closed network connection") {
|
||||
log.Println("连接已关闭")
|
||||
return
|
||||
}
|
||||
log.Printf("接收消息失败: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("收到来自服务器的消息: %s\n", msg)
|
||||
}
|
||||
}()
|
||||
|
||||
//发送心跳
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * 5)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
mutex.Lock()
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte("ping"))
|
||||
mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "use of closed network connection") {
|
||||
log.Println("连接已关闭")
|
||||
return
|
||||
}
|
||||
t.Errorf("websocket 写入失败: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
updateSubscribe, _ := json.Marshal(map[string]interface{}{
|
||||
"method": "test_method",
|
||||
})
|
||||
|
||||
//发送一条消息
|
||||
mutex.Lock()
|
||||
err = conn.WriteMessage(websocket.TextMessage, updateSubscribe)
|
||||
mutex.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("websocket write failed: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 20)
|
||||
conn.Close()
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user