--- outline: deep --- # 通过 TCP 端口访问容器内服务 本文档将指导你如何在 Mythra Agent Studio 沙箱中运行 HTTP 或 WebSocket 服务,并通过 TCP 端口直接访问容器内的服务。 ## 概述 在 Mythra Agent Studio 平台上,你可以在沙箱的 Docker 容器中运行自己的 HTTP 服务。由于平台是多租户架构,所有用户的 HTTP 服务都共享同一个域名(`https://agent.wiseverds.com`),平台通过 URL 路径来进行路由转发,将请求正确地发送到你的服务。 两种访问方式的选择: - **TCP 端口** (本文档):通过容器名 + 端口号访问,适用于大多数现成的 Docker 镜像和服务(如数据库、缓存、Web 框架等),无需修改应用代码。 - **UNIX Domain Socket** ([另见文档](./sandbox-http-service-via-unix-socket.md)):需要你的应用直接监听 Unix socket 文件,适用于新建服务或愿意改造服务的场景;平台通过 socket 文件路径进行路由,安全高效。 ## 平台的路由机制 当你启动一个容器并在其中运行 HTTP/WebSocket 服务后,外部客户端可以通过以下 URL 访问: ``` https://agent.wiseverds.com/WebIDE/api/v1/projects/{ProjectID}/sandboxes/{SandboxID}/tcp-socket/{ServiceAddress}/http/{RequestPath} ``` 其中: - `ProjectID`:你的项目 ID - `SandboxID`:你的沙箱 ID - `ServiceAddress`:`容器名:端口号`(例如 `mythra-svc-12345678-projabcd-sandbox1-AbCdEfGh:8000`) - `RequestPath`:你的 HTTP 服务的接口路径(例如 `/api/users`) 平台会将这个 URL 的请求经过身份验证和容器归属校验后,通过 TCP 连接转发到沙箱容器中的指定端口。 ### 容器命名规则 当前平台创建的容器命名格式为: ``` mythra-{purpose}-{userId[:8]}-{projectId[:8]}-{sandboxId[:8]}-{nonce} ``` 例如:`mythra-svc-12345678-projabcd-sandbox1-AbCdEfGh` 你可以通过沙箱管理界面或 API 获取你当前沙箱中运行的容器名称。 ## 两种方式对比 | 特性 | TCP 端口 | UNIX Domain Socket | |------|----------|-------------------| | URL 路径 | `.../tcp-socket/{容器名}:{端口}/http/...` | `.../unix-socket/{Base64路径}/http/...` | | 寻址方式 | 容器名 + 端口号 | Socket 文件路径(Base64 编码) | | 适用场景 | 现成软件、数据库、缓存、Web 框架 | 新建服务、需要 socket 权限控制的场景 | | 代码改造 | 不需要 | 需要监听 Unix socket | | TCP 端口管理 | 需要监听指定端口 | 不需要 | | Socket 文件管理 | 不需要 | 需要创建、清理、设置权限 | | 性能 | 略低(需经网络栈) | 更高(直接内核通信) | | 跨容器访问 | 支持(同一项目内) | 支持(同一沙箱内) | ## 快速开始 让我们从一个最简单的例子开始:创建一个 "Hello World" HTTP 服务。 ### Node.js 示例 创建一个名为 `server.js` 的文件: ```javascript const http = require('http'); const PORT = 3000; const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello from TCP service!\n'); }); // 正常监听 TCP 端口 server.listen(PORT, '0.0.0.0', () => { console.log(`HTTP server listening on port ${PORT}`); }); ``` ### Python 示例 创建一个名为 `server.py` 的文件: ```python from http.server import HTTPServer, BaseHTTPRequestHandler class Handler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(b'Hello from TCP service!\n') PORT = 8000 server = HTTPServer(('0.0.0.0', PORT), Handler) print(f'HTTP server listening on port {PORT}') server.serve_forever() ``` ### 启动服务 在容器中运行你的服务(在容器启动时由 `command` 和 `args` 参数指定): ```bash # Node.js node server.js # Python python3 server.py ``` 服务启动后,你的 HTTP 服务就在容器内的 TCP 端口上监听了。 ## 访问你的 HTTP 服务 ### 获取容器名称 首先,你需要知道容器的名称。可以通过沙箱管理界面查看,或通过 API 获取容器列表。 假设容器的名称为: ``` mythra-svc-abcd1234-projabcd-sandbox1-XyZ12345 ``` 服务监听在容器的 `3000` 端口。 ### 构造访问 URL 假设: - 项目 ID:`2231` - 沙箱 ID:`j4urFhRG` - 容器名:`mythra-svc-abcd1234-projabcd-sandbox1-XyZ12345` - 端口:`3000` - 接口路径:`/user/login` 完整的 URL 为: ``` https://agent.wiseverds.com/WebIDE/api/v1/projects/2231/sandboxes/j4urFhRG/tcp-socket/mythra-svc-abcd1234-projabcd-sandbox1-XyZ12345:3000/http/user/login ``` > **提示**:URL 中的 `:` 可以使用 URL 编码 `%3A` 替代,两种写法均支持: > ``` > .../tcp-socket/mythra-svc-abcd1234-projabcd-sandbox1-XyZ12345%3A3000/http/... > ``` 为了保证接口安全,平台要求在请求头中包含有效的 API 密钥。请确保在 HTTP 请求的 `Authorization` 头中添加正确的 API 密钥,使用 `Bearer` 前缀。 ### 使用 curl 测试 ```bash #!/bin/bash WDS_BASE_URL="https://agent.wiseverds.com" WDS_API_KEY="WDS_**************" ProjectID=2231 SandboxID=j4urFhRG ContainerName="mythra-svc-abcd1234-projabcd-sandbox1-XyZ12345" Port=3000 ServiceAddress="${ContainerName}:${Port}" curl -i \ -H "Authorization: Bearer $WDS_API_KEY" \ "$WDS_BASE_URL/WebIDE/api/v1/projects/$ProjectID/sandboxes/$SandboxID/tcp-socket/$ServiceAddress/http/" ``` 如果一切正常,你会看到: ``` HTTP/1.1 200 OK Content-Type: text/plain Hello from TCP service! ``` ### 使用 JavaScript (fetch) 测试 ```javascript const baseUrl = 'https://agent.wiseverds.com'; const apiKey = 'WDS_**************'; const projectId = 2231; const sandboxId = 'j4urFhRG'; const containerName = 'mythra-svc-abcd1234-projabcd-sandbox1-XyZ12345'; const port = 3000; const serviceAddress = `${containerName}:${port}`; const url = `${baseUrl}/WebIDE/api/v1/projects/${projectId}/sandboxes/${sandboxId}/tcp-socket/${serviceAddress}/http/api/users`; const response = await fetch(url, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const data = await response.json(); console.log(data); ``` ## 进阶用法 ### 使用 Express 框架(Node.js) 使用 Express 框架的方式与标准 TCP 监听完全一致: ```javascript const express = require('express'); const app = express(); app.get('/api/users', (req, res) => { res.json({ users: ['Alice', 'Bob'] }); }); app.post('/api/users', (req, res) => { res.status(201).json({ message: 'User created' }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, '0.0.0.0', () => { console.log(`Express server listening on port ${PORT}`); }); ``` ### 使用 Flask 框架(Python) ```python from flask import Flask, jsonify app = Flask(__name__) @app.route('/api/users', methods=['GET']) def get_users(): return jsonify({'users': ['Alice', 'Bob']}) @app.route('/api/users', methods=['POST']) def create_user(): return jsonify({'message': 'User created'}), 201 if __name__ == '__main__': app.run(host='0.0.0.0', port=8000, debug=False) ``` ### 处理路径和查询参数 平台在转发请求时,会自动进行 URL rewrite,将请求路径中的 `.../http/` 前缀去掉,仅保留自定义接口路径。 因此,你的 HTTP 服务可以按照正常逻辑处理接口路径和查询参数: ```javascript const http = require('http'); const url = require('url'); const PORT = 3000; 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(PORT, '0.0.0.0', () => { console.log(`HTTP server listening on port ${PORT}`); }); ``` 访问示例: ```bash curl -H "Authorization: Bearer $WDS_API_KEY" \ "$WDS_BASE_URL/WebIDE/api/v1/projects/$ProjectID/sandboxes/$SandboxID/tcp-socket/$ServiceAddress/http/api/users?page=1" ``` ### WebSocket 协议 平台对 TCP 端口的 WebSocket 支持与 HTTP 完全相同,平台会自动判断请求是 HTTP 还是 WebSocket,并进行相应的转发。 #### Node.js WebSocket 示例 ```javascript const http = require('http'); const WebSocket = require('ws'); // npm install ws const PORT = 3000; const server = http.createServer(); server.listen(PORT, '0.0.0.0', () => { console.log('HTTP server listening on port', PORT); }); 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 示例 ```python import asyncio import websockets # pip install websockets 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.serve(echo, '0.0.0.0', 8765): print('WebSocket server listening on port 8765') await asyncio.Future() if __name__ == '__main__': asyncio.run(main()) ``` #### 使用 wscat 测试 首先安装 `wscat`: ```bash npm install -g wscat ``` 然后连接到你的 WebSocket 服务: ```bash #!/bin/bash WDS_BASE_URL="agent.wiseverds.com" WDS_API_KEY="WDS_**************" ProjectID=2231 SandboxID=j4urFhRG ContainerName="mythra-svc-abcd1234-projabcd-sandbox1-XyZ12345" Port=8765 ServiceAddress="${ContainerName}:${Port}" wscat -c "wss://$WDS_BASE_URL/WebIDE/api/v1/projects/$ProjectID/sandboxes/$SandboxID/tcp-socket/$ServiceAddress/http/" \ -H "Authorization: Bearer $WDS_API_KEY" ``` 注意: - WebSocket 使用 `wss://` 协议(加密的 WebSocket) - URL 路径格式与 HTTP 完全相同 - 同样需要提供 `Authorization` 头部进行身份验证 ## 安全机制 ### 容器归属验证 平台会对每次请求进行容器归属验证,确保请求的容器确实属于指定的项目和沙箱。验证逻辑: - 从容器名中提取项目 ID 和沙箱 ID 的缩写(前 8 位) - 与 URL 中的 `project_id` 和 `sandbox_id` 进行匹配 - 匹配失败返回 `403 Forbidden`,防止跨项目/跨沙箱访问 ### 网络隔离 每个项目拥有独立的 Docker 网络(`mythra-project-{ProjectID}`),不同项目的容器之间网络隔离,无法互相通信。 ## 常见问题 ### 如何获取容器名称 你可以通过以下方式获取容器名称: 1. **通过沙箱管理界面**:在 Mythra Agent Studio 的沙箱管理页面查看正在运行的容器列表 2. **通过平台API**:调用沙箱容器列表 API 获取 ### 连接被拒绝 / 502 错误 如果返回 502 错误,可能原因: 1. 容器内的服务未启动或已崩溃——请在容器内检查服务状态 2. 服务监听的端口不正确——请确认服务监听的端口与 URL 中的端口一致 3. 服务只监听 `localhost`(`127.0.0.1`)而非 `0.0.0.0`——请确保服务监听在 `0.0.0.0` 上,以便 Docker 网络内的其他容器可以访问 ### 容器名无效 / 400 错误 如果返回 400 错误,可能原因: 1. `service_address` 格式不正确——确保格式为 `容器名:端口号` 2. 端口号不在有效范围(1-65535) 3. 容器名包含不合法字符 ### 容器不属于当前项目 / 403 错误 如果返回 403 错误,说明容器不属于当前项目或沙箱。请确认: 1. 容器名称是否正确 2. 该容器是否确实是在当前项目和沙箱中创建的 ### 服务没有响应 / 504 错误 如果返回 504 错误,说明请求超时。请检查: 1. 容器是否正在运行 2. 服务是否响应正常 3. 网络连接是否正常 ## 最佳实践 1. **监听所有网络接口**:确保服务监听在 `0.0.0.0` 而非 `127.0.0.1`,否则 Docker 网络中的其他容器(包括 Agent Server)无法访问 2. **使用环境变量配置端口**:使用 `PORT` 等环境变量配置服务端口,提高灵活性 3. **日志记录**:记录请求日志,便于调试和监控 4. **错误处理**:妥善处理各种错误情况,返回有意义的错误信息 5. **健康检查**:为服务添加健康检查端点(如 `/health`),便于排查问题 6. **选择合适的访问方式**:如果是新建服务且对性能有更高要求,可考虑使用 [UNIX Domain Socket 方式](./sandbox-http-service-via-unix-socket.md) ## 相关文档 - [沙箱](./sandbox.md) - 了解沙箱的基本概念和使用方法 - [通过 UNIX Domain Socket 访问容器内服务](./sandbox-http-service-via-unix-socket.md) - 另一种访问容器内服务的方式,性能更高 - [开发外部 AI Agent](./develop-external-agent.md) - 学习如何开发外部 Agent 服务