云计算百科
云计算领域专业知识百科平台

数据库轻量RPC网关兼容web跨版本交互-东方仙盟

前言

维护老旧业务系统的程序员,多半会撞上一类很难取舍的工程困境:业务逻辑本身稳定可用,仅仅因为编程语言版本升级,原生配套的老旧数据库驱动被官方移除,整个项目就卡在原地,进退两难。

PHP 版本迭代向来激进。PHP8 等高版本拥有完善的安全补丁,更适合对外承载业务,但官方已经彻底移除了 mssql 系列旧驱动;而不少企业现存的 SQL Server 老库,仅能依靠 PHP5.6 环境下的 sqlsrv 扩展稳定访问。

这里有一个非常关键的工程红线必须放在最前面说明:低版本 PHP 本身存在大量已公开的安全漏洞,如果业务环境逼不得已要保留旧版本、老旧驱动,承载驱动的服务端口绝对不能对外开放,仅允许内网或者本机回环访问。 旧版本没有后续安全补丁,一旦端口暴露在公网,很容易被探测、利用,进而造成数据库泄露、服务器沦陷。本方案默认监听 127.0.0.1,正是出于这个安全考量。

常规方案只有两条路:投入不小成本重构业务代码适配新驱动,或者停留在低版本 PHP 直接对外提供服务,持续暴露安全风险。两种方案都有明显短板。

本文基于适配器模式(Adapter)与中介者模式(Mediator),搭配一套自研 TCP-JSON 轻量 RPC 远程调用机制,实现这个数据库中介网关。它不需要大规模改造上层业务,业务侧可以继续使用高版本 PHP,并且协议本身不绑定 PHP,Java、Go、Python 等其他语言都可以作为客户端接入调用。

它不只是一个临时的兼容脚本,在 RPC 网关这一层,我们内置了 Token 授信、表级读写权限隔离、调用频次限流、访问计数统计、事务会话管理等管控能力。

一、两种设计模式的定位(分开阐述,不强行混为一谈)

适配器模式(Adapter)

适配器模式的核心作用:将一个类的接口转换成客户希望的另外一个接口,使得原本因为接口不兼容而不能一起工作的那些类可以一起工作。

放到本项目里:上层业务(PHP8)想要执行 SQL 访问 SQL Server,但是新版环境缺少对应的驱动接口,无法直接发起数据库调用。低版本 PHP5.6 进程携带可用的 sqlsrv 驱动,充当适配器,对外输出一套统一的调用接口。上层客户端不用感知底层驱动差异,统一走 RPC 接口发起请求,由适配器层完成底层数据库驱动的适配。

简单来说:适配器负责抹平接口、驱动、版本之间的不兼容。

中介者模式(Mediator)

中介者模式定义:用一个中介对象来封装一系列对象的交互,使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

传统直连架构是网状依赖:每一个业务客户端都直接连接数据库,客户端持有数据库账号、地址,多接入方同时访问的时候,依赖关系杂乱,权限、审计逻辑散落在各个业务代码里,很难统一管控。 我们增加网关作为中介者:所有客户端不再直接和数据库通信,全部 RPC 请求统一发给中介网关。鉴权、限流、表权限校验、会话维护、结果回传全部收拢在中介内部处理。客户端和数据库彻底解耦,新增接入方的时候,只需要在网关配置授信信息,不需要改动数据库和其他客户端。

简单来说:中介者收拢多方通信,解决网状依赖混乱的问题。

两者分工清晰:适配器解决 “能不能调用” 的兼容问题;中介者解决 “怎么管控通信” 的解耦问题。再依托 RPC 远程调用,实现跨进程的请求传递。

