ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PB远程访问SQL Server:用查询网关解决动态IP与数据库端口暴露难题

PB远程访问SQL Server:用查询网关解决动态IP与数据库端口暴露难题

简介:面向PowerBuilder开发者的远程数据库连接资料包,专门解决动态IP环境下PB客户端无法稳定访问SQL Server的问题。内容涉及ODBC与ADO接口配置、SQL Server远程连接开关、TCP/IP协议及1433端口放通、动态DNS映射等关键技术点,并给出连接池和重试机制建议,适合需要远程维护数据库的PB应用开发者参考。压缩包共25个文件,总体积73KB,主体为pbl、pbt、pbw等PB工程文件,以及srf窗口对象(覆盖ODBC配置、加解密、路径获取等功能),另附bat远程连接测试脚本与js/css网页辅助文件,目录结构清晰。目前已有610人学习浏览。包内含ping远程测试批处理、动态IP下的注册码处理及加解密源码,可帮助读者快速搭建远程连接环境,理解IP变化时的配置与排错思路,是一份小巧实用的参考资源。

1. PB远程连接SQL Server:先绕过动态IP这道坎

做了十来年PB运维,最头疼的一句话不是“系统崩了”,而是“分厂那边连不上总部数据库了”。用PB远程连接SQL Server,看起来就是把连接串里的IP地址从内网换成公网的事,可真放到跨地域、出口IP天天变的宽带上,这招基本是玄学——今天能连,明天分厂那边换个IP段就全部掉线。而 PB 的传统直连方式(OLE DB、ODBC)在广域网上稳定性本身就一般,再加上动态 IP 这个变量,基本就是天天救火。

这篇笔记要拆的这份资源,核心思路不是去跟IP地址死磕,而是用一个“查询网关”把远程连接变成一次HTTP请求:PB 客户端不再直连数据库端口,而是访问一个固定地址的中间层服务,由中间层在服务器内网去访问 SQL Server。这样远程IP怎么变,客户端永远只认中间层那一个地址。这套方案适合三类人:维护老PB系统、被异地分厂连接问题折腾过的工程师,以及准备把C/S架构朝“半B/S”方向改造但不想大动干戈的团队。

2. 方案选型:为什么直连数据库端口不是好主意

2.1 直连模式在动态IP下必然翻车的原因

很多老系统现在还是这么干的:SQL Server 暴露1433端口,路由器上做端口映射,PB 客户端里写Server=公网IP,端口。这个模式在公司有固定公网IP的时候勉强能跑,但一旦落在普通宽带或企业动态专线上,每个月IP地址一变,所有客户端的连接串都得改一遍。更麻烦的是,你没法保证两边的防火墙策略能在IP变动后第一时间同步更新,经常是分厂那边报错,总部这边还一头雾水。

动态IP的核心问题不在于“IP会变”,而在于“变了你不知道”。PB 的数据库接口没有自动重连域名的机制,连接字符串里的IP是写死的。就算你给这条宽带配了DDNS域名,SQL Server 的 TCP/IP 属性里也填不了域名,只能填IP。所以直连方案在动态IP面前,天生就是残废。

2.2 中间层方案选型:WebService、TCP中继还是数据库网关

既然直连不行,市面上能绕开动态IP的思路大致有三类。第一类是 TCP 中继:在固定IP的服务器上部署一个端口转发工具,客户端连中转服务器的固定端口,由它把流量再转到 SQL Server。这个方案能解决IP变动问题,但报文要经过两层转发,SQL Server 的登录认证和数据传输全都裸奔在公共线路上,安全性基本看运气。

第二类是数据库网关中间层,也就是这份资源采用的思路。中间层服务部署在一台能稳定访问 SQL Server 的机器上(通常跟数据库同机房),对外暴露的是一个HTTP地址。PB 客户端通过 HTTP 调用中间层接口,中间层再去执行 SQL 查询并返回数据。这样数据库端口完全不对公网开放,SQL Server 的账号密码也不会下发到客户端,安全性和稳定性都高一个级别。

第三类是内网映射类工具,把SQL Server的端口映射到某个固定域名下。这类工具配置简单,但免费版本普遍限制流量,而且中间节点故障你没法控制,对生产系统来说不稳定因素太多。我一般不推荐在正式环境用这个。

2.3 三类方案的适用边界对比

