前言
维护老旧业务系统的程序员,多半会撞上一类很难取舍的工程困境:业务逻辑本身稳定可用,仅仅因为编程语言版本升级,原生配套的老旧数据库驱动被官方移除,整个项目就卡在原地,进退两难。
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客户端(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 对公网开放。
网硕互联帮助中心


评论前必须登录!
注册