二、架构优势

  • 跨语言 RPC 服务,不限于 PHP 通信协议为 TCP+JSON,任意语言只要可以创建 Socket 收发 JSON 报文,都可以作为 RPC 客户端接入。这套网关不止解决 PHP 高低版本兼容,其他语言想要访问这套老旧 SQL Server,都可以复用。
  • 数据库敏感信息隔离,严格收敛攻击面 业务客户端不保存数据库 IP、账号、密码,凭证只保存在中介网关。就算业务站点被攻陷,攻击者无法拿到数据库接入凭证。网关默认监听127.0.0.1,不对外开放端口,规避低版本 PHP 本身的安全缺陷。
  • 细粒度权限控制,强于数据库原生权限 原生数据库权限最小粒度一般到整个库。本 RPC 网关在应用层增加管控:Token 作为接入凭证、按 Token 限制每日调用次数、精确到单表区分读、写权限,可以约束客户端仅能查询指定数据表,禁止写入或者访问核心表。
  • 调用计数原生支持按量统计 每一次 RPC 调用都会统计次数,天然支持调用对账、多租户按量计费,这是原生数据库本身不具备的能力。
  • 完整事务会话支持 很多简易转发工具只支持单条 SQL,无法跑事务。网关维护独立会话 ID,同一个事务的多次 RPC 请求复用同一个数据库连接,完整支持 begin /exec/commit /rollback。
  • 三、整体架构链路

    任意语言RPC客户端(PHP8 / Java / Go / Python)
    ↓ 内网TCP JSON RPC请求
    PHP5.6 中介网关(中介者角色:鉴权、限流、权限、会话管理)
    ↓ sqlsrv驱动适配(适配器角色,抹平版本驱动差异)
    SQL Server数据库

    安全说明:PHP5.6 网关进程仅开放本机回环地址监听,不监听 0.0.0.0,禁止公网直接访问。

    四、源码实现

    4.1 权限内核 ApiAuthor.php

    负责 Token 校验、频次限流、数据表权限判断、调用计数,是中介网关的管控层:

    <?php
    class ApiAuthor
    {
    // 授信token白名单
    private static $tokenWhiteList = [
    "DbAgent_2026_Secret_001"
    ];

    // 表级权限配置,区分读、写
    private static $tableRule = [
    "DbAgent_2026_Secret_001" => [
    "read" => ["user", "log", "goods"],
    "write" => ["temp_data"]
    ]
    ];

    // 单日最大调用上限
    private static $dayMaxLimit = 999999;

    // 调用计数缓存
    public static $statics = [];

    public static function checkToken($token)
    {
    return in_array($token, self::$tokenWhiteList);
    }

    public static function checkFlowLimit($token)
    {
    $key = $token . "_" . date("Ymd");
    !isset(self::$statics[$key]) && self::$statics[$key] = 0;
    self::$statics[$key]++;
    return self::$statics[$key] <= self::$dayMaxLimit;
    }

    public static function checkTableAuth($token, $sql)
    {
    $sql = strtolower(trim($sql));
    $rule = self::$tableRule[$token] ?? ["read" => [], "write" => []];

    $isRead = str_starts_with($sql, "select");
    $isWrite = in_array(true, [
    str_starts_with($sql, "insert"),
    str_starts_with($sql, "update"),
    str_starts_with($sql, "delete")
    ]);

    preg_match("/(from|into|update)\\s+([a-z0-9_]+)/", $sql, $match);
    $table = $match[2] ?? "";

    if ($isRead) {
    return $rule["read"] === ["*"] || in_array($table, $rule["read"]);
    }
    if ($isWrite) {
    return in_array($table, $rule["write"]);
    }
    return true;
    }

    public static function getTodayCount($token)
    {
    return self::$statics[$token . "_" . date("Ymd")] ?? 0;
    }
    }

    4.2 RPC 中介网关服务端(php5.6 常驻运行)

    同时承载适配器、中介者双重角色:接收 RPC 请求、统一校验权限、通过 sqlsrv 适配驱动访问数据库、维护事务会话池,超时自动回滚防止锁表。

    重点:代码里写死监听 127.0.0.1,不要改成 0.0.0.0。

    <?php
    require_once "ApiAuthor.php";

    $server = stream_socket_server("tcp://127.0.0.1:9006", $errno, $errstr);
    if (!$server) exit("网关启动失败:{$errstr}");
    echo "数据库RPC中介网关启动成功 | 监听 127.0.0.1:9006\\n";

    $sessionPool = [];
    $sessionTimeout = 30;

    while ($conn = stream_socket_accept($server)) {
    $rawData = fgets($conn, 65536);
    $req = json_decode(trim($rawData), true);
    $res = ["ok" => 0, "msg" => "请求异常"];

    if (empty($req["token"]) || empty($req["cmd"])) {
    fwrite($conn, json_encode($res) . "\\n");
    fclose($conn);
    continue;
    }

    $token = $req["token"];

    if (!ApiAuthor::checkToken($token)) {
    $res["msg"] = "客户端Token未授信";
    fwrite($conn, json_encode($res) . "\\n");
    fclose($conn);
    continue;
    }

    if (!ApiAuthor::checkFlowLimit($token)) {
    $res["msg"] = "当日RPC调用频次已超限";
    fwrite($conn, json_encode($res) . "\\n");
    fclose($conn);
    continue;
    }

    if (!empty($req["sql"]) && !ApiAuthor::checkTableAuth($token, $req["sql"])) {
    $res["msg"] = "没有该数据表的操作权限";
    fwrite($conn, json_encode($res) . "\\n");
    fclose($conn);
    continue;
    }

    $cmd = $req["cmd"];
    $sid = $req["sessionId"] ?? "";

    if ($cmd === "begin") {
    $dbcfg = [
    "UID" => $req["dbuser"],
    "PWD" => $req["dbpwd"],
    "Database" => $req["dbname"],
    "ConnectionPooling" => true
    ];
    $dbh = sqlsrv_connect($req["dbhost"], $dbcfg);
    if (!$dbh) {
    $res["err"] = sqlsrv_errors();
    } else {
    $sid = uniqid("sess_", true);
    $sessionPool[$sid] = [
    "handle" => $dbh,
    "lasttime" => time()
    ];
    sqlsrv_begin_transaction($dbh);
    $res = ["ok" => 1, "sessionId" => $sid];
    }
    } elseif ($cmd === "exec" && !empty($sid) && isset($sessionPool[$sid])) {
    $sessionPool[$sid]["lasttime"] = time();
    $stmt = sqlsrv_query($sessionPool[$sid]["handle"], $req["sql"], $req["param"] ?? []);
    if (!$stmt) {
    $res["err"] = sqlsrv_errors();
    } else {
    $rows = [];
    while ($r = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
    $rows[] = $r;
    }
    $res = ["ok" => 1, "data" => $rows];
    sqlsrv_free_stmt($stmt);
    }
    } elseif ($cmd === "commit" && !empty($sid) && isset($sessionPool[$sid])) {
    sqlsrv_commit($sessionPool[$sid]["handle"]);
    sqlsrv_close($sessionPool[$sid]["handle"]);
    unset($sessionPool[$sid]);
    $res = ["ok" => 1];
    } elseif ($cmd === "rollback" && !empty($sid) && isset($sessionPool[$sid])) {
    sqlsrv_rollback($sessionPool[$sid]["handle"]);
    sqlsrv_close($sessionPool[$sid]["handle"]);
    unset($sessionPool[$sid]);
    $res = ["ok" => 1];
    } elseif ($cmd === "single") {
    $dbcfg = [
    "UID" => $req["dbuser"],
    "PWD" => $req["dbpwd"],
    "Database" => $req["dbname"],
    "ConnectionPooling" => true
    ];
    $dbh = sqlsrv_connect($req["dbhost"], $dbcfg);
    if (!$dbh) {
    $res["err"] = sqlsrv_errors();
    } else {
    $stmt = sqlsrv_query($dbh, $req["sql"], $req["param"] ?? []);
    if (!$stmt) {
    $res["err"] = sqlsrv_errors();
    } else {
    $rows = [];
    while ($r = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
    $rows[] = $r;
    }
    $res = ["ok" => 1, "data" => $rows];
    sqlsrv_free_stmt($stmt);
    }
    sqlsrv_close($dbh);
    }
    } else {
    $res["msg"] = "未知RPC指令";
    }

    $res["todayCount"] = ApiAuthor::getTodayCount($token);
    fwrite($conn, json_encode($res) . "\\n");
    fclose($conn);

    // 清理超时未提交的事务,自动回滚防锁表
    foreach ($sessionPool as $k => $v) {
    if (time() – $v["lasttime"] > $sessionTimeout) {
    sqlsrv_rollback($v["handle"]);
    sqlsrv_close($v["handle"]);
    unset($sessionPool[$k]);
    }
    }
    }
    fclose($server);

    4.3 RPC 客户端示例(PHP8 版本)

    客户端调用如同本地函数,底层封装 Socket RPC 请求,不需要任何 sqlsrv 驱动:

    <?php
    $addr = "127.0.0.1:9006";
    $clientToken = "DbAgent_2026_Secret_001";

    function rpcSend($pkg)
    {
    global $addr, $clientToken;
    $pkg["token"] = $clientToken;
    $fp = stream_socket_client("tcp://" . $addr, $ec, $em, 15);
    if (!$fp) return false;
    fwrite($fp, json_encode($pkg) . "\\n");
    $resp = fgets($fp, 65536);
    fclose($fp);
    return json_decode($resp, true);
    }

    function dbQuery($dbcfg, $sql, $param = [])
    {
    return rpcSend([
    "cmd" => "single",
    "dbhost" => $dbcfg["host"],
    "dbuser" => $dbcfg["user"],
    "dbpwd" => $dbcfg["pass"],
    "dbname" => $dbcfg["dbname"],
    "sql" => $sql,
    "param" => $param
    ]);
    }

    // 使用示例
    $cfg = [
    "host" => "127.0.0.1",
    "user" => "sa",
    "pass" => "xxx",
    "dbname" => "testdb"
    ];
    $ret = dbQuery($cfg, "select top 10 * from user", []);
    var_dump($ret);

    五、结语

    适配器和中介者,在工程实践里经常一起配合使用,但二者职责完全不同,不能混淆。适配器解决接口兼容,中介者解决通信耦合,再搭配轻量 RPC,刚好落地这个老旧数据库兼容场景。

    很多开发者提到 RPC 第一反应就是 gRPC、Thrift 这类成熟重型框架。但在内网闭环、需求稳定的场景,自研一套极简 RPC 足够好用,没有额外代码生成、序列化组件的依赖。

    这套方案代码量精简、逻辑可控,适合老旧系统平滑升级、多语言共享一套存量数据库。它既是一次驱动兼容的工程实践,也是适配器、中介者两种设计模式从理论走向线上生产的落地案例。 再次重申安全底线:承载老旧 PHP 版本的网关,只允许内网 / 本机回环访问,严禁监听 0.0.0.0 对公网开放。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 数据库轻量RPC网关兼容web跨版本交互-东方仙盟
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!