更改项目名为NebulaShell
This commit is contained in:
51
store/@{NebulaShell}/http-tcp/README.md
Normal file
51
store/@{NebulaShell}/http-tcp/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# http-tcp HTTP TCP 服务
|
||||
|
||||
提供基于 TCP 的 HTTP 协议实现。
|
||||
|
||||
## 功能
|
||||
|
||||
- 原始 TCP HTTP 服务器
|
||||
- 路由匹配
|
||||
- 中间件链(日志/CORS)
|
||||
- 连接管理
|
||||
- 事件发布(通过 plugin-bridge)
|
||||
|
||||
## 使用
|
||||
|
||||
```python
|
||||
tcp = plugin_mgr.get("http-tcp")
|
||||
|
||||
# 注册路由
|
||||
tcp.router.get("/api/status", lambda req: {
|
||||
"status": 200,
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": '{"status": "ok"}'
|
||||
})
|
||||
|
||||
# 获取客户端
|
||||
clients = tcp.server.get_clients()
|
||||
```
|
||||
|
||||
## 事件
|
||||
|
||||
```python
|
||||
bridge = plugin_mgr.get("plugin-bridge")
|
||||
bus = bridge.event_bus
|
||||
|
||||
bus.on("tcp.connect", lambda e: print(f"连接: {e.client.id}"))
|
||||
bus.on("tcp.http.request", lambda e: print(f"请求: {e.context['request']['path']}"))
|
||||
bus.on("tcp.disconnect", lambda e: print(f"断开: {e.client.id}"))
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8082
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
8
store/@{NebulaShell}/http-tcp/SIGNATURE
Normal file
8
store/@{NebulaShell}/http-tcp/SIGNATURE
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"signature": "Adt4Pa7dzXVC9LuotOb2hvUREP2sQyInReCfPRVnKLuD2IB+5Uk4BSCjt5EkUUcMiEwIYoefntc1Q0f4k/OL3F4WtKFrwb4G+WJZYuwSbYZ3l4wYtivMFTuP4PjIgz1/sWUfqHdd+jwOquM9a8+uiNaxiz+Ed9UmBCqiJXjbfiP5A5RlkUGO3evwuP51dhfo3BVU+YuVWzSWfVw8Ov9Wx1V0h7fEjPPYof1d9AP+yVnfLLfBeNL1T/VlpkogllRlcqOQm5w+s17sLhR6sQEBHHTsga7Nilh8/BMmXr3vFDrtPbPsOqVGzHvYOFFJf26geFgxowPJ5YxEL9FKp9NtOp0fsDsq6f74mES9nTg7v9uImL8zzYn774fpaIfbOL2CVqsCqzW+kYhNm7fsJD8SfmhwKR8tVEsYvqUiHqpzUwX/J7soD0jlN/ttUUCZREERRKIpumHNNxkcgLuTYsloeSrG935ZOSEt6QuWSg9+dlXgdi84UmE1TbU6Q6HKExopOJitYCUM1p21G5wcFgEn+o7zdkDUdCJEliG1QeqSHdhlo/QyLuH/7mZQOMdprHabggTUrmbrES78nT10XEFWjtUfKxuzQkWwozwYPx6cBdmO4OLYJ+C5u1hwgmVm6if6IbCPm0l/NGy8NUNjH0PxDdmPaUSdnvSLLwa6fwr5/h0=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775960645.9258935,
|
||||
"plugin_hash": "136d916944b4b1e37134b3b9807a8ea19fc9c4971c62d15cc11e019502de5617",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
21
store/@{NebulaShell}/http-tcp/events.py
Normal file
21
store/@{NebulaShell}/http-tcp/events.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""HTTP TCP 事件定义"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class TcpEvent:
|
||||
"""TCP 事件"""
|
||||
type: str
|
||||
client: Any = None
|
||||
data: bytes = b""
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# 事件类型常量
|
||||
EVENT_CONNECT = "tcp.connect"
|
||||
EVENT_DISCONNECT = "tcp.disconnect"
|
||||
EVENT_DATA = "tcp.data"
|
||||
EVENT_REQUEST = "tcp.http.request"
|
||||
EVENT_RESPONSE = "tcp.http.response"
|
||||
EVENT_ERROR = "tcp.error"
|
||||
34
store/@{NebulaShell}/http-tcp/main.py
Normal file
34
store/@{NebulaShell}/http-tcp/main.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""HTTP TCP 插件入口"""
|
||||
from oss.plugin.types import Plugin, register_plugin_type
|
||||
from .server import TcpHttpServer
|
||||
from .router import TcpRouter
|
||||
from .middleware import TcpMiddlewareChain
|
||||
|
||||
|
||||
class HttpTcpPlugin(Plugin):
|
||||
"""HTTP TCP 插件"""
|
||||
|
||||
def __init__(self):
|
||||
self.server = None
|
||||
self.router = TcpRouter()
|
||||
self.middleware = TcpMiddlewareChain()
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
"""初始化"""
|
||||
self.server = TcpHttpServer(self.router, self.middleware)
|
||||
|
||||
def start(self):
|
||||
"""启动"""
|
||||
self.server.start()
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
if self.server:
|
||||
self.server.stop()
|
||||
|
||||
|
||||
register_plugin_type("HttpTcpPlugin", HttpTcpPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return HttpTcpPlugin()
|
||||
21
store/@{NebulaShell}/http-tcp/manifest.json
Normal file
21
store/@{NebulaShell}/http-tcp/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "http-tcp",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "HTTP TCP 服务 - 基于 TCP 的 HTTP 协议实现/多语言支持",
|
||||
"type": "protocol"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8082,
|
||||
"ssl_enabled": false,
|
||||
"max_connections": 500,
|
||||
"timeout": 30
|
||||
}
|
||||
},
|
||||
"dependencies": ["i18n"],
|
||||
"permissions": ["lifecycle"]
|
||||
}
|
||||
53
store/@{NebulaShell}/http-tcp/middleware.py
Normal file
53
store/@{NebulaShell}/http-tcp/middleware.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""TCP HTTP 中间件链"""
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
|
||||
class TcpMiddleware:
|
||||
"""TCP 中间件基类"""
|
||||
def process(self, request: dict, next_fn: Callable) -> Optional[dict]:
|
||||
"""处理请求"""
|
||||
return next_fn()
|
||||
|
||||
|
||||
class TcpLogMiddleware(TcpMiddleware):
|
||||
"""日志中间件"""
|
||||
def process(self, request, next_fn):
|
||||
print(f"[http-tcp] {request.get('method')} {request.get('path')}")
|
||||
return next_fn()
|
||||
|
||||
|
||||
class TcpCorsMiddleware(TcpMiddleware):
|
||||
"""CORS 中间件"""
|
||||
def process(self, request, next_fn):
|
||||
response = next_fn()
|
||||
if response:
|
||||
response.setdefault("headers", {})
|
||||
response["headers"]["Access-Control-Allow-Origin"] = "*"
|
||||
return response
|
||||
|
||||
|
||||
class TcpMiddlewareChain:
|
||||
"""TCP 中间件链"""
|
||||
|
||||
def __init__(self):
|
||||
self.middlewares: list[TcpMiddleware] = []
|
||||
self.add(TcpLogMiddleware())
|
||||
self.add(TcpCorsMiddleware())
|
||||
|
||||
def add(self, middleware: TcpMiddleware):
|
||||
"""添加中间件"""
|
||||
self.middlewares.append(middleware)
|
||||
|
||||
def run(self, request: dict) -> Optional[dict]:
|
||||
"""执行中间件链"""
|
||||
idx = 0
|
||||
|
||||
def next_fn():
|
||||
nonlocal idx
|
||||
if idx < len(self.middlewares):
|
||||
mw = self.middlewares[idx]
|
||||
idx += 1
|
||||
return mw.process(request, next_fn)
|
||||
return None
|
||||
|
||||
return next_fn()
|
||||
21
store/@{NebulaShell}/http-tcp/router.py
Normal file
21
store/@{NebulaShell}/http-tcp/router.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""TCP HTTP 路由器"""
|
||||
from typing import Callable, Optional, Any
|
||||
from oss.shared.router import BaseRouter, match_path
|
||||
|
||||
|
||||
class TcpRouter(BaseRouter):
|
||||
"""TCP HTTP 路由器"""
|
||||
|
||||
def handle(self, request: dict) -> dict:
|
||||
"""处理请求"""
|
||||
method = request.get("method", "GET")
|
||||
path = request.get("path", "/")
|
||||
|
||||
result = self.find_route(method, path)
|
||||
if result:
|
||||
route, params = result
|
||||
# 将路径参数注入到请求中
|
||||
request["path_params"] = params
|
||||
return route.handler(request)
|
||||
|
||||
return {"status": 404, "headers": {}, "body": "Not Found"}
|
||||
237
store/@{NebulaShell}/http-tcp/server.py
Normal file
237
store/@{NebulaShell}/http-tcp/server.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""TCP HTTP 服务器核心"""
|
||||
import socket
|
||||
import threading
|
||||
import re
|
||||
from typing import Any, Callable, Optional
|
||||
from oss.config import get_config
|
||||
from .events import TcpEvent, EVENT_CONNECT, EVENT_DISCONNECT, EVENT_DATA, EVENT_REQUEST, EVENT_RESPONSE
|
||||
|
||||
|
||||
class TcpClient:
|
||||
"""TCP 客户端连接"""
|
||||
|
||||
def __init__(self, conn: socket.socket, address: tuple):
|
||||
self.conn = conn
|
||||
self.address = address
|
||||
self.id = f"{address[0]}:{address[1]}"
|
||||
|
||||
def send(self, data: bytes):
|
||||
"""发送数据"""
|
||||
self.conn.sendall(data)
|
||||
|
||||
def close(self):
|
||||
"""关闭连接"""
|
||||
self.conn.close()
|
||||
|
||||
|
||||
class TcpHttpServer:
|
||||
"""TCP HTTP 服务器"""
|
||||
|
||||
def __init__(self, router, middleware, event_bus=None, host=None, port=None):
|
||||
config = get_config()
|
||||
self.host = host or config.get("HOST", "0.0.0.0")
|
||||
self.port = port or config.get("HTTP_TCP_PORT", 8082)
|
||||
self.router = router
|
||||
self.middleware = middleware
|
||||
self.event_bus = event_bus
|
||||
self._server = None
|
||||
self._thread = None
|
||||
self._running = False
|
||||
self._clients: dict[str, TcpClient] = {}
|
||||
|
||||
def start(self):
|
||||
"""启动服务器"""
|
||||
self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._server.bind((self.host, self.port))
|
||||
self._server.listen(128)
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._accept_loop, daemon=True)
|
||||
self._thread.start()
|
||||
print(f"[http-tcp] 服务器启动: {self.host}:{self.port}")
|
||||
|
||||
def _accept_loop(self):
|
||||
"""接受连接循环"""
|
||||
while self._running:
|
||||
try:
|
||||
conn, address = self._server.accept()
|
||||
client = TcpClient(conn, address)
|
||||
self._clients[client.id] = client
|
||||
|
||||
# 触发连接事件
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(type=EVENT_CONNECT, client=client))
|
||||
|
||||
# 启动处理线程
|
||||
t = threading.Thread(target=self._handle_client, args=(client,), daemon=True)
|
||||
t.start()
|
||||
except Exception as e:
|
||||
if self._running:
|
||||
print(f"[http-tcp] 接受连接失败: {e}")
|
||||
|
||||
def _handle_client(self, client: TcpClient):
|
||||
"""处理客户端请求"""
|
||||
buffer = b""
|
||||
try:
|
||||
while self._running:
|
||||
data = client.conn.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
buffer += data
|
||||
|
||||
# 检查 HTTP 请求头是否完整
|
||||
if b"\r\n\r\n" in buffer:
|
||||
# 先解析请求头以获取 Content-Length
|
||||
header_end = buffer.find(b"\r\n\r\n")
|
||||
header_text = buffer[:header_end].decode("utf-8", errors="replace")
|
||||
|
||||
# 从请求头中提取 Content-Length
|
||||
content_length = 0
|
||||
for line in header_text.split("\r\n")[1:]:
|
||||
if line.lower().startswith("content-length:"):
|
||||
content_length = int(line.split(":", 1)[1].strip())
|
||||
break
|
||||
|
||||
# 计算 body 起始位置
|
||||
body_start_pos = header_end + 4 # \r\n\r\n
|
||||
body_received = len(buffer) - body_start_pos
|
||||
|
||||
# 等待完整 body
|
||||
if body_received < content_length:
|
||||
# 继续接收剩余数据
|
||||
while body_received < content_length:
|
||||
remaining = content_length - body_received
|
||||
chunk = client.conn.recv(min(4096, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
buffer += chunk
|
||||
body_received += len(chunk)
|
||||
|
||||
# 现在解析完整请求
|
||||
request = self._parse_request(buffer)
|
||||
if request:
|
||||
# 触发请求事件
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(
|
||||
type=EVENT_REQUEST,
|
||||
client=client,
|
||||
context={"request": request}
|
||||
))
|
||||
|
||||
# 路由处理
|
||||
response = self.router.handle(request)
|
||||
|
||||
# 发送响应
|
||||
response_bytes = self._format_response(response)
|
||||
client.send(response_bytes)
|
||||
|
||||
# 触发响应事件
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(
|
||||
type=EVENT_RESPONSE,
|
||||
client=client,
|
||||
data=response_bytes
|
||||
))
|
||||
|
||||
buffer = b""
|
||||
|
||||
except ConnectionResetError:
|
||||
# 客户端断开连接,正常情况
|
||||
pass
|
||||
except BrokenPipeError:
|
||||
# 管道破裂,正常情况
|
||||
pass
|
||||
except OSError as e:
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(type=EVENT_ERROR, client=client, context={"error": f"OSError: {e}"}))
|
||||
except Exception as e:
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(type=EVENT_ERROR, client=client, context={"error": f"{type(e).__name__}: {e}"}))
|
||||
finally:
|
||||
del self._clients[client.id]
|
||||
client.close()
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(type=EVENT_DISCONNECT, client=client))
|
||||
|
||||
def _parse_request(self, data: bytes) -> Optional[dict]:
|
||||
"""解析 HTTP 请求"""
|
||||
try:
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
lines = text.split("\r\n")
|
||||
if not lines:
|
||||
return None
|
||||
|
||||
# 解析请求行
|
||||
match = re.match(r'(\w+)\s+(\S+)\s+HTTP/(\d\.\d)', lines[0])
|
||||
if not match:
|
||||
return None
|
||||
|
||||
method, path, version = match.groups()
|
||||
|
||||
# 解析头
|
||||
headers = {}
|
||||
body_start = 0
|
||||
for i, line in enumerate(lines[1:], 1):
|
||||
if line == "":
|
||||
body_start = i + 1
|
||||
break
|
||||
if ":" in line:
|
||||
key, value = line.split(":", 1)
|
||||
headers[key.strip()] = value.strip()
|
||||
|
||||
# 解析体
|
||||
content_length = int(headers.get("Content-Length", 0))
|
||||
body = "\r\n".join(lines[body_start:]) if body_start else ""
|
||||
|
||||
return {
|
||||
"method": method,
|
||||
"path": path,
|
||||
"version": version,
|
||||
"headers": headers,
|
||||
"body": body,
|
||||
}
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
except Exception as e:
|
||||
# 其他解析错误
|
||||
import traceback; print(f"[http-tcp] HTTP 解析失败:{type(e).__name__}: {e}"); traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _format_response(self, response: dict) -> bytes:
|
||||
"""格式化 HTTP 响应"""
|
||||
status = response.get("status", 200)
|
||||
headers = response.get("headers", {})
|
||||
body = response.get("body", "")
|
||||
|
||||
status_text = {200: "OK", 404: "Not Found", 500: "Internal Server Error"}.get(status, "OK")
|
||||
|
||||
response_lines = [
|
||||
f"HTTP/1.1 {status} {status_text}",
|
||||
]
|
||||
|
||||
if "Content-Type" not in headers:
|
||||
headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||
headers["Content-Length"] = str(len(body))
|
||||
|
||||
for key, value in headers.items():
|
||||
response_lines.append(f"{key}: {value}")
|
||||
|
||||
response_lines.append("")
|
||||
response_lines.append(body)
|
||||
|
||||
return "\r\n".join(response_lines).encode("utf-8")
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
self._running = False
|
||||
for client in self._clients.values():
|
||||
client.close()
|
||||
if self._server:
|
||||
self._server.close()
|
||||
print("[http-tcp] 服务器已停止")
|
||||
|
||||
def get_clients(self) -> list[TcpClient]:
|
||||
"""获取所有客户端"""
|
||||
return list(self._clients.values())
|
||||
Reference in New Issue
Block a user