wip: 0.2.1
1. websocket 连接,退出 优化 2. 基本页面
This commit is contained in:
parent
ec3f76e0c0
commit
3053394f03
6
frontend/src/interface/response.ts
Normal file
6
frontend/src/interface/response.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface Resp<T>{
|
||||||
|
status: number;
|
||||||
|
msg: string;
|
||||||
|
err: string;
|
||||||
|
data: T;
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
import {CloudBackground} from "../component/fluid/cloud.tsx";
|
import {CloudBackground} from "../component/fluid/cloud.tsx";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect} from "react";
|
||||||
import {createUseStyles} from "react-jss";
|
import {createUseStyles} from "react-jss";
|
||||||
import {useRoom} from "../store/ws.ts";
|
import {useRTC} from "../store/rtc.ts";
|
||||||
|
import {Client, useRoom} from "../store/local.ts";
|
||||||
|
|
||||||
const useClass = createUseStyles({
|
const useClass = createUseStyles({
|
||||||
'@global': {
|
'@global': {
|
||||||
@ -27,6 +28,12 @@ const useClass = createUseStyles({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
width: '100%',
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: '#1661ab',
|
||||||
|
},
|
||||||
bubble: {
|
bubble: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: "100px",
|
width: "100px",
|
||||||
@ -50,13 +57,10 @@ const useClass = createUseStyles({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
interface Client {
|
|
||||||
|
interface Bubble {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
|
||||||
|
|
||||||
interface BubblePosition {
|
|
||||||
id: string;
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
color: string;
|
color: string;
|
||||||
@ -66,10 +70,8 @@ interface BubblePosition {
|
|||||||
|
|
||||||
export const LocalSharing: React.FC = () => {
|
export const LocalSharing: React.FC = () => {
|
||||||
const classes = useClass();
|
const classes = useClass();
|
||||||
const [clients, setClients] = useState<Client[]>([]);
|
const {register, enter, list, cleanup, client, clients} = useRoom();
|
||||||
const [bubbles, setBubbles] = useState<BubblePosition[]>([]);
|
const {connect, create} = useRTC();
|
||||||
const {register, cleanup} = useRoom();
|
|
||||||
const BUBBLE_SIZE = 100;
|
|
||||||
|
|
||||||
// 生成随机颜色
|
// 生成随机颜色
|
||||||
const generateColor = () => {
|
const generateColor = () => {
|
||||||
@ -80,131 +82,92 @@ export const LocalSharing: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 防碰撞位置生成
|
// 防碰撞位置生成
|
||||||
const generatePosition = (existing: BubblePosition[]) => {
|
const generateBubbles = (cs: Client[]) => {
|
||||||
|
if (!cs) return []
|
||||||
|
|
||||||
|
const BUBBLE_SIZE = 100;
|
||||||
const centerX = window.innerWidth / 2;
|
const centerX = window.innerWidth / 2;
|
||||||
const centerY = window.innerHeight / 2;
|
const centerY = window.innerHeight / 2;
|
||||||
const maxRadius = Math.min(centerX, centerY) - BUBBLE_SIZE;
|
|
||||||
|
|
||||||
// 初始化参数
|
const bubbles: Bubble[] = [];
|
||||||
let radius = 0;
|
let currentRadius = 0;
|
||||||
let angle = Math.random() * Math.PI * 2;
|
let angleStep = (2 * Math.PI) / 6; // 初始6个位置
|
||||||
let attempts = 0;
|
|
||||||
|
for (let index = 0; index < cs.length; index++) {
|
||||||
|
let attempt = 0;
|
||||||
|
let validPosition = false;
|
||||||
|
|
||||||
|
if (cs[index].id == client?.id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!validPosition && attempt < 100) {
|
||||||
|
// 螺旋布局算法
|
||||||
|
const angle = angleStep * (index + attempt);
|
||||||
|
const radius = currentRadius + (attempt * BUBBLE_SIZE * 0.8);
|
||||||
|
|
||||||
do {
|
|
||||||
// 极坐标转笛卡尔坐标
|
// 极坐标转笛卡尔坐标
|
||||||
const x = centerX + radius * Math.cos(angle);
|
const x = centerX + radius * Math.cos(angle);
|
||||||
const y = centerY + radius * Math.sin(angle);
|
const y = centerY + radius * Math.sin(angle);
|
||||||
|
|
||||||
// 边界检测
|
// 边界检测
|
||||||
if (x < 0 || x > window.innerWidth - BUBBLE_SIZE ||
|
const inBounds = x >= 0 && x <= window.innerWidth - BUBBLE_SIZE &&
|
||||||
y < 0 || y > window.innerHeight - BUBBLE_SIZE) {
|
y >= 0 && y <= window.innerHeight - BUBBLE_SIZE;
|
||||||
radius = 0;
|
|
||||||
angle += Math.PI / 6;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 碰撞检测
|
// 碰撞检测
|
||||||
const collision = existing.some(bubble => {
|
const collision = bubbles.some(pos => {
|
||||||
const distance = Math.sqrt(
|
const distance = Math.sqrt(
|
||||||
Math.pow(bubble.x - x, 2) +
|
Math.pow(pos.x - x, 2) +
|
||||||
Math.pow(bubble.y - y, 2)
|
Math.pow(pos.y - y, 2)
|
||||||
);
|
);
|
||||||
return distance < BUBBLE_SIZE * 1.5;
|
return distance < BUBBLE_SIZE * 1.5;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!collision) {
|
if (inBounds && !collision) {
|
||||||
return {
|
bubbles.push({
|
||||||
x,
|
id: cs[index].id,
|
||||||
y,
|
name: cs[index].name,
|
||||||
radius,
|
x: x,
|
||||||
angle
|
y: y,
|
||||||
};
|
color: generateColor(),
|
||||||
|
} as Bubble);
|
||||||
|
|
||||||
|
// 动态调整布局参数
|
||||||
|
currentRadius = Math.max(currentRadius, radius);
|
||||||
|
angleStep = (2 * Math.PI) / Math.max(6, bubbles.length * 0.7);
|
||||||
|
validPosition = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 逐步扩大搜索半径和角度
|
attempt++;
|
||||||
radius += BUBBLE_SIZE * 0.7;
|
}
|
||||||
if (radius > maxRadius) {
|
|
||||||
radius = 0;
|
|
||||||
angle += Math.PI / 6; // 每30度尝试一次
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts++;
|
return bubbles;
|
||||||
} while (attempts < 200);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 修改updateBubbles中的生成逻辑
|
|
||||||
const updateBubbles = (newClients: Client[]) => {
|
|
||||||
const newBubbles: BubblePosition[] = [];
|
|
||||||
|
|
||||||
newClients.forEach(client => {
|
|
||||||
const existing = bubbles.find(b => b.id === client.id);
|
|
||||||
if (existing) {
|
|
||||||
newBubbles.push(existing);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = generatePosition([...bubbles, ...newBubbles]);
|
|
||||||
if (position) {
|
|
||||||
newBubbles.push({
|
|
||||||
id: client.id,
|
|
||||||
...position,
|
|
||||||
color: generateColor()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setBubbles(newBubbles);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 模拟API获取数据
|
register().then(() => {
|
||||||
const fetchData = async () => {
|
enter().then(() => {
|
||||||
// const response = await fetch('/api/clients');
|
list().then()
|
||||||
// const data = await response.json();
|
})
|
||||||
|
});
|
||||||
await register();
|
connect().then(() => {
|
||||||
|
console.log("[D] rtc create!!!")
|
||||||
const mockData: Client[] = [
|
})
|
||||||
{ id: '1', name: '宁静的梦境' },
|
|
||||||
{ id: '2', name: '温暖的时光' },
|
|
||||||
{ id: '3', name: '甜蜜的旋律' },
|
|
||||||
{ id: '4', name: '柔和的花园' }
|
|
||||||
];
|
|
||||||
setClients(mockData);
|
|
||||||
updateBubbles(mockData);
|
|
||||||
|
|
||||||
return () => cleanup();
|
return () => cleanup();
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 窗口尺寸变化处理
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
const validBubbles = bubbles.filter(bubble =>
|
|
||||||
bubble.x <= window.innerWidth - BUBBLE_SIZE &&
|
|
||||||
bubble.y <= window.innerHeight - BUBBLE_SIZE
|
|
||||||
);
|
|
||||||
setBubbles(validBubbles);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
}, [bubbles]);
|
|
||||||
|
|
||||||
// 气泡点击处理
|
// 气泡点击处理
|
||||||
const handleBubbleClick = (id: string) => {
|
const handleBubbleClick = async (id: string) => {
|
||||||
// 实际开发中这里调用API删除
|
console.log('[D] click bubble!!!', id)
|
||||||
setClients(prev => prev.filter(c => c.id !== id));
|
await create()
|
||||||
setBubbles(prev => prev.filter(b => b.id !== id));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className={classes.container}>
|
return <div className={classes.container}>
|
||||||
<CloudBackground />
|
<CloudBackground/>
|
||||||
{bubbles.map(bubble => {
|
<h1 className={classes.title}>{client?.name}</h1>
|
||||||
const client = clients.find(c => c.id === bubble.id);
|
{clients && generateBubbles(clients).map(bubble => {
|
||||||
|
// const client = clients.find(c => c.id === bubble.id);
|
||||||
return client ? (
|
return client ? (
|
||||||
<div
|
<div
|
||||||
key={bubble.id}
|
key={bubble.id}
|
||||||
@ -219,7 +182,7 @@ export const LocalSharing: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
onClick={() => handleBubbleClick(bubble.id)}
|
onClick={() => handleBubbleClick(bubble.id)}
|
||||||
>
|
>
|
||||||
{client.name}
|
{bubble.name}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
})}
|
})}
|
||||||
|
140
frontend/src/store/local.ts
Normal file
140
frontend/src/store/local.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import {create} from 'zustand'
|
||||||
|
import {Resp} from "../interface/response.ts";
|
||||||
|
|
||||||
|
export interface Client {
|
||||||
|
client_type: 'desktop' | 'mobile' | 'tablet';
|
||||||
|
app_type: 'web';
|
||||||
|
room: string;
|
||||||
|
ip: number;
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
register_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomState = {
|
||||||
|
conn: WebSocket | null
|
||||||
|
client: Client | null
|
||||||
|
clients: Client[]
|
||||||
|
retryCount: number
|
||||||
|
reconnectTimer: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomActions = {
|
||||||
|
register: () => Promise<void>
|
||||||
|
enter: () => Promise<void>
|
||||||
|
list: () => Promise<void>
|
||||||
|
cleanup: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
type: 'ping' | 'self' | 'enter' | 'leave';
|
||||||
|
time: number;
|
||||||
|
body: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_RETRY_DELAY = 30000 // 最大重试间隔30秒
|
||||||
|
const NORMAL_CLOSE_CODE = 1000 // 正常关闭的状态码
|
||||||
|
|
||||||
|
export const useRoom = create<RoomState & RoomActions>()((set, get) => ({
|
||||||
|
conn: null,
|
||||||
|
client: null,
|
||||||
|
clients: [],
|
||||||
|
retryCount: 0,
|
||||||
|
reconnectTimer: null,
|
||||||
|
register: async () => {
|
||||||
|
const api = `/api/ulocal/register`
|
||||||
|
const res = await fetch(api, {method: 'POST'})
|
||||||
|
const jes = await res.json() as Resp<Client>
|
||||||
|
return set(state => {
|
||||||
|
return {...state, client: jes.data}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
enter: async () => {
|
||||||
|
const {conn, reconnectTimer} = get()
|
||||||
|
|
||||||
|
// 清理旧连接和定时器
|
||||||
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||||
|
if (conn) conn.close()
|
||||||
|
|
||||||
|
const api = `${window.location.protocol === 'https' ? 'wss' : 'ws'}://${window.location.host}/api/ulocal/ws?id=${get().client?.id}`
|
||||||
|
console.log('[D] websocket api =',api)
|
||||||
|
const newConn = new WebSocket(api)
|
||||||
|
|
||||||
|
newConn.onopen = () => {
|
||||||
|
set({conn: newConn, retryCount: 0}) // 重置重试计数器
|
||||||
|
}
|
||||||
|
|
||||||
|
newConn.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
newConn.onmessage = (event) => {
|
||||||
|
const msg = JSON.parse(event.data) as Message;
|
||||||
|
console.log('[D] ws msg =', msg)
|
||||||
|
let nc: Client
|
||||||
|
switch (msg.type) {
|
||||||
|
case "enter":
|
||||||
|
nc = msg.body as Client
|
||||||
|
if(nc.id && nc.name && nc.id !== get().client?.id) {
|
||||||
|
console.log('[D] enter new client =', nc)
|
||||||
|
set(state => {
|
||||||
|
return {...state, clients: [...get().clients, nc]}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "leave":
|
||||||
|
nc = msg.body as Client
|
||||||
|
if(nc.id) {
|
||||||
|
let idx = 0;
|
||||||
|
let items = get().clients;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.id === nc.id) {
|
||||||
|
items.splice(idx, 1)
|
||||||
|
set(state => {
|
||||||
|
return {...state, clients: items}
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newConn.onclose = (event) => {
|
||||||
|
// 非正常关闭时触发重连
|
||||||
|
if (event.code !== NORMAL_CLOSE_CODE) {
|
||||||
|
const {retryCount} = get()
|
||||||
|
const nextRetry = retryCount + 1
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, nextRetry), MAX_RETRY_DELAY)
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
get().register()
|
||||||
|
}, delay)
|
||||||
|
|
||||||
|
set({
|
||||||
|
retryCount: nextRetry,
|
||||||
|
reconnectTimer: timer,
|
||||||
|
conn: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set({conn: newConn, reconnectTimer: null})
|
||||||
|
},
|
||||||
|
list: async () => {
|
||||||
|
const api = "/api/ulocal/clients?room="
|
||||||
|
const res = await fetch(api + get().client?.room)
|
||||||
|
const jes = await res.json() as Resp<Client[]>
|
||||||
|
set(state => {
|
||||||
|
return {...state, clients: jes.data}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cleanup: () => {
|
||||||
|
const {conn, reconnectTimer} = get()
|
||||||
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||||
|
if (conn) conn.close()
|
||||||
|
set({conn: null, retryCount: 0, reconnectTimer: null})
|
||||||
|
}
|
||||||
|
}))
|
50
frontend/src/store/rtc.ts
Normal file
50
frontend/src/store/rtc.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {create} from 'zustand'
|
||||||
|
|
||||||
|
type RTCState = {
|
||||||
|
conn: RTCPeerConnection | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type RTCAction = {
|
||||||
|
connect: () => Promise<void>
|
||||||
|
create: () => Promise<void>
|
||||||
|
cleanup: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRTC = create<RTCState & RTCAction>()((set, get) => ({
|
||||||
|
conn: null,
|
||||||
|
connect: async () => {
|
||||||
|
const conn = new RTCPeerConnection()
|
||||||
|
|
||||||
|
const ch = conn.createDataChannel("fileTransfer", {ordered: true})
|
||||||
|
|
||||||
|
console.log('[D] channel =', ch)
|
||||||
|
|
||||||
|
ch.onopen = (event) => {
|
||||||
|
console.log('🚀🚀🚀 / rtc open event', event)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.onclose = (event) => {
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.onerror = (event) => {
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.onmessage = (event) => {
|
||||||
|
console.log('🚀🚀🚀 / rtc message event', event)
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
return {...state, conn: conn}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
create: async () => {
|
||||||
|
const conn = get().conn
|
||||||
|
if (conn) conn.onicecandidate = async (event) => {
|
||||||
|
console.log('[D] rtc local desc =', conn.localDescription)
|
||||||
|
const offer = await conn.createOffer()
|
||||||
|
await conn.setLocalDescription(offer)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cleanup: () => {
|
||||||
|
},
|
||||||
|
}))
|
@ -1,74 +0,0 @@
|
|||||||
import { create } from 'zustand'
|
|
||||||
|
|
||||||
type RoomState = {
|
|
||||||
conn: WebSocket | null
|
|
||||||
retryCount: number
|
|
||||||
reconnectTimer: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoomActions = {
|
|
||||||
register: () => Promise<void>
|
|
||||||
cleanup: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_RETRY_DELAY = 30000 // 最大重试间隔30秒
|
|
||||||
const NORMAL_CLOSE_CODE = 1000 // 正常关闭的状态码
|
|
||||||
|
|
||||||
export const useRoom = create<RoomState & RoomActions>()((set, get) => ({
|
|
||||||
conn: null,
|
|
||||||
retryCount: 0,
|
|
||||||
reconnectTimer: null,
|
|
||||||
|
|
||||||
register: async () => {
|
|
||||||
const { conn, reconnectTimer } = get()
|
|
||||||
|
|
||||||
// 清理旧连接和定时器
|
|
||||||
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
||||||
if (conn) conn.close()
|
|
||||||
|
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'
|
|
||||||
const wsUrl = protocol + window.location.host + '/api/ulocal/registry'
|
|
||||||
const newConn = new WebSocket(wsUrl)
|
|
||||||
|
|
||||||
newConn.onopen = () => {
|
|
||||||
console.log('WebSocket connected')
|
|
||||||
set({ conn: newConn, retryCount: 0 }) // 重置重试计数器
|
|
||||||
}
|
|
||||||
|
|
||||||
newConn.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
newConn.onmessage = (event) => {
|
|
||||||
console.log("[D] websocket message =", event)
|
|
||||||
}
|
|
||||||
|
|
||||||
newConn.onclose = (event) => {
|
|
||||||
// 非正常关闭时触发重连
|
|
||||||
if (event.code !== NORMAL_CLOSE_CODE) {
|
|
||||||
const { retryCount } = get()
|
|
||||||
const nextRetry = retryCount + 1
|
|
||||||
const delay = Math.min(1000 * Math.pow(2, nextRetry), MAX_RETRY_DELAY)
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
get().register()
|
|
||||||
}, delay)
|
|
||||||
|
|
||||||
set({
|
|
||||||
retryCount: nextRetry,
|
|
||||||
reconnectTimer: timer,
|
|
||||||
conn: null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set({ conn: newConn, reconnectTimer: null })
|
|
||||||
},
|
|
||||||
|
|
||||||
cleanup: () => {
|
|
||||||
const { conn, reconnectTimer } = get()
|
|
||||||
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
||||||
if (conn) conn.close()
|
|
||||||
set({ conn: null, retryCount: 0, reconnectTimer: null })
|
|
||||||
}
|
|
||||||
}))
|
|
@ -10,7 +10,7 @@ export default defineConfig({
|
|||||||
target: 'http://127.0.0.1:9119',
|
target: 'http://127.0.0.1:9119',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/api/ulocal/registry': {
|
'/api/ulocal/ws': {
|
||||||
target: 'ws://127.0.0.1:9119',
|
target: 'ws://127.0.0.1:9119',
|
||||||
rewriteWsOrigin: true,
|
rewriteWsOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
|
@ -25,7 +25,9 @@ func Start(ctx context.Context) <-chan struct{} {
|
|||||||
|
|
||||||
{
|
{
|
||||||
api := app.Group("/api/ulocal")
|
api := app.Group("/api/ulocal")
|
||||||
api.Get("/registry", handler.LocalRegistry())
|
api.Post("/register", handler.LocalRegister())
|
||||||
|
api.Get("/clients", handler.LocalClients())
|
||||||
|
api.Get("/ws", handler.LocalWS())
|
||||||
}
|
}
|
||||||
|
|
||||||
ready := make(chan struct{})
|
ready := make(chan struct{})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// room controller:
|
// room controller:
|
||||||
// local share websocket room controller
|
// local share websocket room controller
|
||||||
// same remote ip as a
|
// same remote IP as a
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -29,53 +29,71 @@ const (
|
|||||||
RoomAppTypeWeb = "web"
|
RoomAppTypeWeb = "web"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RoomMessageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoomMessageTypePing RoomMessageType = "ping"
|
||||||
|
RoomMessageTypeSelf RoomMessageType = "self"
|
||||||
|
RoomMessageTypeEnter RoomMessageType = "enter"
|
||||||
|
RoomMessageTypeLeave RoomMessageType = "leave"
|
||||||
|
)
|
||||||
|
|
||||||
type roomClient struct {
|
type roomClient struct {
|
||||||
controller *roomController
|
controller *roomController
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
clientType RoomClientType
|
ClientType RoomClientType `json:"client_type"`
|
||||||
appType RoomAppType
|
AppType RoomAppType `json:"app_type"`
|
||||||
ip string
|
IP string `json:"ip"`
|
||||||
name string
|
Room string `json:"room"`
|
||||||
id string
|
Name string `json:"name"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
RegisterAt time.Time `json:"register_at"`
|
||||||
msgChan chan any
|
msgChan chan any
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *roomClient) start(ctx context.Context) {
|
func (rc *roomClient) start(ctx context.Context) {
|
||||||
|
// start write
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
_ = rc.conn.Close()
|
||||||
return
|
return
|
||||||
case msg, _ := <-rc.msgChan:
|
case msg, _ := <-rc.msgChan:
|
||||||
err := rc.conn.WriteJSON(msg)
|
err := rc.conn.WriteJSON(msg)
|
||||||
log.Debug("RoomClient: write json message, ip = %s, id = %s, name = %s, err = %v", rc.ip, rc.id, rc.name, err)
|
log.Debug("RoomClient: write json message, IP = %s, Id = %s, Name = %s, err = %v", rc.IP, rc.Id, rc.Name, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("RoomClient: write json message failed, ip = %s, id = %s, name = %s, err = %s", rc.ip, rc.id, rc.name, err.Error())
|
log.Error("RoomClient: write json message failed, IP = %s, Id = %s, Name = %s, err = %s", rc.IP, rc.Id, rc.Name, err.Error())
|
||||||
}
|
}
|
||||||
default:
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// start read
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
mt, bs, err := rc.conn.ReadMessage()
|
mt, bs, err := rc.conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("RoomClient: read message failed, ip = %s, id = %s, name = %s, err = %s", rc.ip, rc.id, rc.name, err.Error())
|
log.Error("RoomClient: read message failed, IP = %s, Id = %s, Name = %s, err = %s", rc.IP, rc.Id, rc.Name, err.Error())
|
||||||
rc.controller.Unregister(rc)
|
rc.controller.Unregister(rc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mt {
|
switch mt {
|
||||||
case websocket.PingMessage:
|
case websocket.PingMessage:
|
||||||
rs, _ := json.Marshal(map[string]any{"type": "pong", "time": time.Now().UnixMilli(), "id": rc.id, "name": rc.name})
|
rs, _ := json.Marshal(map[string]any{"type": "pong", "time": time.Now().UnixMilli(), "Id": rc.Id, "Name": rc.Name})
|
||||||
if err := rc.conn.WriteMessage(websocket.PongMessage, rs); err != nil {
|
if err := rc.conn.WriteMessage(websocket.PongMessage, rs); err != nil {
|
||||||
log.Error("RoomClient: response ping message failed, ip = %s, id = %s, name = %s, err = %s", rc.ip, rc.id, rc.name, err.Error())
|
log.Error("RoomClient: response ping message failed, IP = %s, Id = %s, Name = %s, err = %s", rc.IP, rc.Id, rc.Name, err.Error())
|
||||||
}
|
}
|
||||||
case websocket.CloseMessage:
|
case websocket.CloseMessage:
|
||||||
log.Debug("RoomClient: received close message, unregister ip = %s id = %s, name = %s", rc.ip, rc.id, rc.name)
|
log.Debug("RoomClient: received close message, unregister IP = %s Id = %s, Name = %s", rc.IP, rc.Id, rc.Name)
|
||||||
rc.controller.Unregister(rc)
|
rc.controller.Unregister(rc)
|
||||||
return
|
return
|
||||||
case websocket.TextMessage:
|
case websocket.TextMessage:
|
||||||
log.Info("RoomClient: received text message, ip = %s, id = %s, name = %s, text = %s", rc.ip, rc.id, rc.name, string(bs))
|
log.Info("RoomClient: received text message, IP = %s, Id = %s, Name = %s, text = %s", rc.IP, rc.Id, rc.Name, string(bs))
|
||||||
case websocket.BinaryMessage:
|
case websocket.BinaryMessage:
|
||||||
// todo
|
// todo
|
||||||
log.Info("RoomClient: received bytes message, ip = %s, id = %s, name = %s, text = %s", rc.ip, rc.id, rc.name, string(bs))
|
log.Info("RoomClient: received bytes message, IP = %s, Id = %s, Name = %s, text = %s", rc.IP, rc.Id, rc.Name, string(bs))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -84,51 +102,56 @@ func (rc *roomClient) start(ctx context.Context) {
|
|||||||
type roomController struct {
|
type roomController struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
rooms map[string]map[string]*roomClient // map[room_id(remote-ip)][id]
|
rooms map[string]map[string]*roomClient // map[room_id(remote-IP)][Id]
|
||||||
|
notReadies map[string]*roomClient
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
RoomController = &roomController{
|
RoomController = &roomController{
|
||||||
rooms: make(map[string]map[string]*roomClient),
|
rooms: make(map[string]map[string]*roomClient),
|
||||||
|
notReadies: make(map[string]*roomClient),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rc *roomController) Start(ctx context.Context) {
|
func (rc *roomController) Start(ctx context.Context) {
|
||||||
rc.ctx = ctx
|
rc.ctx = ctx
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-rc.ctx.Done():
|
||||||
return
|
return
|
||||||
case now := <-ticker.C:
|
case now := <-ticker.C:
|
||||||
for room := range rc.rooms {
|
for _, nrc := range rc.notReadies {
|
||||||
rc.Broadcast(room, now.String())
|
if now.Sub(nrc.RegisterAt).Minutes() > 1 {
|
||||||
|
rc.Lock()
|
||||||
|
delete(rc.notReadies, nrc.Id)
|
||||||
|
rc.Unlock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *roomController) Register(c *websocket.Conn, ip, userAgent string) {
|
func (rc *roomController) Register(ip, userAgent string) *roomClient {
|
||||||
nrc := &roomClient{
|
nrc := &roomClient{
|
||||||
controller: rc,
|
controller: rc,
|
||||||
conn: c,
|
ClientType: ClientTypeDesktop,
|
||||||
clientType: ClientTypeDesktop,
|
AppType: RoomAppTypeWeb,
|
||||||
appType: RoomAppTypeWeb,
|
IP: ip,
|
||||||
ip: ip,
|
Id: uuid.Must(uuid.NewV7()).String(),
|
||||||
id: uuid.Must(uuid.NewV7()).String(),
|
Name: tool.RandomName(),
|
||||||
name: tool.RandomName(),
|
|
||||||
msgChan: make(chan any, 1),
|
msgChan: make(chan any, 1),
|
||||||
|
RegisterAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ua := useragent.Parse(userAgent)
|
ua := useragent.Parse(userAgent)
|
||||||
switch {
|
switch {
|
||||||
case ua.Mobile:
|
case ua.Mobile:
|
||||||
nrc.clientType = ClientTypeMobile
|
nrc.ClientType = ClientTypeMobile
|
||||||
case ua.Tablet:
|
case ua.Tablet:
|
||||||
nrc.clientType = ClientTypeTablet
|
nrc.ClientType = ClientTypeTablet
|
||||||
}
|
}
|
||||||
|
|
||||||
key := "local"
|
key := "local"
|
||||||
@ -136,37 +159,84 @@ func (rc *roomController) Register(c *websocket.Conn, ip, userAgent string) {
|
|||||||
key = ip
|
key = ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nrc.Room = key
|
||||||
|
|
||||||
rc.Lock()
|
rc.Lock()
|
||||||
|
|
||||||
if _, ok := rc.rooms[key]; !ok {
|
log.Debug("controller.room: registry client, IP = %s(%s), Id = %s, Name = %s", key, nrc.IP, nrc.Id, nrc.Name)
|
||||||
rc.rooms[key] = make(map[string]*roomClient)
|
rc.notReadies[nrc.Id] = nrc
|
||||||
|
if _, ok := rc.rooms[nrc.Room]; !ok {
|
||||||
|
rc.rooms[nrc.Room] = make(map[string]*roomClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
nrc.start(rc.ctx)
|
|
||||||
log.Debug("controller.room: registry client, ip = %s(%s), id = %s, name = %s", key, nrc.ip, nrc.id, nrc.name)
|
|
||||||
rc.rooms[key][nrc.id] = nrc
|
|
||||||
|
|
||||||
rc.Unlock()
|
rc.Unlock()
|
||||||
|
|
||||||
rc.Broadcast(key, "new member")
|
return nrc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *roomController) Enter(conn *websocket.Conn, id string) {
|
||||||
|
client, ok := rc.notReadies[id]
|
||||||
|
if !ok {
|
||||||
|
log.Warn("controller.room: entry room id not exist, id = %s", id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.Lock()
|
||||||
|
|
||||||
|
if _, ok = rc.rooms[client.Room]; !ok {
|
||||||
|
log.Warn("controller.room: entry room not exist, room = %s, id = %s, name = %s", client.Room, id, client.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.rooms[client.Room][id] = client
|
||||||
|
client.conn = conn
|
||||||
|
|
||||||
|
rc.Unlock()
|
||||||
|
|
||||||
|
client.start(rc.ctx)
|
||||||
|
|
||||||
|
rc.Broadcast(client.Room, map[string]any{"type": RoomMessageTypeEnter, "time": time.Now().UnixMilli(), "body": client})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *roomController) List(room string) []*roomClient {
|
||||||
|
clientList := make([]*roomClient, 0)
|
||||||
|
|
||||||
|
rc.Lock()
|
||||||
|
defer rc.Unlock()
|
||||||
|
|
||||||
|
clients, ok := rc.rooms[room]
|
||||||
|
if !ok {
|
||||||
|
return clientList
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, client := range clients {
|
||||||
|
clientList = append(clientList, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientList
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *roomController) Broadcast(room string, msg any) {
|
func (rc *roomController) Broadcast(room string, msg any) {
|
||||||
for _, client := range rc.rooms[room] {
|
for _, client := range rc.rooms[room] {
|
||||||
client.msgChan <- msg
|
select {
|
||||||
|
case client.msgChan <- msg:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
log.Warn("RoomController: broadcast timeout, room = %s, client Id = %s, IP = %s", room, client.Id, client.IP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *roomController) Unregister(client *roomClient) {
|
func (rc *roomController) Unregister(client *roomClient) {
|
||||||
key := "local"
|
key := "local"
|
||||||
if !tool.IsPrivateIP(client.ip) {
|
if !tool.IsPrivateIP(client.IP) {
|
||||||
key = client.ip
|
key = client.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug("controller.room: unregister client, IP = %s(%s), Id = %s, Name = %s", client.IP, key, client.Id, client.Name)
|
||||||
|
|
||||||
rc.Lock()
|
rc.Lock()
|
||||||
defer rc.Unlock()
|
delete(rc.rooms[key], client.Id)
|
||||||
|
rc.Unlock()
|
||||||
|
|
||||||
log.Debug("controller.room: unregister client, ip = %s(%s), id = %s, name = %s", client.ip, key, client.id, client.name)
|
rc.Broadcast(key, map[string]any{"type": RoomMessageTypeLeave, "time": time.Now().UnixMilli(), "body": client})
|
||||||
|
|
||||||
delete(rc.rooms[key], client.id)
|
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,36 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/loveuer/nf"
|
"github.com/loveuer/nf"
|
||||||
"github.com/loveuer/nf/nft/log"
|
"github.com/loveuer/nf/nft/log"
|
||||||
|
"github.com/loveuer/nf/nft/resp"
|
||||||
"github.com/loveuer/ushare/internal/controller"
|
"github.com/loveuer/ushare/internal/controller"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LocalRegistry() nf.HandlerFunc {
|
func LocalRegister() nf.HandlerFunc {
|
||||||
|
return func(c *nf.Ctx) error {
|
||||||
|
ip := c.IP(true)
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
|
||||||
|
client := controller.RoomController.Register(ip, ua)
|
||||||
|
|
||||||
|
return resp.Resp200(c, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LocalClients() nf.HandlerFunc {
|
||||||
|
return func(c *nf.Ctx) error {
|
||||||
|
room := c.Query("room")
|
||||||
|
if room == "" {
|
||||||
|
return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": "room can't be empty"})
|
||||||
|
}
|
||||||
|
|
||||||
|
list := controller.RoomController.List(room)
|
||||||
|
|
||||||
|
return resp.Resp200(c, list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LocalWS() nf.HandlerFunc {
|
||||||
upgrader := websocket.Upgrader{
|
upgrader := websocket.Upgrader{
|
||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 1024,
|
WriteBufferSize: 1024,
|
||||||
@ -19,16 +44,19 @@ func LocalRegistry() nf.HandlerFunc {
|
|||||||
|
|
||||||
return func(c *nf.Ctx) error {
|
return func(c *nf.Ctx) error {
|
||||||
|
|
||||||
ip := c.IP(true)
|
id := c.Query("id")
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
|
if id == "" {
|
||||||
|
return c.Status(http.StatusBadRequest).JSON(map[string]string{"error": "id is empty"})
|
||||||
|
}
|
||||||
|
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("LocalRegistry: failed to upgrade websocket connection, err = %s", err.Error())
|
log.Error("LocalWS: failed to upgrade websocket connection, err = %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.RoomController.Register(conn, ip, ua)
|
controller.RoomController.Enter(conn, id)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user