方案动态IP适应性数据库端口暴露风险实施成本推荐场景
直连+端口映射差,IP变更即断连高,1433直接暴露公网低固定IP且安全要求低的临时场景
TCP中继中,客户端配置固定中,协议裸转中临时应急,不适合长期
中间层WebService好,只认HTTP地址低,数据库不对外中高生产环境长期运行,推荐

中间层方案最大的特点是:客户端只认识一个HTTP地址。这个地址可以是域名、可以是固定IP,反正它不会随着分厂宽带的动态IP变化。代价是你需要一台能同时访问公网和 SQL Server 的机器,公司有云服务器最好,没有的话一台办公室固定IP的机器也能跑。

3. 查询网关实现:中间层搭建与核心参数调节

3.1 网关整体结构与SQL映射配置

这份资源的中间层是一个 ASP.NET Core Web API 项目,结构上分三层:控制器接收PB客户端的HTTP请求,业务层做参数校验和存储过程白名单过滤,数据层只负责跟 SQL Server 交互。为什么不用 PB 自带的 WebService 代理而是用普通 Web API?因为 PB 的 WebService 代理生成麻烦,而且接口一旦变更两边都得重新生成代理。普通 REST 接口用 JSON 传参,PowerBuilder 2019 以后原生支持 JSON 解析,两边维护成本最低。

网关里最关键的文件是proc_map.json,它定义了客户端允许调用的存储过程清单和对应的数据库连接名。这样做有两个好处:第一,PB 客户端永远不能拼接任意 SQL 语句,只能在白名单里选存储过程,防止有人拿这个接口当 SqlTools 用;第二,数据库连接信息可以做到按存储过程分组,比如查询走只读账号,写入操作走另一个权限稍高的账号。

{ "connectionString": "Server=192.168.10.50;Database=ERP_Pro;User Id=gw_reader;Password=xxx;TrustServerCertificate=true", "timeoutSeconds": 30, "maxConcurrentQueries": 20, "procedures": [ { "name": "sp_get_sales_report", "dbUser": "gw_reader", "allowedParams": ["startDate", "endDate", "branchId"] }, { "name": "sp_register_outbound_order", "dbUser": "gw_writer", "allowedParams": ["orderJson", "operator"] } ] }

这段配置的意图很明显:所有客户端发起的查询请求,最终都落到sp_get_sales_report这类存储过程上,而不是直接塞一段SELECT过来。allowedParams的作用是过滤参数名,比如有人传一个tableName参数进来,网关直接拒绝,因为白名单里没有它。maxConcurrentQueries是给网关加了个并发闸门,防止某个分厂跑大报表时把数据库连接池占满,影响总部正常业务。

3.2 核心代码:查询接口与参数化执行

中间层的核心接口就一个:接收存储过程名和参数字典,然后执行并返回JSON。下面是控制器里最关键的ExecuteQuery方法的精简实现:

[HttpPost("api/query/execute")] public async Task<IActionResult> ExecuteQuery([FromBody] QueryRequest req) { if (!_config.Procedures.TryGetValue(req.ProcName, out var proc)) return BadRequest(new { error = "procedure_not_allowed", message = "存储过程不在白名单中" }); var extraParams = req.Params.Keys.Except(proc.AllowedParams).ToList(); if (extraParams.Any()) return BadRequest(new { error = "invalid_params", message = $"不允许的参数: {string.Join(",", extraParams)}" }); using var conn = new SqlConnection(proc.GetConnectionString(_config.ConnectionString)); using var cmd = new SqlCommand(proc.Name, conn) { CommandType = CommandType.StoredProcedure, CommandTimeout = _config.TimeoutSeconds }; foreach (var kv in req.Params) cmd.Parameters.AddWithValue("@" + kv.Key, kv.Value ?? DBNull.Value); var dt = new DataTable(); using (var da = new SqlDataAdapter(cmd)) da.Fill(dt); var json = JsonConvert.SerializeObject(dt, new JsonSerializerSettings { DateFormatString = "yyyy-MM-dd HH:mm:ss", NullValueHandling = NullValueHandling.Ignore }); return Ok(new { data = json }); }

这段代码解决了三个关键问题。第一是白名单校验,proc.AllowedParams把传参范围锁死,就算有人知道接口地址,也传不进SELECT语句。第二是参数化查询,所有输入都用AddWithValue绑定,杜绝拼接SQL的注入风险。第三是超时控制,CommandTimeout从配置读,防止某个烂报表挂起几分钟拖死SQL Server。我一般会建议把DefaultCommandTimeout设成 10 秒,报表类查询超过 10 秒就该去优化存储过程了,而不是指望靠客户端多等。

