简介:这是Linphone图片消息中转服务端代码lft.php,面向在局域网、私有服务器或隔离网络环境中部署Linphone通信的开发与运维人员。官方版本依赖外部服务器中转图片,这套重写后的PHP代码可在自有服务器上完成图片消息的接收、处理与转发,彻底摆脱外网连接限制,增强内部通信的数据安全性与可控性,适合企业内网、无外网环境及教学二次开发。资源包为zip格式,共1个文件,即轻量级PHP脚本,压缩包大小仅2KB,部署便捷,可快速嵌入现有SIP/MSRP消息体系,减少额外依赖。已有294人学习下载,具有一定参考价值。内容涵盖图片上传处理、格式与大小校验、转码优化、文件安全策略、身份验证与权限控制、日志记录等关键模块的代码实现,并涉及消息传输协议支持与API扩展设计。读者可据此掌握Linphone图片中转的底层机制,搭建稳定高效的私有中转服务,也可作为功能扩展、安全加固或协议研究的起点。
1. lft.php 在 Linphone 消息图片链路里的角色
用 Linphone 做团队内部协作工具时,最常遇见的不是音质问题,而是消息和图片“发出去就没了”。两端明明都显示在线,却互相收不到内容——因为默认的点对点直连依赖双方网络能互相访问,现实里的 NAT、防火墙和跨网段环境经常让这条通路失效。lft.php 就是为这个场景补位的服务端脚本:一个用 PHP 写成的统一入口,接收客户端上推的消息和图片,暂存在服务器上,等对端来拉取。它是自建 Linphone 消息图片中转服务的关键一环,把“直连失败”变成“服务端兜底”,消息不再依赖双方网络是否连通。这篇文章从最小部署讲起,把路由、存储、鉴权和常见坑一层层拆开,适合正在搭 Linphone 消息服务、或者想把图片上传下载链路从业务里独立出来的开发者。
2. 搭建中转服务:环境准备与最小可跑通的部署动作
中转服务的业务逻辑其实很薄,核心就四件事:收消息、存消息、收图片、发图片。正因为薄,用 PHP 来做往往是性价比最高的选择——部署门槛低,和现有 Web 服务共用一个进程,不需要额外引入进程管理工具,出问题也容易排查。我一般会先把整条链路切成四个 HTTP 接口:POST /push上推文字消息,GET /pull拉取消息,POST /upload上传图片,GET /download下载图片,全部收敛在 lft.php 一个文件里。
2.1 环境选型:PHP 扩展与 Web 服务器的匹配关系
服务端跑的是 PHP 8.0+,因为后面路由部分用到了match表达式。必须装的扩展有三个:pdo_sqlite(连数据库)、fileinfo(校验图片真实类型)、mbstring(处理 URI 里的宽字符)。如果你打算用 MySQL,把pdo_sqlite换成pdo_mysql,建表语句稍作修改即可。
| 配置项 | 推荐值 | 说明 |
|---|---|---|
upload_max_filesize | 10M | 单张图片大小上限,按实际需求调 |
post_max_size | 12M | 必须大于upload_max_filesize,否则图片加上 JSON 头就超了 |
max_execution_time | 30 | 中转服务不应有长事务,30 秒足够 |
client_max_body_size | 15M | nginx 层的限制,必须比 PHP 的post_max_size大 |
这几个参数是最容易埋雷的地方。nginx 默认client_max_body_size只有 1M,图片稍微大一点,在还没到 PHP 之前就被拦下了,返回的 413 会让人误以为是 PHP 配置问题。先做一次环境自检,确认扩展齐全:
# 检查 PHP 版本和依赖扩展 php -v php -m | grep -E 'pdo_sqlite|sqlite3|fileinfo|mbstring' # 确认 php-fpm 的 socket 路径,各发行版不完全一样 ls /run/php/php*-fpm.sock # 创建中转服务目录结构,storage 存图片,data 放 SQLite 库文件 mkdir -p /var/www/lft/storage /var/www/lft/data /var/www/lft/scripts chown -R www-data:www-data /var/www/lft chmod -R 750 /var/www/lft这里有个权限细节:storage目录不要给 777。php-fpm 进程以www-data身份运行,上传的图片由move_uploaded_file写入,文件属主就是www-data,所以目录只给750就够了。给 777 等于把整个存储目录暴露给服务器上的其他进程,完全没有必要。
2.2 nginx 站点配置:把四个接口统一交给 lft.php
Web 服务器选 nginx 还是 Apache 都可以,我习惯用 nginx。核心思路是:除了静态资源请求,其他所有请求都交给 lft.php 处理,由 PHP 脚本内部做路由分发。这样新增接口不需要改服务器配置,只要在 lft.php 里加一个match分支。
server { listen 80; server_name msg.example.com; root /var/www/lft; index lft.php; # 图片中转的上传链路会超过 nginx 默认的 1M 限制,必须调大 client_max_body_size 15m; location / { # 所有请求都落到 lft.php,由 PHP 内部做路由 try_files $uri $uri/ /lft.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } # 图片存储目录禁止直接访问,下载必须走带鉴权的 /download 接口 location /storage/ { deny all; } }try_files的作用是把不存在的 URI 重写到 lft.php,这样POST /push、GET /pull都能进同一个入口。location /storage/的deny all很关键——图片文件本身存在磁盘上,如果不封掉静态路径,别人可以直接拼路径下载,鉴权就成了摆设。下载请求必须走 lft.php 的/download接口,由脚本校验 token 后再输出文件。
2.3 最小跑通验证:先确认路由骨架是通的
在还没写消息逻辑之前,先用 curl 测路由是否通。lft.php 里先放一个最小骨架:
<?php declare(strict_types=1); // 定义 SECRET,用于图片下载 token 签名,生产环境放到 web 目录之外 define('SECRET', 'replace_with_a_random_long_string'); // 统一 JSON 响应格式,避免各接口响应结构不一致 function reply(int $code, string $msg, array $extra = []): void { header('Content-Type: application/json'); echo json_encode(['code' => $code, 'msg' => $msg] + $extra); } // 路由分发:按「请求方法 + 路径」匹配,PHP 8.0+ 的 match 天然只命中一个分支 $uri = rtrim((string)parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'); $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; match ($method . ' ' . $uri) { 'POST /push' => handlePush(), 'GET /pull' => handlePull(), 'POST /upload' => handleUpload(), 'GET /download' => handleDownload(), default => reply(404, 'not found'), };reply函数把响应统一成{code, msg}结构,客户端解析时只需要判断code是否为 0。路由用字符串拼接的方式比较,POST /push这种写法直观,也不会出现数组比较时类型不一致的隐晦问题。现在测试:
# 没写 body 的 push 应该被 400 拦下 curl -i -X POST http://127.0.0.1/push # 不带 user 参数的 pull 应该返回 400 curl -i 'http://127.0.0.1/pull' # 不存在的接口返回 404 curl -i -X DELETE http://127.0.0.1/whatever如果这组请求分别返回 400、400、404,说明 nginx 转发、PHP 入口、路由分发三层都通了。如果返回的是 nginx 的 404 页面而不是 JSON,基本可以确定try_files没生效,检查location /的配置有没有写对。走到这一步,骨架就绪,下面开始填消息转发的核心逻辑。
3. 消息转发核心:JSON 载荷解析与待投递队列的实现
中转服务的消息模型,本质是一个待投递队列。客户端 A 把消息推上来,服务端存进队列,客户端 B 定期来拉取。难点不在“存”和“取”,而在重复消息怎么去重、并发拉取怎么不丢不重、消息拉走后能不能回执。这一章把这三件事一次讲透。
3.1 待投递消息表:用状态机代替读后即删
很多第一次写这个服务的人会直接“读到就删”,理由是消息反正已经发出去了。但真实场景里,客户端 B 可能在拉取后、展示前崩溃,如果消息已经被删,这条消息就永久丢失,发送方还以为是对方已读。正确做法是给消息加状态,用状态机控制生命周期:
| status | 含义 |
|---|---|
| 0 | 待投递,客户端尚未拉取 |
| 1 | 已拉取,客户端已消费但尚未确认 |
| 2 | 已回执,客户端确认收到 |
建表语句用 SQLite 为例,切 MySQL 时把AUTOINCREMENT换成AUTO_INCREMENT:
CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, msg_id VARCHAR(64) NOT NULL, from_uri VARCHAR(128) NOT NULL, to_uri VARCHAR(128) NOT NULL, payload TEXT NOT NULL, status TINYINT NOT NULL DEFAULT 0, pull_batch VARCHAR(32) NOT NULL DEFAULT '', create_time INTEGER NOT NULL, pull_time INTEGER NOT NULL DEFAULT 0 ); -- 拉取查询只走 to_uri + status,联合索引必须建 CREATE INDEX IF NOT EXISTS idx_pull ON messages(to_uri, status); -- msg_id 唯一索引,是幂等兜底的最后一道防线 CREATE UNIQUE INDEX IF NOT EXISTS uq_msg ON messages(msg_id);pull_batch字段是给批量拉取用的批号,后面handlePull会用到,这里先建好。payload存的是客户端 POST 上来的原始 JSON 字符串,不拆开存。这样做的理由有三个:一是消息结构未来加字段不需要改表;二是回执和重放都依赖原始载荷;三是查询时直接转发字符串,省掉一次序列化开销。
3.2 handlePush:幂等入库与载荷校验
客户端上推消息时,必须携带msg_id。这个 ID 由客户端生成,推荐用 UUID,服务端拿它做幂等判断。为什么要幂等?移动网络下客户端 POST 超时后会重试,如果没有去重,同一条消息会入两次库,接收方就会看到两遍。
function handlePush(): void { $raw = file_get_contents('php://input'); $data = json_decode($raw, true); // 必须是合法 JSON,且 from/to/content 三个字段不能缺 if (!is_array($data) || !isset($data['to'], $data['from'], $data['content'])) { reply(400, 'bad json'); return; } // msg_id 缺省时由服务端生成,但客户端最好总是自己传 $msgId = isset($data['msg_id']) ? (string)$data['msg_id'] : bin2hex(random_bytes(8)); $pdo = db(); try { $ins = $pdo->prepare( 'INSERT INTO messages (msg_id, from_uri, to_uri, payload, status, create_time) VALUES (?, ?, ?, ?, 0, ?)' ); $ins->execute([$msgId, $data['from'], $data['to'], $raw, time()]); } catch (PDOException $e) { // 撞了唯一索引,说明是重复消息,返回成功但不重复入库 reply(0, 'dup'); return; } reply(0, 'ok'); }msg_id为空时服务端补一个随机串,这只是兜底,不推荐依赖。因为一旦客户端重试,两个请求的msg_id不同,幂等就失效了。真正保证不重复的是最后那道UNIQUE INDEX,并发场景下两个请求同时插入相同msg_id,数据库会抛唯一索引冲突,被 catch 捕获后返回dup,客户端收到code = 0就知道服务端已经处理过这条消息,不需要再重试。
3.3 handlePull:先标记后返回,避免并发重复消费
拉取接口最容易犯的错是“先 SELECT 再 UPDATE”。两个客户端同时拉取时,可能读到同一条status = 0的消息,然后各自更新状态,结果一条消息被消费了两次。正确顺序是反过来的:先 UPDATE 把消息标记为已拉取,再 SELECT 取回这批消息。我在前面表里留的pull_batch字段,就是为这个场景设计的。
function handlePull(): void { $user = $_GET['user'] ?? ''; if ($user === '') { reply(400, 'user required'); return; } $pdo = db(); $batchId = bin2hex(random_bytes(8)); // 第一步:把待投递的消息标记为本批次,拿到影响行数 $upd = $pdo->prepare( 'UPDATE messages SET status = 1, pull_batch = ?, pull_time = ? WHERE to_uri = ? AND status = 0 ORDER BY id ASC LIMIT 10' ); $upd->execute([$batchId, time(), $user]); if ($upd->rowCount() === 0) { reply(0, 'ok', ['messages' => []]); return; } // 第二步:按批号取回消息,此时这批消息只可能属于当前请求 $sel = $pdo->prepare( 'SELECT msg_id, from_uri, to_uri, payload FROM messages WHERE to_uri = ? AND pull_batch = ? ORDER BY id ASC' ); $sel->execute([$user, $batchId]); header('Content-Type: application/json'); echo json_encode([ 'code' => 0, 'messages' => $sel->fetchAll(PDO::FETCH_ASSOC), ]); }LIMIT 10是单次拉取上限。这个值要控制好:太小则客户端轮询频繁,太大则响应体臃肿、超时风险上升。10 条是在大多数文本消息场景下实测比较舒服的档位。pull_batch用随机串而不是时间戳,是因为同一秒内多次 pull 会用相同时间戳相互污染,随机串没有这个问题。
客户端拿到messages后,每条里的payload还是一个 JSON 字符串,需要再json_decode一次。这样嵌套设计是为了让回执阶段能原样重放原始消息,不丢失任何字段。拉取间隔一般建议 3~5 秒,太短会无谓消耗服务端连接,太长则消息延迟变高。
4. 图片中转:存储目录、访问令牌与下载鉴权
图片和文本消息在存储策略上要分开。文本消息的 payload 本身很小,几十条塞进数据库没感觉;图片不一样,一张照片动不动几 MB,如果 base64 编码后塞进消息表,数据库体积会迅速膨胀,备份变慢,查询变慢,连带着文本消息也遭殃。常见做法是:图片落盘存储,消息表里只存file_id和下载 URL,图片本身走独立的上传下载接口。
4.1 图片为什么不能塞进消息表
把图片塞进数据库有两个直接后果。第一,base64 编码会让体积膨胀约 33%,一张 3MB 的照片变成 4MB 文本,再乘以消息条数,SQLite 单库很快到 GB 级;第二,消息表变大后,to_uri + status的索引扫描成本上升,文本消息的拉取延迟也会被拖累。所以正常的架构是图片和消息分开存储,消息里只带一个引用 ID。
上传接口接收multipart/form-data,字段名叫file,客户端同时需要传一个简短的上下文说明,比如图片属于哪条消息。服务端校验通过后,把文件写入存储目录,返回file_id和带签名的下载 URL。客户端再把字段拼进消息的content里发给对端。
4.2 handleUpload:MIME 白名单与随机文件名
图片类型校验有个常见误区:直接读$_FILES['file']['type']。这个值来自客户端 HTTP 头,是完全可伪造的。正确做法是用finfo读取文件内容的真实 MIME 类型,再和白名单比对。
function handleUpload(): void { $file = $_FILES['file'] ?? null; // 检查是否真的上传了文件,UPLOAD_ERR_OK 才算成功 if (!$file || ($file['error'] ?? 1) !== UPLOAD_ERR_OK) { reply(400, 'upload failed'); return; } // 白名单校验:按文件真实内容判断类型,而不是信 HTTP 头 $allow = ['image/jpeg', 'image/png', 'image/webp']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); if (!in_array($mime, $allow, true)) { reply(415, 'type not allowed'); return; } // 大小上限,这里是 10MB,按业务需要调整 $maxSize = 10 * 1024 * 1024; if ($file['size'] > $maxSize) { reply(413, 'file too large'); return; } // 随机文件名:bin2hex(random_bytes(16)) 生成 32 位十六进制串,不可枚举 $extMap = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp']; $name = bin2hex(random_bytes(16)) . '.' . $extMap[$mime]; $storeDir = __DIR__ . '/storage/'; // move_uploaded_file 只处理真实上传的文件,防止把任意路径搬进来 if (!move_uploaded_file($file['tmp_name'], $storeDir . $name)) { reply(500, 'save failed'); return; } // 返回下载 URL,token 用 md5(file_id + SECRET) 签名 $token = md5($name . SECRET); reply(0, 'ok', [ 'file_id' => $name, 'url' => '/download?id=' . $name . '&token=' . $token, ]); }bin2hex(random_bytes(16))生成的文件名是 32 位十六进制,攻击者无法枚举出其他人的图片路径。扩展名按白名单映射,而不是直接用客户端提供的原始文件名,从源头杜绝了路径穿越。.jpg后缀不能直接用$_FILES里的名字,原因就在这——如果客户端把文件名写成../../config.php,配合错误的移动逻辑,文件可能被写到 web 目录之外。
4.3 handleDownload:下载鉴权与响应头
图片下载必须过 token。token 的签名算法是md5(file_id + SECRET),服务端用相同算法重新算一份,再用hash_equals比较,避免用==比较字符串时的时序攻击。校验通过后,还要检查文件名格式,防止有人在id参数里传../data/lft.db这种路径。
function handleDownload(): void { $id = $_GET['id'] ?? ''; $token = $_GET['token'] ?? ''; // 文件名必须是「32位十六进制 + 白名单扩展名」,其他一律拒绝 if (!preg_match('/^[a-f0-9]{32}\.(jpg|png|webp)$/', $id)) { reply(400, 'bad id'); return; } // hash_equals 防时序攻击,普通比较运算符在这里不推荐 if (!hash_equals(md5($id . SECRET), $token)) { reply(403, 'forbidden'); return; } // 文件经过格式校验,不存在路径穿越风险,直接拼路径 $file = __DIR__ . '/storage/' . $id; if (!is_file($file)) { reply(404, 'not found'); return; } // 按扩展名映射 Content-Type,不要用 finfo 重新识别,保持和存储时一致 $mimeMap = ['jpg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp']; $ext = pathinfo($id, PATHINFO_EXTENSION); header('Content-Type: ' . $mimeMap[$ext]); header('Content-Length: ' . (string)filesize($file)); readfile($file); }Content-Type必须显式设置,否则 PHP 默认输出text/html,浏览器拿到图片数据也会按文本来解析,表现就是“下载了但打不开”。如果图片需要被客户端直接展示而不是下载,可以再补一个Content-Disposition: inline,默认缺省行为也是 inline,只有想强制下载时才用attachment。
还有一个容易被忽视的点:file_id里的md5($id . SECRET)这个 token 是永不过期的。生产环境如果对安全要求高,可以把 token 改成“随机串 + 过期时间”入库校验,每次下载都先查一张 token 表,过期就拒绝。这套简化方案适合内部工具,对外网服务建议升级。
5. lft.php 部署避坑手册:5 个常见翻车现场
中转服务上线后,我整理过一份踩坑记录。下面这 5 个问题是出现频率最高的,每一个都踩出过血泪经验,按“现象 → 原因 → 解决”列出来,可以当排查手册用。
5.1 图片上传必现 413:nginx 层先拦截了
现象:客户端上传图片时立即收到 413,PHP 错误日志里什么都查不到。原因:nginx 的client_max_body_size默认只有 1M,请求还没到 php-fpm 就被拦了。解决:把 nginx 配置里这个值调到 15M 并 reload,同时确认 PHP 的upload_max_filesize和post_max_size都已经同步调大。这三个参数是对齐的,漏一个都会出问题。
# 修改配置后重新加载,不要重启整个 nginx nginx -t && nginx -s reload # 验证:传一张 5MB 的图片,应该返回 JSON 而不是 413 页面 curl -F "file=@/tmp/test.jpg" http://127.0.0.1/upload我一般会刻意测一张比配置上限大 1MB 的图,确认它能稳定返回 413 JSON,而不是 nginx 的 HTML 错误页。这两种 413 的排查路径完全不同,前者看 PHP,后者看 nginx。
5.2 消息被重复消费:先 SELECT 再 UPDATE 的顺序反了
现象:压测时开了两个客户端同时 pull,同一条消息偶尔会被两个客户端同时拉到。原因:两个进程都先 SELECT 到了同一条status = 0的消息,都以为自己拿到了,然后再各自 UPDATE 状态,但消息已经发出去两份了。解决:把 UPDATE 放在 SELECT 前边,用pull_batch批号锁定当前批次,再按批号取回。第三章里的handlePull已经是正确写法。
这个坑之所以隐蔽,是因为单客户端联调时永远复现不了,必须并发访问才暴露。建议上线前用ab或并发脚本多测几次,验证rowCount()加批号锁的行为符合预期。
5.3 上传成功但打不开图片:文件扩展名与 MIME 没有对齐
现象:图片下载下来了,本地预览器打不开,或者图标显示的是文本文件。原因:存储时扩展名和实际内容不一致,或者下载时没设Content-Type。我在早期版本里直接用客户端传的文件名存盘,结果有人传了个改名后的文本文件上来,下载后自然没法预览。解决:存储时按finfo识别出的真实 MIME 决定扩展名,下载时按 mimeMap 映射设置Content-Type,两头都用白名单,不信任任何客户端输入。
5.4 pull 一直返回空:问题大多不在数据库而在轮询链路
现象:push 接口调用成功,返回code = 0,但对端 pull 永远拿到空数组。原因分三种:客户端轮询逻辑没被正确触发,to_uri两端格式不一致(比如一个有sip:前缀一个没有),或者消息已经卡在status = 1没有回滚。解决:先看 nginx access log 确认 pull 请求有没有进来,再比对to_uri的实际值,最后查数据库里status = 1且停留时间超过数小时的消息。
# 看 pull 请求是否真的到达了服务端 tail -f /var/log/nginx/access.log | grep '/pull' # 查卡死的消息 sqlite3 /var/www/lft/data/lft.db \ "SELECT id, to_uri, status, pull_time FROM messages WHERE status = 1;"卡死的消息需要一个回滚任务,把超过一定时间还没收到回执的消息重置回status = 0,这个在下一章给方案。
5.5 并发上传时文件互相覆盖:临时文件名不能当存储名
现象:两个人同时上传图片,后上传的人把先上传的图片覆盖了。原因:有人用$_FILES['file']['tmp_name']或者时间戳当存储文件名,并发场景下会产生相同名字。解决:用random_bytes(16)生成 32 位随机名,碰撞概率可以忽略。另外,如果图片量大到单目录文件数过万,建议在storage下按日期再分一层目录,比如storage/2025/03/,既方便清理也降低单目录 inode 压力。
6. 进阶:把 lft.php 改造成带回执与自动清理的中转节点
基础版的中转只能保证消息“被拉走”,但发送方看不到对方有没有收到。要给链路补上可信度,需要加一个回执接口和一套清理脚本。回执的逻辑很简单:客户端成功解析并展示消息后,调用POST /ack,把msg_id列表传给服务端,服务端把对应消息的status从 1 改成 2。
function handleAck(): void { $data = json_decode(file_get_contents('php://input'), true); $ids = $data['msg_ids'] ?? null; // msg_ids 必须是数组且非空,否则拒绝 if (!is_array($ids) || $ids === []) { reply(400, 'msg_ids required'); return; } // 批量更新,只处理状态已经是 1 的消息,避免越权确认 $pdo = db(); $ph = implode(',', array_fill(0, count($ids), '?')); $upd = $pdo->prepare( "UPDATE messages SET status = 2 WHERE msg_id IN ($ph) AND status = 1" ); $upd->execute($ids); reply(0, 'ok', ['updated' => $upd->rowCount()]); }有了回执,发送方 UI 就能区分“已送达服务端”和“对方已收到”。但要注意,ack只验证msg_id,不验证是谁在确认。如果两个客户端共用同一个账号登录,A 拉取的消息可能被 B 确认,这在大多数内部工具场景可以接受,但如果你在意,就需要把to_uri也加进 UPDATE 条件。
清理脚本用来收尾。图片文件和数据库里过期的消息记录会随使用时间持续膨胀,磁盘迟早被塞满。我的习惯是每天凌晨跑一次,做两件事:删除存储目录中超过 7 天的图片文件;把超过 2 小时仍停留在status = 1的消息回滚到status = 0,交给客户端重新拉取。
# crontab 每天凌晨 3 点执行清理 0 3 * * * php /var/www/lft/scripts/cleanup.php >> /var/log/lft_cleanup.log 2>&1清理脚本里有一个取舍:图片文件删除后,消息表里对应的file_id就指向一个不存在的文件。所以删除顺序必须是“先删消息记录,再删文件”,或者只删那些关联消息已经是status = 2且超过 7 天的文件。我的习惯是保守一点——只清status = 2的关联文件,宁可留几天垃圾,也不让用户看到裂图。
整套服务的验证动作我建议固定成一条 curl 链路:push 一条带图片的消息 → pull 拉取 → ack 确认 → 查数据库确认状态流转。这条链路跑通,中转服务就处于健康状态;跑不通,按第五章节的清单逐层排查。如果你也准备起一个中转服务,最值得投入精力的地方不是路由也不是存储,而是消息状态机的流转逻辑——它是整个服务的可信度基础。希望帮到你。
本文还有配套的精品资源,点击获取