Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf0fdfa4d0 | ||
|
|
9a68952fac | ||
|
|
b661afed4c | ||
|
|
33ff81a15d | ||
|
|
c6d9d4f093 | ||
|
|
d862467883 | ||
|
|
83822ae165 | ||
|
|
e5708e10fb |
238
Client/main.lua
238
Client/main.lua
@@ -4,6 +4,15 @@ local httpServer = args[1] or "http://192.168.2.200:8080"
|
|||||||
local roomId = args[2]
|
local roomId = args[2]
|
||||||
local pollInterval = 1
|
local pollInterval = 1
|
||||||
local computerID = tostring(os.computerID() or "unknown")
|
local computerID = tostring(os.computerID() or "unknown")
|
||||||
|
-- ========== 加载 JSON ==========
|
||||||
|
local JsonUrl = "https://git.liulikeji.cn/GitHub/json.lua/raw/branch/master/json.lua"
|
||||||
|
local JsonResp = http.get(JsonUrl)
|
||||||
|
if not JsonResp then
|
||||||
|
error("无法下载 Json 框架")
|
||||||
|
end
|
||||||
|
local json = load(JsonResp.readAll())()
|
||||||
|
JsonResp.close()
|
||||||
|
|
||||||
|
|
||||||
-- ========== 加载 Basalt ==========
|
-- ========== 加载 Basalt ==========
|
||||||
local basaltUrl = "https://git.liulikeji.cn/GitHub/Basalt/releases/download/v1.7/basalt.lua"
|
local basaltUrl = "https://git.liulikeji.cn/GitHub/Basalt/releases/download/v1.7/basalt.lua"
|
||||||
@@ -21,129 +30,94 @@ local function log(msg)
|
|||||||
--basalt.debug("[FileClient] " .. tostring(msg))
|
--basalt.debug("[FileClient] " .. tostring(msg))
|
||||||
end
|
end
|
||||||
|
|
||||||
function table_to_json(t, indent)
|
|
||||||
indent = indent or 0
|
|
||||||
local spaces = string.rep(" ", indent)
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
if type(t) ~= "table" then
|
local function isBinaryFile(path)
|
||||||
if type(t) == "string" then
|
local extension = string.lower(string.match(path, "%.([^%.%s]+)$") or "")
|
||||||
-- 正确转义所有特殊字符
|
local binaryExtensions = {
|
||||||
local escaped = t:gsub("[\\\"\b\f\n\r\t]", function(c)
|
["dfpwm"] = true,
|
||||||
local replacements = {
|
}
|
||||||
['\\'] = '\\\\',
|
|
||||||
['"'] = '\\"',
|
if binaryExtensions[extension] then
|
||||||
['\b'] = '\\b',
|
return true
|
||||||
['\f'] = '\\f',
|
|
||||||
['\n'] = '\\n',
|
|
||||||
['\r'] = '\\r',
|
|
||||||
['\t'] = '\\t'
|
|
||||||
}
|
|
||||||
return replacements[c]
|
|
||||||
end)
|
|
||||||
return '"' .. escaped .. '"'
|
|
||||||
elseif type(t) == "number" or type(t) == "boolean" then
|
|
||||||
return tostring(t)
|
|
||||||
else
|
|
||||||
return '"' .. tostring(t) .. '"'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 检查是否是数组
|
-- 对于没有扩展名的文件,检查内容
|
||||||
local is_array = true
|
local absPath = path
|
||||||
local max_index = 0
|
if not fs.exists(absPath) then
|
||||||
local count = 0
|
return false
|
||||||
for k, v in pairs(t) do
|
|
||||||
count = count + 1
|
|
||||||
if type(k) ~= "number" or k <= 0 or math.floor(k) ~= k then
|
|
||||||
is_array = false
|
|
||||||
end
|
|
||||||
if type(k) == "number" and k > max_index then
|
|
||||||
max_index = k
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 空表当作对象处理
|
local ok, handle = pcall(fs.open, absPath, "rb")
|
||||||
if count == 0 then
|
if not ok or not handle then
|
||||||
is_array = false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_array then
|
local data = handle.read(math.min(1024, fs.getSize(absPath)))
|
||||||
-- 处理数组
|
handle.close()
|
||||||
table.insert(result, "[")
|
|
||||||
local items = {}
|
if not data then
|
||||||
for i = 1, max_index do
|
return false
|
||||||
if t[i] ~= nil then
|
end
|
||||||
table.insert(items, table_to_json(t[i], indent + 2))
|
|
||||||
else
|
-- 检查是否存在控制字符(除常见的空白字符外)
|
||||||
table.insert(items, "null")
|
for i = 1, #data do
|
||||||
|
local b = data:byte(i)
|
||||||
|
-- 控制字符范围是 0-8, 11-12, 14-31, 127
|
||||||
|
if (b >= 0 and b <= 8) or (b == 11) or (b == 12) or (b >= 14 and b <= 31) or (b == 127) then
|
||||||
|
-- 如果控制字符过多(超过5%),则认为是二进制文件
|
||||||
|
local controlCount = 0
|
||||||
|
for j = 1, #data do
|
||||||
|
local byte = data:byte(j)
|
||||||
|
if (byte >= 0 and byte <= 8) or (byte == 11) or (byte == 12) or (byte >= 14 and byte <= 31) or (byte == 127) then
|
||||||
|
controlCount = controlCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if controlCount / #data > 0.05 then
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.insert(result, table.concat(items, ", "))
|
|
||||||
table.insert(result, "]")
|
|
||||||
else
|
|
||||||
-- 处理对象
|
|
||||||
table.insert(result, "{")
|
|
||||||
local items = {}
|
|
||||||
for k, v in pairs(t) do
|
|
||||||
local key = '"' .. tostring(k) .. '"'
|
|
||||||
local value = table_to_json(v, indent + 2)
|
|
||||||
if indent > 0 then
|
|
||||||
table.insert(items, spaces .. " " .. key .. ": " .. value)
|
|
||||||
else
|
|
||||||
table.insert(items, key .. ":" .. value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if indent > 0 then
|
|
||||||
table.insert(result, table.concat(items, ",\n"))
|
|
||||||
table.insert(result, "\n" .. spaces .. "}")
|
|
||||||
else
|
|
||||||
table.insert(result, table.concat(items, ","))
|
|
||||||
table.insert(result, "}")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return table.concat(result, indent > 0 and "\n" .. spaces or "")
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function cleanPath(path)
|
local function cleanPath(path)
|
||||||
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
|
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function httpPost(path, data)
|
local function httpPost(path, data)
|
||||||
local jsonData = table_to_json(data)
|
local jsonData = json.encode(data)
|
||||||
local url = httpServer .. path
|
local url = httpServer .. path
|
||||||
|
|
||||||
local response = http.post(url, jsonData, {
|
-- 使用长轮询
|
||||||
["Content-Type"] = "application/json"
|
local response,err = http.post({
|
||||||
|
url = url,
|
||||||
|
body = jsonData,
|
||||||
|
method = "POST",
|
||||||
|
headers = {
|
||||||
|
["Content-Type"] = "application/json"
|
||||||
|
},
|
||||||
|
timeout = 60
|
||||||
})
|
})
|
||||||
|
|
||||||
if not response then
|
if not response then
|
||||||
return nil, "无法连接到服务器"
|
return nil, "0 "..err
|
||||||
end
|
end
|
||||||
|
|
||||||
local responseBody = response.readAll()
|
local responseBody = response.readAll()
|
||||||
response.close()
|
response.close()
|
||||||
|
|
||||||
local ok, result = pcall(textutils.unserialiseJSON, responseBody)
|
local ok, result = pcall(json.decode, responseBody)
|
||||||
if ok then
|
if ok then
|
||||||
return result
|
return result
|
||||||
else
|
else
|
||||||
return nil, "无效的JSON响应"
|
return nil, "1无效的JSON响应: " .. responseBody
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ========== 文件系统操作 ==========
|
-- ========== 文件系统操作 ==========
|
||||||
local function isLikelyText(data)
|
|
||||||
for i = 1, math.min(#data, 1024) do
|
|
||||||
local b = data:byte(i)
|
|
||||||
if b < 32 and b ~= 9 and b ~= 10 and b ~= 13 then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getFiles(currentPath, result, prefix)
|
local function getFiles(currentPath, result, prefix)
|
||||||
local computerPrefix = "computer_" .. computerID
|
local computerPrefix = "computer_" .. computerID
|
||||||
local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath
|
local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath
|
||||||
@@ -158,15 +132,16 @@ local function getFiles(currentPath, result, prefix)
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
local content = "[binary]"
|
local content = "[binary]"
|
||||||
local ok, handle = pcall(fs.open, absPath, "rb")
|
if not isBinaryFile(absPath) then
|
||||||
if ok and handle then
|
local ok, handle = pcall(fs.open, absPath, "r")
|
||||||
local data = handle.readAll()
|
if ok and handle then
|
||||||
handle.close()
|
local data = handle.readAll()
|
||||||
if data and isLikelyText(data) then
|
handle.close()
|
||||||
content = data
|
content = data or ""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
result[fullPrefix] = { isFile = true, content = content }
|
|
||||||
|
result[fullPrefix] = { isFile = true, content = content, isBinary = isBinaryFile(absPath) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -292,10 +267,13 @@ end
|
|||||||
|
|
||||||
local function sendResponse(response)
|
local function sendResponse(response)
|
||||||
if response and roomId then
|
if response and roomId then
|
||||||
httpPost("/api/client/send", {
|
local result, err = httpPost("/api/client/send", {
|
||||||
room_id = roomId,
|
room_id = roomId,
|
||||||
message = response
|
message = response
|
||||||
})
|
})
|
||||||
|
if not result then
|
||||||
|
log("3 发送响应失败: " .. tostring(err))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -310,47 +288,51 @@ local function pollMessages()
|
|||||||
room_id = roomId
|
room_id = roomId
|
||||||
})
|
})
|
||||||
|
|
||||||
if result and result.success and result.message then
|
if result and result.success then
|
||||||
local msg = result.message
|
local msg = result.message
|
||||||
local msgType = msg.type
|
log(msg)
|
||||||
|
if msg then
|
||||||
|
local msgType = msg.type
|
||||||
|
|
||||||
if msgType == "file_operation" or msgType == "file_operation_request" then
|
if msgType == "file_operation" or msgType == "file_operation_request" then
|
||||||
local op = msg.operation_type or msg.type
|
local op = msg.operation_type or msg.type
|
||||||
local data = msg.data or {}
|
local data = msg.data or {}
|
||||||
local reqId = msg.requestId or msg.request_id
|
local reqId = msg.requestId or msg.request_id
|
||||||
local sender = msg.sender_id
|
local sender = msg.sender_id
|
||||||
|
|
||||||
local response
|
local response
|
||||||
|
|
||||||
if op == "fetch_files" then
|
if op == "fetch_files" then
|
||||||
response = handleFetchFiles(reqId, sender)
|
response = handleFetchFiles(reqId, sender)
|
||||||
elseif op == "create_or_save_file" then
|
elseif op == "create_or_save_file" then
|
||||||
response = handleSaveFile(data, reqId, sender)
|
response = handleSaveFile(data, reqId, sender)
|
||||||
elseif op == "new_file" then
|
elseif op == "new_file" then
|
||||||
response = handleCreateFile(data, reqId, sender)
|
response = handleCreateFile(data, reqId, sender)
|
||||||
elseif op == "new_folder" then
|
elseif op == "new_folder" then
|
||||||
response = handleCreateFolder(data, reqId, sender)
|
response = handleCreateFolder(data, reqId, sender)
|
||||||
elseif op == "rename" then
|
elseif op == "rename" then
|
||||||
response = handleRename(data, reqId, sender)
|
response = handleRename(data, reqId, sender)
|
||||||
elseif op == "delete_file" then
|
elseif op == "delete_file" then
|
||||||
response = handleDelete(data, reqId, sender)
|
response = handleDelete(data, reqId, sender)
|
||||||
else
|
else
|
||||||
response = {
|
response = {
|
||||||
type = "file_operation_response",
|
type = "file_operation_response",
|
||||||
requestId = reqId,
|
requestId = reqId,
|
||||||
success = false,
|
success = false,
|
||||||
error = "Unknown operation: " .. tostring(op),
|
error = "Unknown operation: " .. tostring(op),
|
||||||
target_client_id = sender
|
target_client_id = sender
|
||||||
}
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
sendResponse(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
sendResponse(response)
|
|
||||||
end
|
end
|
||||||
elseif err then
|
elseif err then
|
||||||
log("轮询错误: " .. tostring(err))
|
log("2 轮询错误: " .. tostring(err))
|
||||||
|
-- 如果是连接错误,稍后再试
|
||||||
|
sleep(5)
|
||||||
end
|
end
|
||||||
|
|
||||||
sleep(pollInterval)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import tomllib
|
|
||||||
|
|
||||||
# 您的 TOML 字符串
|
|
||||||
toml_str = '''{ a = "你好", "a-c-v" = "你好", b = "世界", }'''
|
|
||||||
|
|
||||||
# 反序列化
|
|
||||||
data = tomllib.loads(toml_str)
|
|
||||||
print(data)
|
|
||||||
# 输出: {'a': '你好', 'a-c-v': '你好', 'b': '世界'}
|
|
||||||
|
|
||||||
# 访问数据
|
|
||||||
print(data['a']) # 输出: 你好
|
|
||||||
print(data['a-c-v']) # 输出: 你好
|
|
||||||
@@ -2,7 +2,7 @@ import type { Files } from 'monaco-tree-editor'
|
|||||||
|
|
||||||
let roomId: string | null = null
|
let roomId: string | null = null
|
||||||
let serverUrl: string | null = null
|
let serverUrl: string | null = null
|
||||||
let pollIntervalMs = 1000
|
let pollIntervalMs = 100
|
||||||
let isPolling = false
|
let isPolling = false
|
||||||
let pollingTimeout: number | null = null
|
let pollingTimeout: number | null = null
|
||||||
|
|
||||||
@@ -32,29 +32,6 @@ function getParamsFromUrl(): { roomId: string | null } {
|
|||||||
return { roomId }
|
return { roomId }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function httpPost(path: string, data: any): Promise<any> {
|
|
||||||
const url = `${serverUrl}${path}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('HTTP请求失败:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initWebSocketConnection(): Promise<void> {
|
export async function initWebSocketConnection(): Promise<void> {
|
||||||
const params = getParamsFromUrl()
|
const params = getParamsFromUrl()
|
||||||
roomId = params.roomId
|
roomId = params.roomId
|
||||||
@@ -67,6 +44,17 @@ export async function initWebSocketConnection(): Promise<void> {
|
|||||||
console.log('HTTP连接已初始化,服务器:', serverUrl)
|
console.log('HTTP连接已初始化,服务器:', serverUrl)
|
||||||
console.log('房间ID:', roomId)
|
console.log('房间ID:', roomId)
|
||||||
|
|
||||||
|
// 处理挂起的初始请求
|
||||||
|
for (const request of pendingInitialRequests) {
|
||||||
|
try {
|
||||||
|
const result = await sendFileOperationInternal(request.operation, request.data)
|
||||||
|
request.resolve(result)
|
||||||
|
} catch (error) {
|
||||||
|
request.reject(error as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingInitialRequests.length = 0
|
||||||
|
|
||||||
startPolling()
|
startPolling()
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
@@ -86,18 +74,33 @@ function stopPolling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pollForResponses() {
|
async function pollForResponses() {
|
||||||
if (!isPolling || !roomId) return
|
if (!isPolling || !roomId || !serverUrl) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await httpPost('/api/frontend/receive', {
|
const response = await fetch(`${serverUrl}/api/frontend/receive`, {
|
||||||
room_id: roomId,
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
room_id: roomId,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.success && response.message) {
|
if (response.ok) {
|
||||||
handleMessage(response.message)
|
const data = await response.json()
|
||||||
|
if (data.success && data.message) {
|
||||||
|
handleMessage(data.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('轮询请求失败:', response.status)
|
||||||
|
// 短暂的延迟后重试
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('轮询消息失败:', error)
|
console.error('轮询消息失败:', error)
|
||||||
|
// 网络错误,稍后重试
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPolling) {
|
if (isPolling) {
|
||||||
@@ -130,8 +133,13 @@ function handleFileOperationResponse(data: any): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: number = 30000): Promise<any> {
|
async function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: number = 30000): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!serverUrl || !roomId) {
|
||||||
|
reject(new Error('未初始化连接'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
const timeout = window.setTimeout(() => {
|
const timeout = window.setTimeout(() => {
|
||||||
@@ -143,21 +151,28 @@ function sendFileOperationInternal(operationType: string, data?: any, timeoutMs:
|
|||||||
|
|
||||||
pendingRequests.set(requestId, { resolve, reject, timeout })
|
pendingRequests.set(requestId, { resolve, reject, timeout })
|
||||||
|
|
||||||
httpPost('/api/frontend/send', {
|
fetch(`${serverUrl}/api/frontend/send`, {
|
||||||
room_id: roomId,
|
method: 'POST',
|
||||||
message: {
|
headers: {
|
||||||
type: 'file_operation',
|
'Content-Type': 'application/json',
|
||||||
requestId: requestId,
|
|
||||||
operation_type: operationType,
|
|
||||||
data: data,
|
|
||||||
room_id: roomId,
|
|
||||||
},
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
room_id: roomId,
|
||||||
|
message: {
|
||||||
|
type: 'file_operation',
|
||||||
|
requestId: requestId,
|
||||||
|
operation_type: operationType,
|
||||||
|
data: data,
|
||||||
|
room_id: roomId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then(async (response) => {
|
||||||
if (!response.success) {
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
pendingRequests.delete(requestId)
|
pendingRequests.delete(requestId)
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
reject(new Error(response.message || '发送请求失败'))
|
reject(new Error(errorText || `发送请求失败: ${response.status}`))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
552
PyServer/main.py
552
PyServer/main.py
@@ -3,9 +3,11 @@ import logging
|
|||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp import web
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import re
|
import re
|
||||||
@@ -20,6 +22,8 @@ rooms = {}
|
|||||||
frontend_to_client_queues = {}
|
frontend_to_client_queues = {}
|
||||||
# 客户端到前端的消息队列
|
# 客户端到前端的消息队列
|
||||||
client_to_frontend_queues = {}
|
client_to_frontend_queues = {}
|
||||||
|
# 挂起的请求管理
|
||||||
|
pending_requests = {}
|
||||||
|
|
||||||
# 静态文件目录
|
# 静态文件目录
|
||||||
STATIC_DIR = os.path.join(os.path.dirname(__file__), 'static')
|
STATIC_DIR = os.path.join(os.path.dirname(__file__), 'static')
|
||||||
@@ -44,6 +48,18 @@ class Room:
|
|||||||
'created_at': self.created_at.isoformat()
|
'created_at': self.created_at.isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def safe_decode_json(raw_body: bytes) -> Dict[Any, Any]:
|
||||||
|
"""
|
||||||
|
尝试用 UTF-8 解码,失败则尝试 GB18030(兼容 GBK/GB2312)
|
||||||
|
"""
|
||||||
|
for encoding in ['utf-8', 'gb18030', 'latin1']:
|
||||||
|
try:
|
||||||
|
text = raw_body.decode(encoding)
|
||||||
|
return json.loads(text)
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
raise ValueError("无法解码请求体为有效 JSON")
|
||||||
|
|
||||||
def get_frontend_to_client_queue(room_id: str) -> List[Dict[str, Any]]:
|
def get_frontend_to_client_queue(room_id: str) -> List[Dict[str, Any]]:
|
||||||
if room_id not in frontend_to_client_queues:
|
if room_id not in frontend_to_client_queues:
|
||||||
frontend_to_client_queues[room_id] = []
|
frontend_to_client_queues[room_id] = []
|
||||||
@@ -54,160 +70,15 @@ def get_client_to_frontend_queue(room_id: str) -> List[Dict[str, Any]]:
|
|||||||
client_to_frontend_queues[room_id] = []
|
client_to_frontend_queues[room_id] = []
|
||||||
return client_to_frontend_queues[room_id]
|
return client_to_frontend_queues[room_id]
|
||||||
|
|
||||||
class HTTPHandler(BaseHTTPRequestHandler):
|
class HTTPHandler:
|
||||||
def do_GET(self):
|
async def handle_create_room(self, request):
|
||||||
"""处理HTTP GET请求"""
|
|
||||||
try:
|
|
||||||
parsed_path = urlparse(self.path)
|
|
||||||
path = parsed_path.path
|
|
||||||
query_params = parse_qs(parsed_path.query)
|
|
||||||
|
|
||||||
# API路由
|
|
||||||
if path == '/api/room' and self.command == 'POST':
|
|
||||||
self.handle_create_room()
|
|
||||||
elif path.startswith('/api/room/') and self.command == 'GET':
|
|
||||||
room_id = path.split('/')[-1]
|
|
||||||
self.handle_get_room(room_id)
|
|
||||||
elif path == '/api/rooms' and self.command == 'GET':
|
|
||||||
self.handle_list_rooms()
|
|
||||||
# 根路径处理
|
|
||||||
elif path == '/':
|
|
||||||
self.handle_root_path(query_params)
|
|
||||||
# 静态文件服务
|
|
||||||
else:
|
|
||||||
self.handle_static_file(path)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"处理GET请求时发生错误: {e}")
|
|
||||||
self.send_error(500, f"Internal Server Error: {str(e)}")
|
|
||||||
|
|
||||||
def do_POST(self):
|
|
||||||
"""处理HTTP POST请求"""
|
|
||||||
try:
|
|
||||||
content_length = int(self.headers.get('Content-Length', 0))
|
|
||||||
post_data = self.rfile.read(content_length) if content_length > 0 else b'{}'
|
|
||||||
|
|
||||||
parsed_path = urlparse(self.path)
|
|
||||||
path = parsed_path.path
|
|
||||||
|
|
||||||
if path == '/api/room':
|
|
||||||
self.handle_create_room(post_data)
|
|
||||||
elif path == '/api/frontend/send':
|
|
||||||
self.handle_frontend_send_message(post_data)
|
|
||||||
elif path == '/api/frontend/receive':
|
|
||||||
self.handle_frontend_receive_message(post_data)
|
|
||||||
elif path == '/api/client/send':
|
|
||||||
self.handle_client_send_message(post_data)
|
|
||||||
elif path == '/api/client/receive':
|
|
||||||
self.handle_client_receive_message(post_data)
|
|
||||||
else:
|
|
||||||
self.send_error(404, "Not Found")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"处理POST请求时发生错误: {e}")
|
|
||||||
self.send_error(500, f"Internal Server Error: {str(e)}")
|
|
||||||
|
|
||||||
def handle_root_path(self, query_params: Dict[str, Any]):
|
|
||||||
"""处理根路径请求"""
|
|
||||||
room_id = query_params.get('id', [None])[0]
|
|
||||||
|
|
||||||
if room_id:
|
|
||||||
# 有房间ID参数,直接返回前端页面
|
|
||||||
logger.info(f"请求根路径,有房间ID: {room_id}")
|
|
||||||
self.serve_static_file('/index.html')
|
|
||||||
else:
|
|
||||||
# 没有房间ID,创建新房间并重定向
|
|
||||||
try:
|
|
||||||
# 生成唯一房间ID
|
|
||||||
room_id = str(uuid.uuid4())[:8]
|
|
||||||
|
|
||||||
# 获取服务器主机地址并移除端口号
|
|
||||||
host = self.headers.get('Host', 'localhost')
|
|
||||||
host_without_port = re.sub(r':\d+$', '', host)
|
|
||||||
|
|
||||||
# 创建房间
|
|
||||||
room = Room(room_id, host_without_port)
|
|
||||||
rooms[room_id] = room
|
|
||||||
|
|
||||||
logger.info(f"通过根路径创建新房间: {room_id}")
|
|
||||||
|
|
||||||
# 重定向到带房间ID的URL
|
|
||||||
redirect_url = f'/?id={room_id}'
|
|
||||||
self.send_response(302)
|
|
||||||
self.send_header('Location', redirect_url)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"创建房间失败: {e}")
|
|
||||||
self.send_error(500, str(e))
|
|
||||||
|
|
||||||
def handle_static_file(self, path: str):
|
|
||||||
"""处理静态文件请求"""
|
|
||||||
# 安全检查:防止路径遍历攻击
|
|
||||||
if '..' in path:
|
|
||||||
self.send_error(403, "Forbidden: Path traversal not allowed")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 规范化路径
|
|
||||||
if path == '/':
|
|
||||||
path = '/index.html'
|
|
||||||
|
|
||||||
# 移除开头的斜杠
|
|
||||||
file_path = path.lstrip('/')
|
|
||||||
if not file_path:
|
|
||||||
file_path = 'index.html'
|
|
||||||
|
|
||||||
# 构建完整文件路径
|
|
||||||
full_path = os.path.join(STATIC_DIR, file_path)
|
|
||||||
|
|
||||||
# 如果是目录,尝试查找index.html
|
|
||||||
if os.path.isdir(full_path):
|
|
||||||
index_path = os.path.join(full_path, 'index.html')
|
|
||||||
if os.path.exists(index_path):
|
|
||||||
full_path = index_path
|
|
||||||
else:
|
|
||||||
self.send_error(404, "Directory index not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 检查文件是否存在且是普通文件
|
|
||||||
if not os.path.exists(full_path) or not os.path.isfile(full_path):
|
|
||||||
self.send_error(404, f"File not found: {path}")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 读取文件内容
|
|
||||||
with open(full_path, 'rb') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# 获取MIME类型
|
|
||||||
mime_type, encoding = mimetypes.guess_type(full_path)
|
|
||||||
if mime_type is None:
|
|
||||||
mime_type = 'application/octet-stream'
|
|
||||||
|
|
||||||
# 发送响应头
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header('Content-Type', mime_type)
|
|
||||||
self.send_header('Content-Length', str(len(content)))
|
|
||||||
self.send_header('Cache-Control', 'public, max-age=3600')
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
# 发送文件内容
|
|
||||||
self.wfile.write(content)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"读取或发送文件失败: {e}")
|
|
||||||
self.send_error(500, f"Error reading file: {str(e)}")
|
|
||||||
|
|
||||||
def serve_static_file(self, path: str):
|
|
||||||
"""服务静态文件(内部方法)"""
|
|
||||||
self.handle_static_file(path)
|
|
||||||
|
|
||||||
def handle_create_room(self, post_data=None):
|
|
||||||
"""创建新房间"""
|
"""创建新房间"""
|
||||||
try:
|
try:
|
||||||
# 生成唯一房间ID
|
# 生成唯一房间ID
|
||||||
room_id = str(uuid.uuid4())[:8]
|
room_id = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
# 获取服务器主机地址并移除端口号
|
# 获取服务器主机地址并移除端口号
|
||||||
host = self.headers.get('Host', 'localhost')
|
host = request.headers.get('Host', 'localhost')
|
||||||
host_without_port = re.sub(r':\d+$', '', host)
|
host_without_port = re.sub(r':\d+$', '', host)
|
||||||
|
|
||||||
# 创建房间
|
# 创建房间
|
||||||
@@ -222,158 +93,391 @@ class HTTPHandler(BaseHTTPRequestHandler):
|
|||||||
'frontend_url': room.frontend_url
|
'frontend_url': room.frontend_url
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_response(200)
|
return web.json_response(response)
|
||||||
self.send_header('Content-Type', 'application/json')
|
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(json.dumps(response).encode())
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"创建房间失败: {e}")
|
logger.error(f"创建房间失败: {e}")
|
||||||
self.send_error(500, str(e))
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
def handle_frontend_send_message(self, post_data):
|
async def handle_frontend_send_message(self, request):
|
||||||
"""前端发送消息到客户端"""
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
raw_body = await request.read()
|
||||||
|
data = safe_decode_json(raw_body)
|
||||||
room_id = data.get('room_id')
|
room_id = data.get('room_id')
|
||||||
message = data.get('message')
|
message_data = data.get('message')
|
||||||
|
|
||||||
if not room_id or not message:
|
if not room_id or not message_data:
|
||||||
self.send_error(400, "需要room_id和message参数")
|
return web.json_response({'error': '需要room_id和message参数'}, status=400)
|
||||||
return
|
|
||||||
|
|
||||||
queue = get_frontend_to_client_queue(room_id)
|
queue = get_frontend_to_client_queue(room_id)
|
||||||
queue.append(message)
|
queue.append(message_data)
|
||||||
|
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
||||||
|
client_req_id = pending_requests[room_id]['client']
|
||||||
|
if client_req_id in pending_requests:
|
||||||
|
pending_requests[client_req_id]['event'].set()
|
||||||
|
logger.info(f"立即响应挂起的客户端请求: {client_req_id}")
|
||||||
|
|
||||||
response = {
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '消息已发送到客户端队列'
|
'message': '消息已发送到客户端队列'
|
||||||
}
|
})
|
||||||
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header('Content-Type', 'application/json')
|
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(json.dumps(response).encode())
|
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"前端发送消息失败: 无效的JSON或编码错误 - {e}")
|
||||||
|
return web.json_response({'error': '无效的JSON数据或不支持的文本编码'}, status=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"前端发送消息失败: {e}")
|
logger.error(f"前端发送消息失败: {e}")
|
||||||
self.send_error(500, str(e))
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
def handle_frontend_receive_message(self, post_data):
|
async def handle_frontend_receive_message(self, request):
|
||||||
"""前端接收来自客户端的消息"""
|
"""前端接收来自客户端的消息(长轮询)"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
data = await request.json()
|
||||||
room_id = data.get('room_id')
|
room_id = data.get('room_id')
|
||||||
|
|
||||||
if not room_id:
|
if not room_id:
|
||||||
self.send_error(400, "需要room_id参数")
|
return web.json_response({'error': '需要room_id参数'}, status=400)
|
||||||
return
|
|
||||||
|
|
||||||
queue = get_client_to_frontend_queue(room_id)
|
queue = get_client_to_frontend_queue(room_id)
|
||||||
|
|
||||||
|
# 立即检查是否有消息
|
||||||
if queue:
|
if queue:
|
||||||
# 返回队列中的第一个消息
|
|
||||||
message = queue.pop(0)
|
message = queue.pop(0)
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': message
|
'message': message
|
||||||
}
|
}
|
||||||
else:
|
return web.json_response(response)
|
||||||
# 没有消息
|
|
||||||
|
# 没有消息,设置长轮询
|
||||||
|
req_id = str(uuid.uuid4())
|
||||||
|
event = asyncio.Event()
|
||||||
|
|
||||||
|
# 存储挂起的请求
|
||||||
|
pending_requests[req_id] = {
|
||||||
|
'room_id': room_id,
|
||||||
|
'event': event,
|
||||||
|
'type': 'frontend',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
if room_id not in pending_requests:
|
||||||
|
pending_requests[room_id] = {}
|
||||||
|
pending_requests[room_id]['frontend'] = req_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 等待295秒或直到有消息
|
||||||
|
await asyncio.wait_for(event.wait(), timeout=295)
|
||||||
|
|
||||||
|
# 检查队列中是否有消息
|
||||||
|
queue = get_client_to_frontend_queue(room_id)
|
||||||
|
if queue:
|
||||||
|
message = queue.pop(0)
|
||||||
|
response = {
|
||||||
|
'success': True,
|
||||||
|
'message': message
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 超时返回空消息
|
||||||
|
response = {
|
||||||
|
'success': True,
|
||||||
|
'message': None
|
||||||
|
}
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# 超时返回空消息
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': None
|
'message': None
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_response(200)
|
# 清理挂起的请求
|
||||||
self.send_header('Content-Type', 'application/json')
|
if req_id in pending_requests:
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
del pending_requests[req_id]
|
||||||
self.end_headers()
|
if room_id in pending_requests and 'frontend' in pending_requests[room_id]:
|
||||||
self.wfile.write(json.dumps(response).encode())
|
del pending_requests[room_id]['frontend']
|
||||||
|
|
||||||
|
return web.json_response(response)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error("JSON解析失败")
|
||||||
|
return web.json_response({'error': '无效的JSON数据'}, status=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"前端接收消息失败: {e}")
|
logger.error(f"前端接收消息失败: {e}")
|
||||||
self.send_error(500, str(e))
|
# 清理挂起的请求
|
||||||
|
if 'req_id' in locals() and req_id in pending_requests:
|
||||||
|
del pending_requests[req_id]
|
||||||
|
if room_id in pending_requests and 'frontend' in pending_requests[room_id]:
|
||||||
|
del pending_requests[room_id]['frontend']
|
||||||
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
def handle_client_send_message(self, post_data):
|
async def handle_client_send_message(self, request):
|
||||||
"""客户端发送消息到前端"""
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
raw_body = await request.read() # 获取原始字节
|
||||||
|
data = safe_decode_json(raw_body)
|
||||||
room_id = data.get('room_id')
|
room_id = data.get('room_id')
|
||||||
message = data.get('message')
|
message_data = data.get('message')
|
||||||
|
|
||||||
if not room_id or not message:
|
if not room_id or not message_data:
|
||||||
self.send_error(400, "需要room_id和message参数")
|
return web.json_response({'error': '需要room_id和message参数'}, status=400)
|
||||||
return
|
|
||||||
|
|
||||||
queue = get_client_to_frontend_queue(room_id)
|
queue = get_client_to_frontend_queue(room_id)
|
||||||
queue.append(message)
|
queue.append(message_data)
|
||||||
|
|
||||||
response = {
|
if room_id in pending_requests and 'frontend' in pending_requests[room_id]:
|
||||||
|
frontend_req_id = pending_requests[room_id]['frontend']
|
||||||
|
if frontend_req_id in pending_requests:
|
||||||
|
pending_requests[frontend_req_id]['event'].set()
|
||||||
|
logger.info(f"立即响应挂起的前端请求: {frontend_req_id}")
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '消息已发送到前端队列'
|
'message': '消息已发送到前端队列'
|
||||||
}
|
})
|
||||||
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header('Content-Type', 'application/json')
|
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(json.dumps(response).encode())
|
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"客户端发送消息失败: 无效的JSON或编码错误 - {e}")
|
||||||
|
return web.json_response({'error': '无效的JSON数据或不支持的文本编码'}, status=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"客户端发送消息失败: {e}")
|
logger.error(f"客户端发送消息失败: {e}")
|
||||||
self.send_error(500, str(e))
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
def handle_client_receive_message(self, post_data):
|
async def handle_client_receive_message(self, request):
|
||||||
"""客户端接收来自前端的消息"""
|
"""客户端接收来自前端的消息(长轮询)"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
raw_body = await request.read()
|
||||||
|
data = safe_decode_json(raw_body) # 使用你之前添加的 safe_decode_json
|
||||||
room_id = data.get('room_id')
|
room_id = data.get('room_id')
|
||||||
|
|
||||||
if not room_id:
|
if not room_id:
|
||||||
self.send_error(400, "需要room_id参数")
|
return web.json_response({'error': '需要room_id参数'}, status=400)
|
||||||
return
|
|
||||||
|
|
||||||
queue = get_frontend_to_client_queue(room_id)
|
queue = get_frontend_to_client_queue(room_id)
|
||||||
|
|
||||||
|
# 立即检查是否有消息
|
||||||
if queue:
|
if queue:
|
||||||
# 返回队列中的第一个消息
|
|
||||||
message = queue.pop(0)
|
message = queue.pop(0)
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': message
|
'message': message
|
||||||
}
|
}
|
||||||
else:
|
return web.json_response(response)
|
||||||
# 没有消息
|
|
||||||
|
# 没有消息,设置长轮询(最多等待 58 秒)
|
||||||
|
req_id = str(uuid.uuid4())
|
||||||
|
event = asyncio.Event()
|
||||||
|
|
||||||
|
# 存储挂起的请求
|
||||||
|
pending_requests[req_id] = {
|
||||||
|
'room_id': room_id,
|
||||||
|
'event': event,
|
||||||
|
'type': 'client',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
if room_id not in pending_requests:
|
||||||
|
pending_requests[room_id] = {}
|
||||||
|
pending_requests[room_id]['client'] = req_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ⏱️ 只等待 58 秒(略小于客户端或代理的 60 秒超时)
|
||||||
|
await asyncio.wait_for(event.wait(), timeout=58)
|
||||||
|
|
||||||
|
# 被唤醒后,检查队列
|
||||||
|
queue = get_frontend_to_client_queue(room_id)
|
||||||
|
if queue:
|
||||||
|
message = queue.pop(0)
|
||||||
|
response = {
|
||||||
|
'success': True,
|
||||||
|
'message': message
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
response = {
|
||||||
|
'success': True,
|
||||||
|
'message': None
|
||||||
|
}
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# 58秒超时,返回空消息
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': None
|
'message': None
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_response(200)
|
# 清理挂起的请求
|
||||||
self.send_header('Content-Type', 'application/json')
|
pending_requests.pop(req_id, None)
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
if room_id in pending_requests:
|
||||||
self.end_headers()
|
pending_requests[room_id].pop('client', None)
|
||||||
self.wfile.write(json.dumps(response).encode())
|
# 如果 room_id 下已无其他引用,也可以清理整个 room 条目(可选)
|
||||||
|
|
||||||
|
return web.json_response(response)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"客户端接收消息失败: 无效JSON或编码 - {e}")
|
||||||
|
return web.json_response({'error': '无效的JSON数据'}, status=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"客户端接收消息失败: {e}")
|
logger.error(f"客户端接收消息失败: {e}")
|
||||||
self.send_error(500, str(e))
|
# 清理挂起的请求
|
||||||
|
if 'req_id' in locals():
|
||||||
|
pending_requests.pop(req_id, None)
|
||||||
|
if 'room_id' in locals() and room_id in pending_requests:
|
||||||
|
pending_requests[room_id].pop('client', None)
|
||||||
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
async def handle_static_file(self, request):
|
||||||
"""重写日志方法"""
|
"""处理静态文件请求"""
|
||||||
logger.info("%s - - [%s] %s" % (self.client_address[0],
|
path = request.match_info.get('path', '')
|
||||||
self.log_date_time_string(),
|
|
||||||
format % args))
|
# 安全检查:防止路径遍历攻击
|
||||||
|
if '..' in path:
|
||||||
|
return web.Response(text="Forbidden: Path traversal not allowed", status=403)
|
||||||
|
|
||||||
|
# 规范化路径
|
||||||
|
if path == '' or path == '/':
|
||||||
|
path = 'index.html'
|
||||||
|
|
||||||
|
# 构建完整文件路径
|
||||||
|
full_path = os.path.join(STATIC_DIR, path)
|
||||||
|
|
||||||
|
# 如果是目录,尝试查找index.html
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
index_path = os.path.join(full_path, 'index.html')
|
||||||
|
if os.path.exists(index_path):
|
||||||
|
full_path = index_path
|
||||||
|
else:
|
||||||
|
return web.Response(text="Directory index not found", status=404)
|
||||||
|
|
||||||
|
# 检查文件是否存在且是普通文件
|
||||||
|
if not os.path.exists(full_path) or not os.path.isfile(full_path):
|
||||||
|
return web.Response(text=f"File not found: {path}", status=404)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 读取文件内容
|
||||||
|
with open(full_path, 'rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 获取MIME类型
|
||||||
|
mime_type, encoding = mimetypes.guess_type(full_path)
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type = 'application/octet-stream'
|
||||||
|
|
||||||
|
# 发送响应
|
||||||
|
return web.Response(
|
||||||
|
body=content,
|
||||||
|
content_type=mime_type,
|
||||||
|
headers={
|
||||||
|
'Cache-Control': 'public, max-age=3600'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"读取或发送文件失败: {e}")
|
||||||
|
return web.Response(text=f"Error reading file: {str(e)}", status=500)
|
||||||
|
|
||||||
|
async def handle_root_path(self, request):
|
||||||
|
"""处理根路径请求"""
|
||||||
|
query_params = dict(request.query)
|
||||||
|
room_id = query_params.get('id')
|
||||||
|
|
||||||
|
if room_id:
|
||||||
|
# 有房间ID参数,直接返回前端页面
|
||||||
|
logger.info(f"请求根路径,有房间ID: {room_id}")
|
||||||
|
return await self.handle_static_file(request)
|
||||||
|
else:
|
||||||
|
# 没有房间ID,创建新房间并重定向
|
||||||
|
try:
|
||||||
|
# 生成唯一房间ID
|
||||||
|
room_id = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
# 获取服务器主机地址并移除端口号
|
||||||
|
host = request.headers.get('Host', 'localhost')
|
||||||
|
host_without_port = re.sub(r':\d+$', '', host)
|
||||||
|
|
||||||
|
# 创建房间
|
||||||
|
room = Room(room_id, host_without_port)
|
||||||
|
rooms[room_id] = room
|
||||||
|
|
||||||
|
logger.info(f"通过根路径创建新房间: {room_id}")
|
||||||
|
|
||||||
|
# 重定向到带房间ID的URL
|
||||||
|
redirect_url = f'/?id={room_id}'
|
||||||
|
return web.HTTPFound(redirect_url)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建房间失败: {e}")
|
||||||
|
return web.Response(text=str(e), status=500)
|
||||||
|
|
||||||
|
async def create_app():
|
||||||
|
"""创建Web应用"""
|
||||||
|
handler = HTTPHandler()
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
|
||||||
|
# API路由
|
||||||
|
app.router.add_post('/api/room', handler.handle_create_room)
|
||||||
|
app.router.add_post('/api/frontend/send', handler.handle_frontend_send_message)
|
||||||
|
app.router.add_post('/api/frontend/receive', handler.handle_frontend_receive_message)
|
||||||
|
app.router.add_post('/api/client/send', handler.handle_client_send_message)
|
||||||
|
app.router.add_post('/api/client/receive', handler.handle_client_receive_message)
|
||||||
|
|
||||||
|
# 静态文件和根路径路由
|
||||||
|
app.router.add_get('/', handler.handle_root_path)
|
||||||
|
app.router.add_get('/{path:.*}', handler.handle_static_file)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
async def cleanup_pending_requests():
|
||||||
|
"""定期清理过期的挂起请求"""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60) # 每分钟清理一次
|
||||||
|
current_time = time.time()
|
||||||
|
expired_reqs = []
|
||||||
|
|
||||||
|
for req_id, req_info in pending_requests.items():
|
||||||
|
if current_time - req_info['timestamp'] > 300: # 超过5分钟
|
||||||
|
expired_reqs.append(req_id)
|
||||||
|
|
||||||
|
for req_id in expired_reqs:
|
||||||
|
if req_id in pending_requests:
|
||||||
|
# 尝试触发事件以释放挂起的请求
|
||||||
|
try:
|
||||||
|
pending_requests[req_id]['event'].set()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
del pending_requests[req_id]
|
||||||
|
|
||||||
|
if expired_reqs:
|
||||||
|
logger.info(f"清理了 {len(expired_reqs)} 个过期的挂起请求")
|
||||||
|
|
||||||
|
async def start_background_tasks(app):
|
||||||
|
"""启动后台任务"""
|
||||||
|
app['cleanup_task'] = asyncio.create_task(cleanup_pending_requests())
|
||||||
|
|
||||||
|
async def cleanup_background_tasks(app):
|
||||||
|
"""清理后台任务"""
|
||||||
|
if 'cleanup_task' in app:
|
||||||
|
app['cleanup_task'].cancel()
|
||||||
|
try:
|
||||||
|
await app['cleanup_task']
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
def run_http_server():
|
def run_http_server():
|
||||||
"""运行HTTP服务器"""
|
"""运行HTTP服务器"""
|
||||||
try:
|
try:
|
||||||
server = HTTPServer(('0.0.0.0', 80), HTTPHandler)
|
# 创建应用
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
app = loop.run_until_complete(create_app())
|
||||||
|
|
||||||
|
# 注册启动和清理钩子
|
||||||
|
app.on_startup.append(start_background_tasks)
|
||||||
|
app.on_cleanup.append(cleanup_background_tasks)
|
||||||
|
|
||||||
|
# 启动服务器
|
||||||
logger.info("HTTP服务器启动在端口 80")
|
logger.info("HTTP服务器启动在端口 80")
|
||||||
server.serve_forever()
|
web.run_app(app, host='0.0.0.0', port=80)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"HTTP服务器启动失败: {e}")
|
logger.error(f"HTTP服务器启动失败: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
websockets
|
|
||||||
aiohttp
|
aiohttp
|
||||||
requests
|
|
||||||
python-dateutil
|
|
||||||
37
README.md
37
README.md
@@ -12,7 +12,7 @@ Demo: http://cc-web-edit.liulikeji.cn
|
|||||||
|
|
||||||
- **远程文件管理**:实时浏览、编辑和管理 CC:Tweaked 计算机中的文件
|
- **远程文件管理**:实时浏览、编辑和管理 CC:Tweaked 计算机中的文件
|
||||||
- **Monaco 编辑器**:基于 VS Code 的 Monaco 编辑器,提供专业的代码编辑体验
|
- **Monaco 编辑器**:基于 VS Code 的 Monaco 编辑器,提供专业的代码编辑体验
|
||||||
- **HTTP 通信**:基于 HTTP 协议的可靠通信
|
- **低延迟通信**:基于 HTTP 长轮询 + 请求挂起机制,模拟 WebSocket 的低延迟传输
|
||||||
|
|
||||||
### 文件操作
|
### 文件操作
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ Demo: http://cc-web-edit.liulikeji.cn
|
|||||||
- **自动命令生成**:根据 URL 参数自动生成连接命令
|
- **自动命令生成**:根据 URL 参数自动生成连接命令
|
||||||
- **一键复制**:点击即可复制连接命令到剪贴板
|
- **一键复制**:点击即可复制连接命令到剪贴板
|
||||||
- **房间管理**:支持创建和加入房间
|
- **房间管理**:支持创建和加入房间
|
||||||
- **轮询机制**:HTTP 轮询确保连接稳定性
|
- **实时通信**:高效的请求挂起机制确保接近实时的响应
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ cp -r dist/* ../PyServer/static/
|
|||||||
- **二进制文件**:非文本文件会显示为 `[binary]`,无法在线编辑
|
- **二进制文件**:非文本文件会显示为 `[binary]`,无法在线编辑
|
||||||
- **单客户端**:目前主要支持一个网页端和一个 CC 客户端的配对使用
|
- **单客户端**:目前主要支持一个网页端和一个 CC 客户端的配对使用
|
||||||
- **文件大小**:上传文件限制为 1MB
|
- **文件大小**:上传文件限制为 1MB
|
||||||
- **轮询延迟**:HTTP 轮询机制可能有轻微延迟(默认 2 秒)
|
- **低延迟**:HTTP 长轮询机制提供接近实时的响应体验
|
||||||
|
|
||||||
### 计划功能
|
### 计划功能
|
||||||
|
|
||||||
@@ -140,9 +140,17 @@ cp -r dist/* ../PyServer/static/
|
|||||||
|
|
||||||
- `POST /api/room` - 创建房间
|
- `POST /api/room` - 创建房间
|
||||||
- `POST /api/frontend/send` - 前端发送消息到客户端
|
- `POST /api/frontend/send` - 前端发送消息到客户端
|
||||||
- `POST /api/frontend/receive` - 前端接收来自客户端的消息
|
- `POST /api/frontend/receive` - 前端接收来自客户端的消息(长轮询)
|
||||||
- `POST /api/client/send` - 客户端发送消息到前端
|
- `POST /api/client/send` - 客户端发送消息到前端
|
||||||
- `POST /api/client/receive` - 客户端接收来自前端的消息
|
- `POST /api/client/receive` - 客户端接收来自前端的消息(长轮询)
|
||||||
|
|
||||||
|
### 通信机制
|
||||||
|
|
||||||
|
**长轮询 + 请求挂起**:
|
||||||
|
|
||||||
|
- 客户端请求挂起最长达 295 秒,直到有消息到达
|
||||||
|
- 当有新消息时立即响应,实现低延迟传输
|
||||||
|
- 服务器端控制请求超时,避免不必要的轮询
|
||||||
|
|
||||||
### 消息类型
|
### 消息类型
|
||||||
|
|
||||||
@@ -186,27 +194,24 @@ A: 确保 CC 客户端已成功连接,然后刷新文件列表
|
|||||||
**Q: 文件上传失败**
|
**Q: 文件上传失败**
|
||||||
A: 检查文件大小是否超过 1MB 限制
|
A: 检查文件大小是否超过 1MB 限制
|
||||||
|
|
||||||
**Q: 操作响应较慢**
|
**Q: 消息传输延迟**
|
||||||
A: 默认轮询间隔为 1 秒,可通过调整代码中的轮询间隔改善
|
A: 服务器使用长轮询机制,响应通常在毫秒级别
|
||||||
|
|
||||||
## 📄 技术说明
|
## 📄 技术说明
|
||||||
|
|
||||||
- **后端**:Python + HTTP Server
|
- **后端**:Python + HTTP Server + 长轮询机制
|
||||||
- **前端**:Vue 3 + TypeScript + Monaco Editor
|
- **前端**:Vue 3 + TypeScript + Monaco Editor
|
||||||
- **通信**:HTTP 轮询机制实现双向通信
|
- **通信**:HTTP 长轮询 + 请求挂起实现低延迟通信
|
||||||
- **客户端**:CC:Tweaked + HTTP
|
- **客户端**:CC:Tweaked + HTTP
|
||||||
|
|
||||||
## 🤝 开发说明
|
## 🤝 开发说明
|
||||||
|
|
||||||
<<<<<<< HEAD
|
该项目使用创新的 HTTP 长轮询 + 请求挂起机制来模拟 WebSocket 的低延迟传输特性,为远程代码编辑提供了近乎实时的响应体验。
|
||||||
该项目目前主要支持远程代码编辑功能,使用 HTTP 协议替代 WebSocket,提高了兼容性和部署便利性。远程控制台功能计划在后续版本中开发。
|
|
||||||
=======
|
远程控制台功能计划在后续版本中开发。
|
||||||
该项目目前主要支持远程代码编辑功能,远程控制台功能计划在后续版本中开发。
|
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
你可以制作适配不同平台的客户端然后共享其代码
|
你可以制作适配不同平台的客户端然后共享其代码
|
||||||
|
|
||||||
欢迎提交issues
|
欢迎提交 issues
|
||||||
|
|
||||||
>>>>>>> d3faa4b74bc0eeac9a272c4d8a348d98a48dad7e
|
|
||||||
|
|||||||
Reference in New Issue
Block a user