简介:面向使用 VS Code 连接 SQL Server 的数据库开发与运维人员,这份离线压缩包针对 mssql 扩展因 GitHub 访问受限而缺少 Microsoft.SqlTools.ServiceLayer、数据库无法连接的问题,提供了可直接替换的完整服务层。资源按 win-x64 与 .NET 8.0 构建,共含 823 个文件、约 76.46MB;主体为 748 个 DLL,覆盖连接管理、SQL 解析、架构对比等核心能力,另有 EXE 启动程序、JSON/XML 配置、RESX 资源文件及少量前端交互片段,可整体补齐扩展内的 sqltoolsservice 目录。对受网络限制的国内用户而言,下载后即可获得与已安装扩展版本匹配的 SqlTools 服务层,无需再绕行 GitHub,也便于离线部署与后续排错;包内还附带 PDB 调试符号,可辅助定位扩展调用异常。发布至今已有 418 人学习,适合遇到 SQL Server 连接失败或希望预置工具服务的 VS Code 用户。
1. 为什么你连 SQL Server 时卡在转圈,日志里全是 Microsoft.SqlTools.ServiceLayer
你大概率遇到过这个场景:SQL Server 明明能 ping 通,sqlcmd 也能进,可一到 VS Code 或 Azure Data Studio 里点连接,状态栏就一直转圈。翻日志,路径前缀清一色写着 Microsoft.SqlTools.ServiceLayer-win-x64-net8.0.zip。这个看起来像安装包的东西,其实是 SQL 工具链里真正负责连接、解析、执行查询的本地服务层。win-x64 标明了平台,net8.0 标明了 .NET 运行时版本;它不是普通客户端,而是被扩展拉起、常驻在你机器上的独立进程。这篇要解决的,就是把 zip 变成你能主动控制的服务:排查转圈、验证端口、配置参数、避开六个常见坑。适合排查 mssql 扩展连接问题的开发者,也适合想自建轻量 SQL 工具的工程师。
2. 服务层的真实分工:为什么你的工具需要一个 zip 包里的独立进程
2.1 SqlTools 要解决的问题:避免每个工具重复造数据库轮子
如果 VS Code 扩展、Azure Data Studio、Jupyter 都直接引用 SMO 去连库,每个人维护一套连接管理、结果集缓存、脚本解析逻辑,结果就是内存翻倍、行为不一致。微软的做法是把这些能力下沉到一个独立进程里,工具只通过 JSON-RPC 和它对话。SqlTools Service 包含连接管理、元数据查询、脚本 IntelliSense、查询执行和结果集序列化,而 ServiceLayer 是这个服务最核心的入口程序集。
你在包名里看到的 Microsoft.SqlTools.ServiceLayer,就是服务主体所在程序集的名字。它平时由 IDE 扩展自动拉起,所以你感觉不到它的存在;真正排查问题时,你又会发现它的日志散落在临时目录里,路径奇长,内容还夹杂 JSON-RPC 帧。把它当成一个黑匣子去用也行,但一旦要调参、排错、做二次集成,就必须知道它内部怎么分工。
2.2 和 SMO、JDBC、sqlcmd 的本质区别
很多人误以为 ServiceLayer 就是微软官方驱动。其实它比驱动高一层。下表对比四种常见方案:
| 方案 | 交互方式 | 资源占用 | 适用场景 |
|---|---|---|---|
| SMO | 直接引用程序集,进程内调用 | 高,每个进程一份 | 写复杂管理脚本、做数据库迁移工具 |
| JDBC / ADO.NET | 驱动直连 SQL Server | 低,但无会话服务 | 业务代码访问数据库 |
| sqlcmd | 命令行,一次性执行 | 极低 | 手动排错、批处理 |
| SqlTools Service | 本地服务,JSON-RPC 调用 | 中,多工具共享一份 | IDE 连接、自建 SQL 客户端、协议级调试 |
SMO 的问题在于它默认趴在你进程里,GC 压力和程序集版本冲突都要你背着。JDBC 直连又快又稳,但每个工具都要自己实现连接池、查询超时、结果集分页,运维侧很难统一。sqlcmd 可以用来验证网络和权限,但它不提供协议级的会话复用。ServiceLayer 把连接池、会话状态、结果集格式化都放到了独立进程里,你发的每条 JSON-RPC 请求只描述“要做什么”,不关心底层驱动状态。
正因如此,它才值得用独立 zip 发布。VSCode 的 mssql 扩展、Azure Data Studio 都能复用同一个服务,升级服务层时也不用升级整个 IDE 宿主。
2.3 win-x64 与 net8.0 在命名里暗示的部署形态
包名里的 win-x64 是 .NET 运行时标识符(RID),net8.0 是目标框架。这两个字段组合在一起,直接告诉你两件事:这个包只能在 Windows x64 上跑;它需要 .NET 8.0 运行时的支撑。
不过“需要运行时”有两种解读。如果包是 framework-dependent 部署,机器上必须先装 .NET Runtime 8.0,否则跑不起来;如果包是 self-contained,dll 旁边会带 hostfxr.dll、coreclr.dll 等原生文件,解压即用。这里面最常见的误判是:你看包里有 dll,就以为它自带运行时,结果双击 exe 一闪而过,换成 dotnet 命令启动又报“框架未找到”。我一般会先看根目录有没有 runtimeconfig.json,再看里面滚动的 version 字段,这比猜体积可靠得多。
从维护视角看,win-x64 + net8.0 这种命名还有一层含义:要和别的平台版本分开存放。很多团队只在 Windows 上装了 mssql 扩展,下载时顺手选了 win-x64,后来想把这个服务搬到 Linux 容器里做自动化,才发现得重新下载 linux-x64 包,否则直接抛 BadImageFormatException。
2.4 解压后的完整性检查:先搞清楚包是 exe 启动还是 dll 启动
拿到 zip 后不要急着双击。先解压到固定目录,然后做一次目录检查。用 PowerShell 看根目录文件,重点找入口程序集和运行时配置文件:
Get-ChildItem -Path . -Recurse -Depth 1 | Select-Object FullName, Length | Format-Table -AutoSize观察结果里是否有这几类文件:
- Microsoft.SqlTools.ServiceLayer.dll:托管入口。如果没有 exe,就是用 dotnet 命令启动它。
- Microsoft.SqlTools.ServiceLayer.exe / .dll 同名的原生宿主:说明包已经带好自托管入口。
- runtimeconfig.json:标记 framework-dependent 还是 self-contained。里面通常有一行 framework 版本 8.0。
- hostfxr.dll:如果出现,说明这是 self-contained,不需要本机装 .NET。
注意:同一个目录下如果有大量 Newtonsoft.Json、SQLite 等依赖 dll,这是正常现象。真正决定运行方式的是入口程序集和运行时配置,不是依赖数量。
这套检查做完,你就知道下一步该用 exe 还是 dotnet 命令。很多人第一次翻车,就是因为跳过这步,默认拿 exe 双击,最后对着空窗口怀疑人生。
3. 把 zip 变成可复现的本地 SQL 服务:启动、验证、跑通一条查询
3.1 环境预检:先确认 net8.0 运行时和端口空闲
在启动前做两件小事:确认运行时存在,确认默认端口没被占用。SqlTools Service 默认监听本机 127.0.0.1 的 13140 端口,如果被占用,它会尝试 13141、13142 等后续端口。端口不是靠猜的,日志里会写清楚。
dotnet --list-runtimes | findstr "8.0" Get-NetTCPConnection -LocalPort 13140 -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, State, OwningProcess第一条命令看运行时版本。如果结果里有 8.0.x,环境达标;如果只有 7.0 或 9.0,直接启动会报“所需的框架版本低于 X”。第二条命令查 13140 是否已被占用。查出来的 OwningProcess 如果不是你的项目,说明另一个工具实例占着端口,后面要手动指定 --port。
这两步看起来啰嗦,但能过滤掉 80% 的“启动失败”。我见过有人跳过预检,直接跑服务,结果日志被覆盖后根本看不出是端口冲突。先确认环境,再谈排错。
3.2 前台启动:第一次启动一定要盯住输出
进入解压目录,用命令行前台启动。如果包内有 exe,直接:
./Microsoft.SqlTools.ServiceLayer.exe --enable-logging --log-level DEBUG --log-dir ./logs如果包内没有 exe,只有 dll,则用 dotnet:
dotnet Microsoft.SqlTools.ServiceLayer.dll --enable-logging --log-level DEBUG --log-dir ./logs参数说明:--enable-logging 强制写日志,不发环境变量也能落盘;--log-level DEBUG 把连接握手、JSON-RPC 帧都记录下来,第一次调试建议用它;--log-dir ./logs 把日志写到当前目录,方便随时翻看。不要图省事省略日志参数,因为服务一旦被 IDE 启动,默认日志位置在临时目录,找起来很痛苦。
前台启动的好处是 ctrl+c 能停掉,你能直接看到 stdout 里的异常堆栈。正常启动后,日志会出现类似 Listening on 127.0.0.1:13140 的字样,说明服务已经进入等待状态。这时不要关窗口,留着它做下一步验证。
3.3 最小探测:用 JSON-RPC 验证服务进程真的能接请求
服务没有提供“Hello World”网页,访问根路径只会得到 404。真正验证它,要发一条 JSON-RPC 消息。我习惯用 Python 写最小客户端,一次请求验证端口通、消息解析通、服务可响应:
import json import socket payload = { "jsonrpc": "2.0", "id": 1, "method": "connection/connect", "params": { "ownerUri": "probe-connection", "connection": { "server": "localhost", "database": "master", "authenticationType": "Integrated", "connectTimeout": 5 } } } with socket.create_connection(("127.0.0.1", 13140), timeout=10) as sock: request = json.dumps(payload).encode("utf-8") sock.sendall(request + b"\n") sock.settimeout(10) data = b"" while True: chunk = sock.recv(4096) if not chunk: break data += chunk if b"\n" in data: break print(data.decode("utf-8", errors="replace"))这段脚本的逻辑是:创建一个原始 TCP 连接,向 13140 发送一行 JSON,然后读取响应。它不依赖任何第三方库,能在最小环境里确认服务端口确实是活的。注意 payload 里的字段,不同版本的 SqlTools 对 connection/connect 的参数略有差异;如果收到 error 响应,不要慌,这本身就已经证明协议层在工作,错误信息里会告诉你哪个字段不匹配。
提示:这只是一个“探针”,不是完整的 SQL 客户端。真正要干活,还是用官方扩展或成熟的 JSON-RPC 客户端库,否则你要自己处理消息分帧、批量请求、进度通知,工作量会迅速膨胀。
3.4 不想写协议:用现成客户端做对照验证
协议手写毕竟麻烦,日常排错我更推荐用 mssql 扩展做对照。装好扩展后,它自己会去找对应的 ServiceLayer 包,并把它拉起来。你只需确认一件事实:扩展连接你本机实例成功时,后台进程列表里多了几个 dotnet 或 SqlTools 进程。
打开任务管理器,筛选名称里带 SqlTools 或 dotnet 的进程,看它的命令行参数。这个方法能直接暴露出实际用的日志目录、端口号、连接参数,比你猜配置快得多。如果扩展连接失败,但你在 3.2 节手动启动的服务里连接成功,那问题就出在扩展自带的连接串配置和你的手动会话不一致,优先核对认证方式与加密选项。
要注意链路差异:sqlcmd 直连的是 SQL Server 的 1433 端口,ServiceLayer 监听的是 13140 工具端口,两者不是一回事。sqlcmd 能连不代表 ServiceLayer 一定能连,后者中间多了协议转换层,很多权限错误要回到 SQL Server 日志里看。
4. 参数怎么调:从启动参数到连接串的四个必设项
4.1 启动参数:日志粒度、日志目录、端口、绑定地址
ServiceLayer 的命令行参数不多,但每个都直接影响排查效率。我一般固定使用四个参数组合:
| 参数 | 作用 | 推荐设置 |
|---|---|---|
| --enable-logging | 是否落盘日志 | 设为 true,否则排错无头绪 |
| --log-level | 日志粒度 | 调试用 DEBUG,生产用 INFO |
| --log-dir | 日志输出目录 | 独立目录,别放临时文件夹 |
| --port | 监听端口 | 默认 13140,多实例时手动指定 |
| --host | 绑定地址 | 默认 127.0.0.1,别改成 0.0.0.0 |
日志粒度调试时用 DEBUG 会非常吵,每条 JSON-RPC 请求都带完整参数,但排查“为什么连接不上”时,你恰恰需要看全链路。生产环境退回 INFO,只记录连接建立和断开,日志体积会小很多。log-dir 最好固定到项目目录下,比如 C:\dev\tools\sqltools\logs,这样清理和归档都方便。
绑定地址一定坚持用 127.0.0.1。ServiceLayer 本身没有认证层,谁拿到端口就能发请求。如果绑到 0.0.0.0,局域网里任何机器都可以用 JSON-RPC 操作你的 SQL 会话,安全上完全失控。
4.2 连接串参数:服务层通往 SQL Server 的通行证
ServiceLayer 不自己持有连接串,连接参数由客户端在 connection/connect 消息里传入。所以你在启动参数里调半天,连不上库也没用,要回头检查连接串。这里四个参数最容易被忽视:
| 参数 | 踩坑点 | 建议值 |
|---|---|---|
| Encrypt | 新版默认 true,旧库证书不匹配会失败 | 临时排错可 false,生产保持 true |
| TrustServerCertificate | 是否信任自签名证书 | 开发环境可 true,生产必须 false |
| Connect Timeout | 握手超时,默认 15 秒太长 | 内网 5 秒,跨网 10 秒 |
| Application Name | 标记请求来源 | 填工具名,方便 DBA 溯源 |
跨版本连接时最容易翻车的组合是:SQL Server 2016 没有现代 TLS 支持,而新客户端默认 Encrypt=true,于是握手时直接报 SSL 错误。解决方法是把 TrustServerCertificate 设为 true,先把链路跑通,再回头升级数据库的证书。这个坑和密码无关,很多人反复确认账号密码却忽略加密策略,白白耗掉半天。
4.3 多实例并行与 ownerUri 会话隔离
同一台开发机上往往同时跑着 mssql 扩展、azdata 和你自己写的小工具。如果它们都去抢占 13140,后启动的那个要么端口冲突,要么会话被串掉。我的习惯是给每个用途指定独立端口和日志目录:
dotnet Microsoft.SqlTools.ServiceLayer.dll --port 13140 --log-dir ./logs-ide dotnet Microsoft.SqlTools.ServiceLayer.dll --port 13141 --log-dir ./logs-tool两个进程互不干扰,日志也分开。你可以同时用一个实例连测试库,另一个实例连生产库,避免 ownerUri 冲突。
ownerUri 是连接会话的唯一标识,相当于一个连接别名。同一个 ownerUri 下的查询会复用同一个底层 SqlConnection,断开时要把挂钩清理干净。如果多个客户端复用同一个 service,必须给每个会话设置不同的 ownerUri,否则会互相踩连接。这也是自建工具时最容易忽略的问题:你以为发了两条独立查询,实际上它们被归到了同一个连接事务上下文里。
5. 避坑:ServiceLayer 本地部署中的六条血泪记录
5.1 双击没反应:找不到可执行入口
现象:解压后双击 Microsoft.SqlTools.ServiceLayer.exe,窗口一闪而过,什么信息都没留下。 原因:包里可能存在两种入口形态,一种是原生 exe 宿主,一种是纯 dll。如果你手上的包只有 dll 且缺 hostfxr,那么 exe 要么不存在,要么是个引导器,没有运行时根本起不来。 解决:先执行 dotnet Microsoft.SqlTools.ServiceLayer.dll 看真实报错。如果提示缺少运行时,按 3.1 节检查 dotnet --list-runtimes;如果提示找不到入口点,检查 runtimeconfig.json 是否存在。
5.2 启动即崩溃:只装了 .NET Runtime,没装 ASP.NET Core Runtime
现象:dotnet 命令启动后,几秒内进程退出,日志里报 The type initializer for ‘xxx’ threw an exception。 原因:SqlTools Service 虽然看起来是个控制台服务,但依赖了一部分 ASP.NET Core runtime 的库。只安装 .NET Desktop Runtime 并不能覆盖全部依赖。 解决:去装完整的 .NET 8.0 Runtime 包,包含 ASP.NET Core Runtime。装完后再跑 dotnet --list-runtimes,确认有 Microsoft.AspNetCore.App 8.0.x 再启动。
5.3 服务起来了,但始终连不上 SQL Server
现象:curl 127.0.0.1:13140 有响应,日志里也没有明显错误,但 connection/connect 请求一直返回 connection failed。 原因:Encrypt 和 TrustServerCertificate 组合不对。SQL Server 若使用自签名证书,客户端默认严格校验就会失败。 解决:在连接参数里临时设置 Encrypt=false 或 TrustServerCertificate=true。先用最宽松的组合验证账号权限,再逐步收紧加密配置。注意不要为了图省事永久关闭加密。
5.4 端口被占:多个工具实例抢 13140
现象:IDE 里连接正常,但自建服务启动后一直往上加到 13141、13142,客户端却还按 13140 连,全部超时。 原因:SqlTools Service 在端口被占时会自动顺延,但客户端不会自动跟着换端口。 解决:启动时用显式端口写死,并让客户端读取实际端口。最稳妥的做法是启动后从日志里解析 Listening on 127.0.0.1:xxxxx,这个端口就是该实例的唯一入口。
5.5 把 win-x64 包搬到 Linux 容器:BadImageFormatException
现象:在 Linux 容器里解压 win-x64 包,用 dotnet 启动直接抛 BadImageFormatException。 原因:这个 zip 名字里 win-x64 已经锁死平台。跨平台必须下载对应 RID 的包,不能用 wine 之类的兼容层硬跑。 解决:容器构建阶段按目标平台换包。如果镜像基于 Linux,就使用 linux-x64 版本;如果基于 ARM 架构,则改用 linux-arm64。进容器前先uname -m确认架构。
5.6 日志目录写不进去:部署在 Program Files 下的权限问题
现象:服务进程正常,但日志目录没有生成文件,或者启动时报 Access to the path is denied。 原因:把 zip 解压到了 Program Files 下,普通权限进程无法在安装目录里建目录写文件。 解决:解压到用户目录或项目目录,比如 C:\Users<你的用户名>\tools\sqltools。强制要放系统路径的话,就用 --log-dir 指向一个有写权限的目录,别把日志和程序文件混在一起。
6. 进阶:把 ServiceLayer 当本地 SQL 代理,复用连接做重复查询
一旦你理解了 ServiceLayer 的会话语义,就可以把它用在自动化场景里:连接一次,后续查询全部复用同一个 ownerUri。这样你的脚本省掉了反复建连和认证的开销,对 SQL Server 的连接风暴也友好很多。
下面这段 Python 演示的是一个“连接一次,查三次”的最小流程。重点看 ownerUri 如何跨请求保持:
import json import socket def send_request(sock, method, params): payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params} sock.sendall(json.dumps(payload).encode("utf-8") + b"\n") result = b"" while True: chunk = sock.recv(4096) if not chunk or b"\n" in chunk: result += chunk break result += chunk return result.decode("utf-8", errors="replace") with socket.create_connection(("127.0.0.1", 13140), timeout=10) as sock: connect_params = { "ownerUri": "my-session-001", "connection": { "server": "localhost", "database": "master", "authenticationType": "Integrated", "connectTimeout": 5 } } print(send_request(sock, "connection/connect", connect_params)) print(send_request(sock, "query/execute", { "ownerUri": "my-session-001", "query": "SELECT @@VERSION" })) print(send_request(sock, "query/execute", { "ownerUri": "my-session-001", "query": "SELECT DB_NAME()" }))代码逻辑:先建立 TCP 连接,再发送 connection/connect 创建会话,后续 query/execute 都带同一个 ownerUri。只要这个会话不显式断开,SqlTools 底层就不会重建 SqlConnection。
验证这个方法是否生效,我一般看三个指标:
| 指标 | 验证方式 | 预期结果 |
|---|---|---|
| SQL Server 连接数 | 查询 sys.dm_exec_sessions | 数量不随查询次数线性增长 |
| ServiceLayer 日志 | 检查 connection 相关事件 | 只有一次 connect 记录 |
| 响应耗时 | 脚本里统计往返时间 | 第二次查询明显快于首次握手 |
把这一套嵌进你自己的 CLI 工具,就相当于拥有一个轻量的数据库代理:连接统一由 ServiceLayer 管理,查询逻辑由你的脚本决定。这也是我后来做内部数据面板时一直沿用的做法,IDE 和脚本共用同一套服务,省掉了一地重复的驱动代码。希望帮到你。
本文还有配套的精品资源,点击获取