--- outline: deep --- # 通过 UNIX Domain Socket 在沙箱中运行 HTTP 服务 本文档将指导你如何通过 **UNIX Domain Socket** 在 Mythra Agent Studio 的沙箱中运行自己的 HTTP 服务,并让外部客户端访问你的服务。 ## 概述 在 Mythra Agent Studio 平台上,你可以在沙箱的 Docker 容器中运行自己的 HTTP 服务。由于平台是多租户架构,所有用户的 HTTP 服务都共享同一个域名(`https://agent.wiseverds.com`),平台通过 URL 路径来进行路由转发,将请求正确地发送到你的服务。 两种访问方式的选择: - **UNIX Domain Socket**(本文档):需要你的应用直接监听 Unix socket 文件,适用于新建服务或愿意改造服务的场景;平台通过 socket 文件路径进行路由,安全高效。 - **TCP 端口**([另见文档](./sandbox-http-service-via-tcp-socket.md)):通过容器名 + 端口号访问,适用于大多数现成的 Docker 镜像和服务(如数据库、缓存、Web 框架等),无需修改应用代码。 ## 什么是 UNIX Domain Socket 如果你是一名 Web 开发者,可能对 TCP Socket 比较熟悉——HTTP 服务通常监听一个 TCP 端口(如 80、8080、3000 等)。而 UNIX Domain Socket 是另一种通信方式,它通过**文件系统中的文件**来进行通信,类似于 Windows 系统中的命名管道。 ### TCP Socket vs UNIX Domain Socket | 特性 | TCP Socket | UNIX Domain Socket | |------|-----------|-------------------| | 通信方式 | 通过 IP 地址和端口号 | 通过文件系统中的文件路径 | | 跨机器访问 | 支持 | 不支持(仅限本机) | | 性能 | 较低(需要网络协议栈) | 更高(直接通过内核通信) | | 典型用途 | Web 服务、API 服务 | 本地进程间通信、Nginx 与后端服务通信 | ### 为什么平台使用 UNIX Domain Socket 在 Mythra Agent Studio 平台上,我们使用 UNIX Domain Socket 有以下原因: 1. **安全性**:UNIX Domain Socket 只能在本机访问,不会暴露到网络上 2. **寻址简单**:Socket 文件有明确的路径,平台可以根据路径进行路由 3. **性能优势**:UNIX Domain Socket 的通信效率比 TCP Socket 更高 4. **无需端口管理**:不需要担心端口冲突问题 ## 平台的路由机制 当你在沙箱中运行一个监听 UNIX Domain Socket 的 HTTP 服务后,外部客户端可以通过以下 URL 格式访问: ``` https://agent.wiseverds.com/WebIDE/api/v1/projects/{ProjectID}/sandboxes/{SandboxID}/unix-socket/{SockPathB64}/http/ ``` 其中: - `ProjectID`:你的项目 ID - `SandboxID`:你的沙箱 ID - `SockPathB64`:Socket 文件路径的 Base64 编码 平台会将这个 URL 的请求转发到沙箱中对应的 Socket 文件。 ## 快速开始 让我们从一个最简单的例子开始:创建一个 "Hello World" HTTP 服务。 ### Node.js 示例 创建一个名为 `server.js` 的文件: ```javascript const http = require('http'); const fs = require('fs'); const path = require('path'); // 定义 socket 文件路径 const socketPath = path.join(__dirname, 'app.sock'); // 启动前先删除旧的 socket 文件(避免地址已被占用错误) if (fs.existsSync(socketPath)) { fs.unlinkSync(socketPath); } const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello from UNIX Domain Socket!\n'); }); // 创建 HTTP 服务器 server.listen(socketPath, () => { console.log(`HTTP server listening on socket: ${socketPath}`); // 给 socket 文件加上权限,让其他进程能访问 fs.chmodSync(socketPath, '0777'); }); ``` ### Python 示例 创建一个名为 `server.py` 的文件: ```python import http.server import socketserver import os import socket import base64 # 定义 socket 文件路径 socket_path = os.path.join(os.path.dirname(__file__), 'app.sock') # 启动前先删除旧的 socket 文件(避免地址已被占用错误) if os.path.exists(socket_path): os.unlink(socket_path) class UnixSocketHTTPServer(socketserver.UnixStreamServer): allow_reuse_address = True def __init__(self, socket_path, handler): self.socket_path = socket_path super().__init__(socket_path, handler) # 创建 HTTP 请求处理类 class Handler(http.server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(b'Hello from UNIX Domain Socket!\n') with UnixSocketHTTPServer(socket_path, Handler) as httpd: # 给 socket 文件加上权限,让其他进程能访问 os.chmod(socket_path, 0o777) print(f'HTTP server listening on socket: {socket_path}') # 保持服务运行 httpd.serve_forever() ``` ### 启动服务 在沙箱中运行你的服务: ```bash # Node.js node server.js # 或 Python python3 server.py ``` 服务启动后,你会在控制台看到类似输出: ``` HTTP server listening on socket: /path/to/app.sock ``` 此时,你的 HTTP 服务已经在沙箱中监听了 `app.sock` 这个 UNIX Domain Socket,你应当可以在沙箱的文件管理器中看到 `app.sock` 文件。 ``` root@8052aa1f3bb3:/data# ls -l app.sock srwxrwxrwx 1 root root 0 May 28 15:21 app.sock ``` ## 访问你的 HTTP 服务 ### 获取访问 URL 要访问你的 HTTP 服务,你需要构造正确的 URL。假设: - 项目 ID:`2231` - 沙箱 ID:`j4urFhRG` - Socket 文件路径:`app.sock`(相对路径) - 接口路径: 你的HTTP接口的路径,例如 `/user/login` 首先,将 Socket 文件路径转换为 Base64 编码: ```bash # 在沙箱中执行 echo -n "app.sock" | base64 # 或者用 Node.js 进行 Base64 编码 node -e "console.log(Buffer.from('app.sock').toString('base64'))" # 输出:YXBwLnNvY2s= ``` 然后构造完整的 URL: ``` https://agent.wiseverds.com/WebIDE/api/v1/projects/2231/sandboxes/j4urFhRG/unix-socket/YXBwLnNvY2s=/http/user/login ``` 此外,为了保证接口安全,平台要求在请求头中包含有效的 API 密钥。请确保在 HTTP 请求的 `Authorization` 头中添加正确的 API 密钥,使用 `Bearer` 前缀。 ### 使用 curl 测试 ```bash #!/bin/bash WDS_BASE_URL="https://agent.wiseverds.com" WDS_API_KEY="WDS_**************" ProjectID=2231 SandboxID=j4urFhRG SockPath=app.sock SockPathB64=$(echo -n "$SockPath" | base64) curl -i \ -H "Authorization: Bearer $WDS_API_KEY" \ "$WDS_BASE_URL/WebIDE/api/v1/projects/$ProjectID/sandboxes/$SandboxID/unix-socket/$SockPathB64/http/" ``` 如果一切正常,你会看到: ``` HTTP/1.1 200 OK Content-Type: text/plain Hello from UNIX Domain Socket! ``` ## 进阶用法 ### 将现有的 TCP 服务转换为 UNIX Socket 如果你已经有一个监听 TCP 端口的 HTTP 服务(例如监听 8080 端口),可以使用沙箱中提供的 `socat` 工具将其转换为监听 UNIX Domain Socket: ```bash # 假设你的服务监听在 localhost:8080 socat UNIX-LISTEN:app.sock,fork,reuseaddr TCP:localhost:8080 ``` 这个命令会: 1. 监听 `app.sock` 这个 UNIX Domain Socket 2. 将所有收到的请求转发到 `localhost:8080` 3. `fork` 参数允许处理多个并发连接 4. `reuseaddr` 参数允许重用 Socket 文件 ### 使用 Express 框架(Node.js) 如果你使用 Express 框架,同样可以监听 UNIX Domain Socket: ```javascript const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); const socketPath = path.join(__dirname, 'app.sock'); if (fs.existsSync(socketPath)) { fs.unlinkSync(socketPath); } app.get('/', (req, res) => { res.send('Hello from Express on UNIX Domain Socket!'); }); app.listen(socketPath, () => { console.log(`Express server listening on socket: ${socketPath}`); fs.chmodSync(socketPath, '0777'); }); ``` ### 使用 Flask 框架(Python) 如果你使用 Flask 框架,可以通过 `gevent` 或 `uwsgi` 来监听 UNIX Domain Socket: ```python from flask import Flask import os import socket app = Flask(__name__) socket_path = os.path.join(os.path.dirname(__file__), 'app.sock') @app.route('/') def hello(): return 'Hello from Flask on UNIX Domain Socket!' if __name__ == '__main__': if os.path.exists(socket_path): os.unlink(socket_path) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(socket_path) os.chmod(socket_path, 0o777) sock.listen(1) print(f'Flask server listening on socket: {socket_path}') app.run(sock=sock) ``` ### 处理路径和查询参数 平台在转发请求时,会自动进行 URL rewrite,将请求路径中的 `.../http/` 前缀去掉,仅保留自定义接口路径。 因此,你的 HTTP 服务可以按照正常逻辑处理接口路径和查询参数: ```javascript const http = require('http'); const fs = require('fs'); const path = require('path'); const url = require('url'); const socketPath = path.join(__dirname, 'app.sock'); if (fs.existsSync(socketPath)) { fs.unlinkSync(socketPath); } const server = http.createServer((req, res) => { const parsedUrl = url.parse(req.url, true); console.log('Path:', parsedUrl.pathname); console.log('Query:', parsedUrl.query); if (parsedUrl.pathname === '/api/users') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ users: ['Alice', 'Bob'] })); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); server.listen(socketPath, () => { console.log(`HTTP server listening on socket: ${socketPath}`); fs.chmodSync(socketPath, '0777'); }); ``` 访问示例: ```bash curl -H "Authorization: Bearer $WDS_API_KEY" \ "$WDS_BASE_URL/WebIDE/api/v1/projects/$ProjectID/sandboxes/$SandboxID/unix-socket/$SockPathB64/http/api/users?page=1" ``` ### 使用 WebSocket 协议 平台不仅支持 HTTP 协议,还支持 WebSocket 协议,可用于开发实时应用。 平台对于 WebSocket 请求的路由规则与 HTTP 完全相同,平台会自动判断请求是 HTTP 还是 WebSocket,并进行相应的转发。 #### Node.js WebSocket 示例 以下是一个简单的 WebSocket echo 服务示例: ```javascript const fs = require('fs'); const path = require('path'); const http = require('http'); const WebSocket = require('ws'); // npm install ws const socketPath = path.join(__dirname, 'app.sock'); if (fs.existsSync(socketPath)) { fs.unlinkSync(socketPath); } // 1. 先建一个普通 http server,监听 unix socket const server = http.createServer(); server.listen(socketPath, () => { console.log('HTTP server listening on', socketPath); fs.chmodSync(socketPath, '0777'); }); // 2. 再在这个 http server 上建一个 websocket server const wss = new WebSocket.Server({ server }); wss.on('connection', (ws) => { console.log('Client connected'); ws.on('message', (message) => { console.log('Received:', message); ws.send(`Echo: ${message}`); }); ws.on('close', () => { console.log('Client disconnected'); }); }); ``` #### Python WebSocket 示例 使用 `websockets` 库实现 WebSocket echo 服务: ```python import asyncio import websockets # pip install websockets 或者 uv add websockets import os socket_path = os.path.join(os.path.dirname(__file__), 'app.sock') if os.path.exists(socket_path): os.unlink(socket_path) async def echo(websocket): print('Client connected') try: async for message in websocket: print(f'Received: {message}') await websocket.send(f'Echo: {message}') except Exception as e: print('Exception:', e) print('Client disconnected') async def main(): async with websockets.unix_serve(echo, socket_path): os.chmod(socket_path, 0o777) print(f'WebSocket server listening on socket: {socket_path}') await asyncio.Future() if __name__ == '__main__': asyncio.run(main()) ``` #### 使用 wscat 测试 首先安装 `wscat`: ```bash npm install -g wscat ``` 然后连接到你的 WebSocket 服务: ```bash #!/bin/bash WDS_BASE_URL="https://agent.wiseverds.com" WDS_API_KEY="WDS_**************" ProjectID=2231 SandboxID=j4urFhRG SockPath=app.sock SockPathB64=$(echo -n "$SockPath" | base64) wscat -c "wss://$WDS_BASE_URL/WebIDE/api/v1/projects/$ProjectID/sandboxes/$SandboxID/unix-socket/$SockPathB64/http/" \ -H "Authorization: Bearer $WDS_API_KEY" ``` 连接成功后,你可以输入任意消息,服务器会返回 `Echo: <你的消息>`。 注意: - WebSocket 使用 `wss://` 协议(加密的 WebSocket) - URL 路径格式与 HTTP 完全相同 - 同样需要提供 `Authorization` 头部进行身份验证 ## 常见问题 ### Socket 文件已存在错误 如果启动服务时遇到 `EADDRINUSE` 错误,说明 Socket 文件已存在。解决方法是在启动前删除旧的 Socket 文件: ```javascript // Node.js if (fs.existsSync(socketPath)) { fs.unlinkSync(socketPath); } ``` ```python # Python if os.path.exists(socket_path): os.unlink(socket_path) ``` ### 权限问题 如果遇到权限错误,确保 Socket 文件的权限设置为 `0777`: ```bash # 在启动服务后执行 chmod 0777 app.sock ``` 或在代码中设置: ```javascript // Node.js fs.chmodSync(socketPath, '0777'); ``` ```python # Python os.chmod(socket_path, 0o777) ``` ### 如何获取项目 ID 和沙箱 ID 你可以在 Mythra Agent Studio 的界面中查看项目 ID 和沙箱 ID,或者通过 API 获取。 ### Socket 文件路径的 Base64 编码 确保使用正确的 Base64 编码方式: ```bash # 正确的方式(不包含换行符) echo -n "app.sock" | base64 # 错误的方式(包含换行符) echo "app.sock" | base64 ``` 你也可以使用 Node.js 进行 Base64 编码: ```javascript // Node.js console.log(Buffer.from('app.sock').toString('base64')); // 输出:YXBwLnNvY2s= ``` 或者使用 Python 进行 Base64 编码: ```python # Python import base64 socket_path = 'app.sock' base64_path = base64.b64encode(socket_path.encode('utf-8')).decode('utf-8') print(base64_path) # 输出:YXBwLnNvY2s= ``` ### 服务无法访问 如果服务无法访问,请检查: 1. 你的 HTTP 服务是否正在运行 2. Socket 文件是否存在 3. Socket 文件权限是否正确 4. URL 中的 Base64 编码是否正确 5. API Key 是否有效 ## 最佳实践 1. **清理 Socket 文件**:在启动服务前,总是删除可能存在的旧 Socket 文件 2. **设置正确的权限**:确保 Socket 文件权限为 `0777`,让平台可以访问 3. **使用绝对路径**:建议使用绝对路径指定 Socket 文件位置,避免路径混淆 4. **日志记录**:记录请求日志,便于调试和监控 5. **错误处理**:妥善处理各种错误情况,如 Socket 文件创建失败、权限问题等 6. **优雅关闭**:在服务关闭时清理 Socket 文件 ## 相关文档 - [通过 TCP 端口访问容器内服务](./sandbox-http-service-via-tcp-socket.md) - 另一种访问容器内服务的方式,无需修改应用代码 - [沙箱](./sandbox.md) - 了解沙箱的基本概念和使用方法 - [开发外部 AI Agent](./develop-external-agent.md) - 学习如何开发外部 Agent 服务