Compare commits
3 Commits
83822ae165
...
1.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33ff81a15d | ||
|
|
c6d9d4f093 | ||
|
|
d862467883 |
@@ -114,7 +114,7 @@ local function httpPost(path, data)
|
|||||||
local jsonData = table_to_json(data)
|
local jsonData = table_to_json(data)
|
||||||
local url = httpServer .. path
|
local url = httpServer .. path
|
||||||
|
|
||||||
-- 使用长轮询,设置超时时间为300秒
|
-- 使用长轮询
|
||||||
local response,err = http.post({
|
local response,err = http.post({
|
||||||
url = url,
|
url = url,
|
||||||
body = jsonData,
|
body = jsonData,
|
||||||
@@ -122,7 +122,7 @@ local function httpPost(path, data)
|
|||||||
headers = {
|
headers = {
|
||||||
["Content-Type"] = "application/json"
|
["Content-Type"] = "application/json"
|
||||||
},
|
},
|
||||||
timeout = 300 -- 300秒超时
|
timeout = 60
|
||||||
})
|
})
|
||||||
|
|
||||||
if not response then
|
if not response then
|
||||||
|
|||||||
105
PyServer/main.py
105
PyServer/main.py
@@ -48,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] = []
|
||||||
@@ -88,35 +100,31 @@ class HTTPHandler:
|
|||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
async def handle_frontend_send_message(self, request):
|
async def handle_frontend_send_message(self, request):
|
||||||
"""前端发送消息到客户端"""
|
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
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 = data.get('message')
|
message_data = data.get('message')
|
||||||
|
|
||||||
if not room_id or not message_data:
|
if not room_id or not message_data:
|
||||||
return web.json_response({'error': '需要room_id和message参数'}, status=400)
|
return web.json_response({'error': '需要room_id和message参数'}, status=400)
|
||||||
|
|
||||||
queue = get_frontend_to_client_queue(room_id)
|
queue = get_frontend_to_client_queue(room_id)
|
||||||
queue.append(message_data)
|
queue.append(message_data)
|
||||||
|
|
||||||
# 检查是否有挂起的客户端请求并立即响应
|
|
||||||
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
||||||
client_req_id = pending_requests[room_id]['client']
|
client_req_id = pending_requests[room_id]['client']
|
||||||
if client_req_id in pending_requests:
|
if client_req_id in pending_requests:
|
||||||
pending_requests[client_req_id]['event'].set()
|
pending_requests[client_req_id]['event'].set()
|
||||||
logger.info(f"立即响应挂起的客户端请求: {client_req_id}")
|
logger.info(f"立即响应挂起的客户端请求: {client_req_id}")
|
||||||
|
|
||||||
response = {
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '消息已发送到客户端队列'
|
'message': '消息已发送到客户端队列'
|
||||||
}
|
})
|
||||||
|
|
||||||
return web.json_response(response)
|
except ValueError as e:
|
||||||
|
logger.error(f"前端发送消息失败: 无效的JSON或编码错误 - {e}")
|
||||||
except json.JSONDecodeError:
|
return web.json_response({'error': '无效的JSON数据或不支持的文本编码'}, status=400)
|
||||||
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}")
|
||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
@@ -204,35 +212,32 @@ class HTTPHandler:
|
|||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
async def handle_client_send_message(self, request):
|
async def handle_client_send_message(self, request):
|
||||||
"""客户端发送消息到前端"""
|
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
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 = data.get('message')
|
message_data = data.get('message')
|
||||||
|
|
||||||
if not room_id or not message_data:
|
if not room_id or not message_data:
|
||||||
return web.json_response({'error': '需要room_id和message参数'}, status=400)
|
return web.json_response({'error': '需要room_id和message参数'}, status=400)
|
||||||
|
|
||||||
queue = get_client_to_frontend_queue(room_id)
|
queue = get_client_to_frontend_queue(room_id)
|
||||||
queue.append(message_data)
|
queue.append(message_data)
|
||||||
|
|
||||||
# 检查是否有挂起的前端请求并立即响应
|
|
||||||
if room_id in pending_requests and 'frontend' in pending_requests[room_id]:
|
if room_id in pending_requests and 'frontend' in pending_requests[room_id]:
|
||||||
frontend_req_id = pending_requests[room_id]['frontend']
|
frontend_req_id = pending_requests[room_id]['frontend']
|
||||||
if frontend_req_id in pending_requests:
|
if frontend_req_id in pending_requests:
|
||||||
pending_requests[frontend_req_id]['event'].set()
|
pending_requests[frontend_req_id]['event'].set()
|
||||||
logger.info(f"立即响应挂起的前端请求: {frontend_req_id}")
|
logger.info(f"立即响应挂起的前端请求: {frontend_req_id}")
|
||||||
|
|
||||||
response = {
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '消息已发送到前端队列'
|
'message': '消息已发送到前端队列'
|
||||||
}
|
})
|
||||||
|
|
||||||
return web.json_response(response)
|
except ValueError as e:
|
||||||
|
logger.error(f"客户端发送消息失败: 无效的JSON或编码错误 - {e}")
|
||||||
except json.JSONDecodeError:
|
return web.json_response({'error': '无效的JSON数据或不支持的文本编码'}, status=400)
|
||||||
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}")
|
||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
@@ -240,12 +245,13 @@ class HTTPHandler:
|
|||||||
async def handle_client_receive_message(self, request):
|
async def handle_client_receive_message(self, request):
|
||||||
"""客户端接收来自前端的消息(长轮询)"""
|
"""客户端接收来自前端的消息(长轮询)"""
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
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:
|
||||||
return web.json_response({'error': '需要room_id参数'}, status=400)
|
return web.json_response({'error': '需要room_id参数'}, status=400)
|
||||||
|
|
||||||
queue = get_frontend_to_client_queue(room_id)
|
queue = get_frontend_to_client_queue(room_id)
|
||||||
|
|
||||||
# 立即检查是否有消息
|
# 立即检查是否有消息
|
||||||
@@ -257,7 +263,7 @@ class HTTPHandler:
|
|||||||
}
|
}
|
||||||
return web.json_response(response)
|
return web.json_response(response)
|
||||||
|
|
||||||
# 没有消息,设置长轮询
|
# 没有消息,设置长轮询(最多等待 58 秒)
|
||||||
req_id = str(uuid.uuid4())
|
req_id = str(uuid.uuid4())
|
||||||
event = asyncio.Event()
|
event = asyncio.Event()
|
||||||
|
|
||||||
@@ -274,10 +280,10 @@ class HTTPHandler:
|
|||||||
pending_requests[room_id]['client'] = req_id
|
pending_requests[room_id]['client'] = req_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 等待295秒或直到有消息
|
# ⏱️ 只等待 58 秒(略小于客户端或代理的 60 秒超时)
|
||||||
await asyncio.wait_for(event.wait(), timeout=295)
|
await asyncio.wait_for(event.wait(), timeout=58)
|
||||||
|
|
||||||
# 检查队列中是否有消息
|
# 被唤醒后,检查队列
|
||||||
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)
|
||||||
@@ -286,37 +292,36 @@ class HTTPHandler:
|
|||||||
'message': message
|
'message': message
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# 超时返回空消息
|
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': None
|
'message': None
|
||||||
}
|
}
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
# 超时返回空消息
|
# 58秒超时,返回空消息
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': None
|
'message': None
|
||||||
}
|
}
|
||||||
|
|
||||||
# 清理挂起的请求
|
# 清理挂起的请求
|
||||||
if req_id in pending_requests:
|
pending_requests.pop(req_id, None)
|
||||||
del pending_requests[req_id]
|
if room_id in pending_requests:
|
||||||
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
pending_requests[room_id].pop('client', None)
|
||||||
del pending_requests[room_id]['client']
|
# 如果 room_id 下已无其他引用,也可以清理整个 room 条目(可选)
|
||||||
|
|
||||||
return web.json_response(response)
|
return web.json_response(response)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except ValueError as e:
|
||||||
logger.error("JSON解析失败")
|
logger.error(f"客户端接收消息失败: 无效JSON或编码 - {e}")
|
||||||
return web.json_response({'error': '无效的JSON数据'}, status=400)
|
return web.json_response({'error': '无效的JSON数据'}, status=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"客户端接收消息失败: {e}")
|
logger.error(f"客户端接收消息失败: {e}")
|
||||||
# 清理挂起的请求
|
# 清理挂起的请求
|
||||||
if 'req_id' in locals() and req_id in pending_requests:
|
if 'req_id' in locals():
|
||||||
del pending_requests[req_id]
|
pending_requests.pop(req_id, None)
|
||||||
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
if 'room_id' in locals() and room_id in pending_requests:
|
||||||
del pending_requests[room_id]['client']
|
pending_requests[room_id].pop('client', None)
|
||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
async def handle_static_file(self, request):
|
async def handle_static_file(self, request):
|
||||||
|
|||||||
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