3.3 部署与运行参数调节要点

网关部署在带固定IP的服务器上,数据库连接串里的Server=192.168.10.50是内网地址,公网完全访问不到这台 SQL Server。部署时注意三个参数:Kestrel 的监听端口(默认5000,生产我一般用反向代理把她挂到80或443端口下)、timeoutSeconds(建议10-30,别超过60)、maxConcurrentQueries(这个要看数据库的连接池上限,通常 20 左右就够)。

连接池参数也很关键。SQL Server 的连接复用是中间层性能的命脉,我在部署脚本里加了连接池的常规配置:Pooling=True; Min Pool Size=5; Max Pool Size=50。Min Pool Size=5的意思是服务启动后预建 5 个物理连接,避免第一个分厂请求进来时现建连接导致的首包延迟。这个数字调大能减少冷启动延迟,但会多占 SQL Server 的许可连接数,自己权衡。

4. PB客户端接入:HTTP调用与DataWindow回填

4.1 PB侧HTTP调用封装

PB 这边的核心工作是把 HTTP 请求封装成一个通用函数,任何窗口需要查数时直接调用。PowerBuilder 2019 R3 以后提供了HttpClient对象,但老项目如果还在用 PB 12 之前的版本,那就得用WinHTTP或者拼一个OLEObject调 MSXML2.ServerXMLHTTP。这里给一个用内置HttpClient的写法:

// 通用网关查询函数, 返回JSON字符串 // ls_procName: 存储过程名; lparams: 参数数组; ls_conditions: 参数值数组 global function string gf_query_gateway(string ls_procName, string lparams[], string ls_conditions[]) { JsonObject lj_params JsonObject lj_body lj_params = create JsonObject lj_body = create JsonObject // 组装参数体, 例如 {"startDate":"2024-01-01","branchId":"1001"} lj_body.set("procName", ls_procName) lj_params = JsonObject() for i = 1 to upperbound(lparams) lj_params.set(lparams[i], ls_conditions[i]) next lj_body.set("params", lj_params) HttpClient lh_client HttpResponse lh_resp lh_client = create HttpClient lh_client.setRequestHeader("Content-Type", "application/json") lh_resp = lh_client.post("http://你的网关地址/api/query/execute", lj_body.toString()) if lh_resp.statusCode = 200 then return lh_resp.body else return "ERROR|" + string(lh_resp.statusCode) + "|" + lh_resp.body end if }

PB 的JsonObject在 2017 版本以后才比较稳定,老版本如果 API 对不上,可以用字符串拼接的方式构造 JSON 请求体,只是要注意转义。返回的lh_resp.body是网关注入的 JSON 字符串,里面嵌套了一个data字段,真正的数据行都在那里面。调用这个函数时,PB 侧所有数据库连接对象都不用创建了,自然也就不存在“连接串写死IP”的问题。

4.2 返回结果回填DataWindow

拿到 JSON 串之后,很多人习惯自己写循环解析,这其实是绕远路。DataWindow 有一个ImportJson方法,可以直接把 JSON 数据灌进 DataWindow 的临时表里,前提是列结构能对得上。下面是回填 dw_report 的完整流程:

// 调用网关查询 string ls_json string ls_procName = "sp_get_sales_report" string lparams[] = {"startDate", "endDate", "branchId"} string ls_vals[] = {"2024-01-01", "2024-12-31", "1001"} ls_json = gf_query_gateway(ls_procName, lparams, ls_vals) if left(ls_json, 5) = "ERROR|" then MessageBox("查询失败", ls_json) return end if // 提取data字段的JSON数组部分 JsonObject lj_resp lj_resp = create JsonObject lj_resp.fromstring(ls_json) string ls_data = lj_resp.getstring("data") // 导入dw_report dw_report.setredraw(false) dw_report.reset() integer li_rtn = dw_report.ImportJson(ls_data) dw_report.setredraw(true) if li_rtn < 0 then MessageBox("导入失败", "ImportJson返回码: " + string(li_rtn)) end if

ImportJson的返回码是有讲究的。返回0表示成功,负数则说明列映射失败,比如 DataWindow 里没有对应的列名、列类型不匹配。最常见的问题是日期列:SQL Server 的datetime传到 JSON 里默认带T分隔符(比如2024-01-01T08:30:00),PB 的ImportJson在部分版本里解析不了这种格式。所以我在网关联的DateFormatString里强制改成yyyy-MM-dd HH:mm:ss,用空格分隔,两边实测都稳定。

DataWindow 的列名必须和存储过程返回的列名一致,否则ImportJson不会报错但数据大量为空。这个坑我踩过不止一次,所以建议在网关返回数据前,让存储过程统一用AS别名把列名改成和 DataWindow 一致。

5. 避坑记录:常见问题与排查顺序

5.1 现象:HTTP能通,但返回数据全是乱码

现象:用浏览器直接访问网关接口,返回的 JSON 中文全变成了????或者乱码。原因:多半不是网关代码问题,而是中间层服务器上的 SQL Server 和 Web API 的字符集不一致,或者 PB 客户端请求头没带对编码。SQL Server 的varchar存的是 GBK,但 Web API 输出时按 UTF-8 序列化,自然就乱了。解决:一律把数据库字段统一成nvarchar,连接串里加CharacterSet=UTF8(如果用的驱动支持),Web API 的响应头加一句response.Headers["Content-Type"] = "application/json; charset=utf-8"。PB 侧HttpClient拿到响应后按 UTF-8 解析,基本能根治。

5.2 现象:连接池被占满,分厂一跑报表总部就卡死

现象:多个分厂同时执行半天下不来的报表查询,总部自己的操作也开始超时。原因:网关的maxConcurrentQueries设得太高,或者中间层跑在了SqlDataReader流式读取模式上没及时关闭连接。等于是把 SQL Server 的 worker 线程全占了。解决:把maxConcurrentQueries从 50 改成 20,并且确保每个查询用 DataAdapter 一次性Fill到 DataTable,不要在连接未关的情况下慢慢遍历。给网关加个最简单的信号量锁,超出并发直接返回“系统繁忙请稍后重试”,别让请求堆积在数据库层。

5.3 现象:分厂换宽带后连不上,但奇怪的是总部一切正常

现象:某分厂反馈客户端报“连接超时”,但总部分厂两边的网络都通,ping 网关地址也通,HTTP 接口手动访问也正常。原因:中间层的防火墙或反向代理配置里,只允许了总部出口IP访问,没给分厂新IP段放行。解决:这是典型的运维惯性坑。建议网关入口不要按IP白名单限制,如果一定需要白名单,那就绑网关所在服务器的安全组,而不是反向代理层。动态IP场景下,在代理层写死IP白名单,等于隔三差五要改配置。

5.4 现象:PB返回“参数个数不匹配”

现象:PB 客户端调用gf_query_gateway,网关返回“存储过程参数不匹配”。原因:PB 的数组是从1开始计数的,而lj_params.set在设置空参数时可能跳过了某些元素,导致 JSON 参数个数对不上存储过程的声明。解决:在 PB 的gf_query_gateway里,遇到空参数也要显示设置lj_params.set(lparams[i], ""),不要continue跳过。存储过程侧把参数设为NULL而不是空字符串,SQL 里用ISNULL(@param, '')做一层兜底。

6. 进阶验证技巧:一分钟定位链路问题那段血泪经验

这套资源里还带了一个自检存储过程和一个配套的 PB 测试窗口,专门用来快速判断故障在哪一层。自检过程很简单,它不接受任何参数,直接返回服务器的当前时间、客户端IP、数据库版本和连接状态:

create procedure dbo.sp_gateway_ping as begin set nocount on select getdate() as server_time, convert(varchar(15), connectionproperty('client_net_address')) as client_ip, @@version as db_version, 'OK' as status end

PB 侧测试窗口拿到返回值后,分别检查三件事:server_time是否接近本机时间(判断中间层服务器有没有时区或系统时间偏差),client_ip是否暴露了真实内网地址(确认中间层连接的不是公网某个不可达的库),db_version是否能正常返回(验证 SQL Server 连接串是否有效)。

我每次给新分厂部署客户端时,都会强制走一遍整个流程:先让现场同事打开测试窗口执行sp_gateway_ping,能返回OK再看数据查询;如果OK都回不来,直接判断是网关到数据库这一段的问题,而不是让业务人员去瞎猜网络。这个习惯帮我在好几次项目交付现场省下了大半天排查时间。从那以后,我每上一个新节点,第一件事就是先跑一遍自检再谈别的。希望这份资源里的思路和脚本,也能帮你把远程连接从玄学变成例行操作。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进