简介check_iis 是一款面向 Windows 服务器运维与监控集成场景的 C# 插件用于在本地计算机上检查 IIS 站点和应用程序池状态可接入 Icinga2、Icinga、Centreon、Shinken、Naemon 等类 Nagios 监控系统。它适合已部署 IIS 7.0 及以上版本、需要借助代理或 NSCP 执行本地检查的运维人员使用时可关注管理员权限、站点与应用程序池名称大小写匹配等约束。插件提供 .NET Framework 3.5 与 4.0 版本仅在同一台计算机本地检查需由 Icinga2 代理、NSCP 或类似执行器调用服务须以管理员身份运行才能读取 IIS 配置存储。压缩包共 18 个文件约 36KB包含 8 个 cs 源码文件、2 个 config 配置、1 个 conf 监控配置、1 个 sln 解决方案、1 个 csproj 项目文件以及 xml、license、md 说明等便于直接编译、查阅和接入现有监控流程。已有 202 人学习读者可从中获得可运行的检查插件源码、配置示例与部署思路用于快速验证 IIS 站点和 AppPool 的监控方案。1. 从一台总是“假死”的 IIS 服务器说起check_iis 到底盯的是什么某公司的一台 Windows Server 上挂了七八个 IIS 站点监控大盘一天到晚都是绿的。业务方却打电话过来说页面 503。人登上去一看某个应用程序池停在 Stopped 状态已经好几个小时了站点跟着躺平。原来那套监控只做了 ICMP ping 加 80 端口探测。端口有监听不代表应用池活着更不代表 IIS 能把请求交给托管运行时。这种「主机绿、业务红」的哑火是 Windows 托管场景里最典型的监控盲区。check_iis 补的就是这个洞。它是一个用 C# 写的命令行插件跑在本机直接读 IIS 的配置存储和运行时状态把站点Site与应用程序池AppPool的存活情况翻译成 Nagios / Icinga 认识的那一行文本加一个退出码。适合谁用手上已经有 Icinga、Nagios 这类轮询式监控同时又管着一批 Windows 应用服务器的运维和开发。你要的不是大而全的 APM而是「池子停了、站点停了五分钟内必须有人知道」。2. 工程骨架与取数选型一个 C# 控制台程序怎么变成合格的监控插件插件类项目和业务项目的分水岭在约束条件不能带一堆第三方 NuGet 包不能要求目标机器装 .NET 运行时以外的东西必须能在一台刚交付的、只有 IIS 的 Windows Server 上双击就跑。check_iis 的工程结构就是围绕这三条约束长出来的。2.1 三条取数路径的取舍WMI、appcmd 与 Microsoft.Web.Administration要在 Windows 上读 IIS 的站点和池状态路子其实不少踩过的都知道差别很大方案典型调用方式优点实际踩到的坑WMIroot\MicrosoftIISv2命名空间下的IIsApplicationPool不需要额外引用老系统也能用IIS 7 之后命名空间换成WebAdministration旧类在部分场景下返回空集合查询慢一次枚举常常要几百毫秒appcmd起子进程跑appcmd list apppool人眼可读调试方便要解析本地化文本输出机器是中文系统就返回中文状态词子进程启动开销大监控每分钟跑一次很浪费Microsoft.Web.Administrationnew ServerManager()直接拿强类型集合强类型、无文本解析、能拿到State枚举程序集在 GAC 里编译时得手动引用首次加载要读applicationHost.config冷启动偏慢check_iis 选的是第三条。理由很直接监控插件最怕的就是「解析文本」一旦目标机器的系统语言变了、IIS 版本变了正则就全废。ServerManager返回的ObjectState是枚举Started就是Started跟语言无关这一条就值回票价。代价是编译期要显式引用Microsoft.Web.Administration.dll。它不在标准框架引用列表里路径一般在%windir%\System32\inetsrv\或者 GAC 里。我一般会在 csproj 里写成 HintPath 指向$(windir)\System32\inetsrv\Microsoft.Web.Administration.dll避免每台构建机路径不一致。2.2 目录结构按「取数 / 判定 / 输出」三层切插件代码最容易写成一坨Main里从头 if 到尾。check_iis 的骨架是三层分离的这个结构值得照搬check_iis/ ├── check_iis.sln ├── src/ │ ├── Program.cs # 入口解析参数 - 调度检查 - 汇总退出码 │ ├── Arguments.cs # 命令行参数解析不引第三方库 │ ├── IisInspector.cs # 取数层封装 ServerManager抛业务异常 │ ├── Checks/ │ │ ├── ICheck.cs # 检查项接口方便二次开发挂新检查 │ │ ├── SiteCheck.cs # 站点检查 │ │ └── AppPoolCheck.cs # 应用程序池检查 │ └── Nagios/ │ ├── ExitCode.cs # 0/1/2/3 枚举 │ └── Output.cs # 拼 状态文本 | perfdata 并落盘退出 └── build/ └── build.bat # 一行 csc 编译产物单 exe分层的价值在二次开发时体现得最明显想加一个「站点响应时间」检查只要写一个实现ICheck的类注册进调度列表Program.cs一行都不用动。取数层出问题比如权限不够异常统一在IisInspector里被翻译成「无法读取 IIS 配置」这种人类能看懂的话而不是把UnauthorizedAccessException的堆栈直接吐到监控告警里。2.3 运行前提权限和框架版本比代码更容易翻车写代码之前先把运行环境钉死能省掉后面一大半排查时间。我一般会要求.NET Framework 4.5 及以上。低于 4.0 的话Microsoft.Web.Administration的部分重载方法没有ServerManager的释放行为也不一样。目标机器必须装 IIS 管理工具Web-Mgmt-Tools这个 Windows 功能否则inetsrv目录下根本没有那个 dll运行时直接FileNotFoundException。执行账户要有读applicationHost.config的权限。用管理员跑当然没问题但很多监控代理是跑在 LocalSystem 或专用服务账户下的这一条经常被忽略。提示如果你把插件挂在监控代理下执行先用代理那个账户手动跑一次 exe确认退出码是 0 而不是 3再去配 CheckCommand。这一步能挡掉后面一半的「配了但不生效」。3. 核心链路拆解一次检查从参数解析到退出码的完整过程这个插件的核心链路其实很短解析参数 → 打开ServerManager→ 按名字找到站点或池 → 判定状态 → 拼一行文本加性能数据 → 用特定退出码结束进程。短归短每一环都有讲究。3.1 参数解析贴着 Nagios 的约定走别自创风格Nagios 系插件的参数有个不成文的默契-w是 warning 阈值-c是 critical 阈值-H是主机。check_iis 沿用了这套同时用-s指定站点、-a指定应用程序池两个都可以重复出现用来一次检查多个目标。// Arguments.cs —— 手写解析不引 CommandLineParser少一个部署依赖 internal sealed class Arguments { public Liststring Sites { get; } new Liststring(); public Liststring AppPools { get; } new Liststring(); public double WarnSeconds { get; private set; } 5; public double CritSeconds { get; private set; } 30; public int TimeoutMs { get; private set; } 20000; public bool Verbose { get; private set; } public static Arguments Parse(string[] argv) { var a new Arguments(); for (int i 0; i argv.Length; i) { switch (argv[i]) { case -s: case --site: a.Sites.Add(argv[i]); // 可重复支持批量 break; case -a: case --apppool: a.AppPools.Add(argv[i]); break; case -w: a.WarnSeconds double.Parse(argv[i], CultureInfo.InvariantCulture); break; case -c: a.CritSeconds double.Parse(argv[i], CultureInfo.InvariantCulture); break; case -t: a.TimeoutMs int.Parse(argv[i]) * 1000; break; case -v: case --verbose: a.Verbose true; break; default: throw new ArgumentException($未知参数: {argv[i]}); } } if (a.WarnSeconds a.CritSeconds) throw new ArgumentException(-w 必须小于 -c否则告警永远先于严重触发); return a; } }参数解析这块最容易翻车的是数字解析的CultureInfo。在某些区域设置下double.Parse(1.5)会因为小数点符号是逗号而抛异常。插件是要跨机器部署的统一用InvariantCulture是最省心的做法。另外-w必须小于-c这个校验看着像废话但真有人在配 CheckCommand 时把两个值写反结果告警一直不触发。宁可在入口就抛异常让配置错误立刻暴露。3.2 取数层把 ServerManager 的异常收敛成一句话ServerManager是个比较“重”的对象构造时要读applicationHost.config出错的方式也五花八门。取数层的职责就是把这些底层异常翻译成监控系统能理解的语义。// IisInspector.cs internal sealed class IisInspector : IDisposable { private readonly ServerManager _mgr; public IisInspector() { try { _mgr new ServerManager(); // 冷启动会读 applicationHost.config } catch (Exception ex) when (ex is UnauthorizedAccessException || ex is FileNotFoundException) { // 权限不足或缺 IIS 管理组件属于环境问题不能当业务故障 throw new IisUnavailableException(无法打开 IIS 配置存储请检查执行账户权限与 IIS 管理工具是否安装, ex); } } public Site FindSite(string name) _mgr.Sites.FirstOrDefault(s string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)); public ApplicationPool FindAppPool(string name) _mgr.ApplicationPools.FirstOrDefault(p string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); public void Dispose() _mgr?.Dispose(); }IisUnavailableException这个自定义异常是整段代码的关键。监控系统里「环境坏了」和「业务坏了」必须区分开前者应该返回UNKNOWN退出码 3让值班的人去找运维修权限后者返回CRITICAL退出码 2去做业务恢复。如果两种情况都返回 2值班的人半夜被叫起来结果发现只是插件没权限读配置这种血泪经验经历过一次就长记性了。查找用的是OrdinalIgnoreCase而不是默认比较。IIS 里站点名的大小写虽然在创建时固定了但配置文件被手工改过之后可能和预期不一致忽略大小写更稳妥。3.3 站点检查状态枚举、绑定信息与缺失判定站点检查要考虑三种结果存在且启动、存在但停止、压根不存在。第三种最容易被忽略但其实是线上最危险的——有人误删了站点检查脚本如果只判断「停止」会因为找不到而静默通过。// Checks/SiteCheck.cs internal sealed class SiteCheck : ICheck { private readonly IisInspector _iis; private readonly string _name; public SiteCheck(IisInspector iis, string name) { _iis iis; _name name; } public CheckResult Run() { var site _iis.FindSite(_name); if (site null) return CheckResult.Critical($站点 {_name} 不存在); // State 是 ObjectState 枚举与系统语言无关 if (site.State ! ObjectState.Started) return CheckResult.Critical($站点 {_name} 状态为 {site.State}); // 绑定端口一并带出来方便排查端口冲突 var bindings string.Join(,, site.Bindings.Select(b b.BindingInformation)); return CheckResult.Ok($站点 {_name} 运行中 [{bindings}]); } }site.State这个枚举值在 IIS 里可能返回Started、Stopped、Starting、Stopping、Unknown。判断只认Started是最保守的做法——Starting虽然看起来像在起来但如果卡在这个状态超过一分钟那就是真出问题了早点报出来比等着好。绑定信息带进输出是有私心的。很多「站点 503」的根因是端口被别的进程占了或者 HTTPS 绑定证书过期。一行输出里带上绑定字符串值班的人看一眼就知道该去查哪儿不用再登录机器翻 IIS 管理器。3.4 应用程序池检查状态之外还要看运行时长池状态只判断Started是不够的。有一种很阴的情况池状态显示Started但实际上刚刚被回收过应用还在重新预热这时候用户访问就是 503。所以要结合性能计数器APP_POOL_WAS里的运行时长一起判断。// Checks/AppPoolCheck.cs internal sealed class AppPoolCheck : ICheck { private readonly IisInspector _iis; private readonly string _name; private readonly double _warn, _crit; public AppPoolCheck(IisInspector iis, string name, double warn, double crit) { _iis iis; _name name; _warn warn; _crit crit; } public CheckResult Run() { var pool _iis.FindAppPool(_name); if (pool null) return CheckResult.Critical($应用程序池 {_name} 不存在); if (pool.State ! ObjectState.Started) return CheckResult.Critical($应用程序池 {_name} 状态为 {pool.State}); // 计数器名在不同语言系统下可能被本地化取不到就降级为“不判定时长” double uptime TryReadUptimeSeconds(_name); if (uptime 0 uptime _crit) return CheckResult.Warning($应用程序池 {_name} 运行时长仅 {uptime:F0}s可能刚被回收); var perf ${_name}_uptime{uptime:F0}s;{_warn};{_crit};0; return CheckResult.Ok($应用程序池 {_name} 运行中uptime{uptime:F0}s, perf); } private static double TryReadUptimeSeconds(string poolName) { try { // APP_POOL_WAS 是 IIS 自带的性能计数器组 using (var pc new PerformanceCounter(APP_POOL_WAS, Current Application Pool Uptime, poolName)) return pc.NextValue(); } catch { return -1; } // 计数器缺失不致命降级处理 } }性能计数器的读取一定要包 try。APP_POOL_WAS这个组在某些精简安装的 Windows Server 上不存在或者计数器名被本地化成中文直接new PerformanceCounter会抛InvalidOperationException。降级成「不判定时长」比整个检查崩掉要合理得多——状态检查这一半还是有价值的。perfdata的格式也值得说一下Nagios 的 perfdata 语法是labelvalue[UOM];[warn];[crit];[min];[max]s是秒单位。拼错一个分号Icinga 那边的图表就画不出来但退出码还是正常的属于典型的「看着没坏其实没用」。3.5 输出与退出码整个插件唯一被监控系统看见的部分不管内部逻辑多漂亮监控系统能看到的只有两样东西标准输出的那一行文本和进程结束时的退出码。这两样必须严格符合约定。// Nagios/Output.cs internal enum ExitCode { Ok 0, Warning 1, Critical 2, Unknown 3 } internal static class Output { public static int Emit(ExitCode code, string message, IEnumerablestring perf null) { var line ${code.ToString().ToUpperInvariant()} - {message}; if (perf ! null perf.Any()) line | string.Join( , perf); Console.WriteLine(line); return (int)code; } }退出码的语义是有严格定义的0 正常、1 告警、2 严重、3 未知。最常犯的错误是把「插件自己出错」也返回 2结果任何权限问题、路径问题都变成了业务故障。正确做法是只有「被监控对象确实异常」才返回 1 或 2插件自身的问题一律 3。输出里|前面的部分要控制长度。Icinga 会把这一行直接展示在告警通知里太长会被截断关键信息反而丢了。我一般的习惯是状态 对象名 一个最关键的数字剩下的细节交给--verbose参数在本地调试时输出。4. 编译部署与 Icinga 接入把 exe 变成一条会报警的服务代码写完之后让它在监控系统里真正跑起来剩下的活儿主要在三处编译成单文件、放到正确的位置、在监控侧定义好调用方式。4.1 用 csc 直接编产物就是一个 exe插件工程不建议用繁琐的 MSBuild 流程一个 bat 文件就能搞定:: build/build.bat —— 单文件产物便于拷到目标机 set CSC%windir%\Microsoft.NET\Framework64\v4.0.30319\csc.exe set IIS_DLL%windir%\System32\inetsrv\Microsoft.Web.Administration.dll %CSC% /target:exe /platform:anycpu /optimize ^ /out:bin\check_iis.exe ^ /reference:%IIS_DLL% ^ /reference:System.Core.dll ^ /reference:System.Management.dll ^ src\*.cs src\Checks\*.cs src\Nagios\*.cs参数说明/target:exe生成控制台程序/platform:anycpu让它在 32 位和 64 位系统上都能跑/optimize打开优化减少冷启动时间——这对监控插件是实打实的收益因为每分钟都会启一次进程。注意一定要引用inetsrv目录下那个Microsoft.Web.Administration.dll不要去 GAC 里随便抓一个版本。GAC 里可能同时存在多个版本编译时用错版本在目标机上就会变成FileLoadException。编译完成后先把 exe 拷到任意一台装了 IIS 的机器上裸跑一次check_iis.exe -s Default -a DefaultAppPool -w 60 -c 10 -v期望看到类似OK - 站点 Default 运行中 [...] 应用程序池 DefaultAppPool 运行中uptime84213s | DefaultAppPool_uptime84213s;60;10;0的输出echo %ERRORLEVEL%应该返回 0。这一步不过后面配什么都没意义。4.2 Icinga2 侧CheckCommand 与 Apply Service 的写法exe 放到监控代理的插件目录常见是C:\Program Files\icinga2\usr\lib\icinga2\plugins\然后在配置里定义一条命令模板object CheckCommand check_iis { command [ C:\\Program Files\\icinga2\\usr\\lib\\icinga2\\plugins\\check_iis.exe ] arguments { -s { value $iis_site$ } -a { value $iis_apppool$ } -w { value $iis_warn_uptime$, default 60 } -c { value $iis_crit_uptime$, default 10 } -t { value $iis_timeout$, default 20 } } timeout 30s }再挂到服务上让同一台机器上的多个站点共用一个检查apply Service iis- for (site cfg in host.vars.iis_sites) { import generic-service check_command check_iis vars.iis_site site vars.iis_apppool cfg assign where host.vars.iis_sites }host.vars.iis_sites是一个字典类型例如{ portal portal_pool, api api_pool }。这样一台服务器加两个站点只需要在主机对象里加一行配置就能自动生成两个 Service不用为每个站点手写一遍 CheckCommand。这里有个细节timeout要设得比插件内部-t大。插件自己超时会先退出并返回临界状态如果外部超时先触发会变成UNKNOWN告警信息里连测的是哪个站点都看不到。4.3 阈值怎么定从实测数据反推别拍脑袋-w和-c在 check_iis 里表示「运行时长低于这个值就告警」单位是秒。这两个值不能瞎填得看你的站点重启后多久能正常提供服务。我的做法是先在测试环境上把网站重启用一下然后每 5 秒跑一次检查记录到第一次返回 OK 的时间。这个时间乘以 2 作为-c乘以 5 作为-w。比如某站点冷启动大约 40 秒才真正能接请求那-c定 120、-w定 300。低于 120 秒就说明池子刚被回收或者刚崩溃重启这是必须立刻知道的低于 300 秒是「近期刚重启过注意观察」属于告警级别。定完阈值之后把常见告警场景过一遍手工停池、手工停站、进程崩溃触发快速故障保护、配置错误导致站点启动失败、IIS 服务重启。五个场景都跑一次确认告警文本能让你一眼看出是哪种。这一步做扎实了后面半年都不会再被问「这个告警是什么意思」。5. 避坑与排查五种跑不起来或误报的典型情况这类插件上线之后最常见的麻烦不是代码 bug而是环境差异。下面五种情况我在不同机器上都遇到过按现象、原因、解决三段的顺序写下来下次再碰到可以少走弯路。现象一插件在命令行里跑得好好的但在监控代理里一直是 UNKNOWN输出无法打开 IIS 配置存储。原因监控代理服务跑在LocalSystem或专用服务账户下而ServerManager需要读%windir%\System32\inetsrv\config\applicationHost.config这个路径的 ACL 默认只给 Administrators 和 SYSTEM 组读权限。某些加固过的机器把 SYSTEM 的读权限也收紧了。 解决先把代理服务的登录账户改成LocalSystem如果还是不行手动给代理账户加该文件与所在目录的「读取和执行」权限。改完不用重启机器但要重启代理服务。现象二CRITICAL - 应用程序池 XXX 状态为 Stopped登上去看明明在跑。原因池名和站点名之间没有一对一的对应关系。一个池可以被多个站点共用也可能存在名字相近的池比如api_pool和api_pool_v2参数里写错一个字符就匹配到另一个池上去了。 解决先用check_iis.exe --list如果实现了的话或者 IIS 管理器确认池名的精确拼写更稳妥的做法是在主机对象里用iis_sites字典显式声明同一个名字在字典 key 和 value 里各出现一次能大幅降低拼错概率。现象三中文站点名匹配不上检查直接返回「不存在」。原因ServerManager返回的站点名是 Unicode 字符串但如果把中文名写在 Icinga 的配置文件里而那个文件保存成不带 BOM 的 UTF-8Icinga 解析时会按系统本地编码处理字符串对不上。或者是插件编译时加了/codepage之类的参数代码里的中文字面量已经和配置文件不一致。 解决配置文件统一用带 BOM 的 UTF-8 保存更省心的做法是给站点起英文名把中文描述放到 Icinga 的display_name里。这一条虽然看起来是「管理规定」但它确实能把一类很隐蔽的编码问题直接消灭。现象四每隔一段时间就会有一次随机超时返回 UNKNOWN日志里看不到任何异常。原因new ServerManager()每次都要重新读applicationHost.config并解析里面的配置节站点多的时候冷启动可能就到秒级。如果监控代理同时并发跑好几个check_iis.exe磁盘 IO 一抢第一个就超时了。 解决把 Icinga 里同一台主机上check_iis相关的服务配置成max_check_attempts至少 3避免单次抖动就直接告警同时把插件侧-t调到 30 秒左右给冷启动留余量。现象五perfdata 显示出来了但 Icinga 图表是空的。原因perfdata 字符串格式写错了。labelvalue;warn;crit;min;max里分号的数量严格固定少一个分号后面的字段就被解释成别的含义多一个分号 Icinga 可能直接丢弃整条 perfdata。还有一种情况是 label 里带空格没加引号。 解决用check_iis.exe ...输出之后把|后面的字符串贴到一个文本编辑器里逐字符数分号。这个活儿看着笨但比在 Icinga 里翻日志快得多。提示上面五条里前四条都和「环境」有关第五条和「格式」有关没有一条是算法问题。这也说明了这类插件项目的工作重心其实在部署和接入不在代码本身。6. 二次开发加一个「站点响应探测」并自证它是对的如果只把 check_iis 当成品用改端口、改站点名这种事在配置文件里就够了。但总有场景需要加检查项比如某个站点的健康检查接口会在应用出错时返回 500而池状态仍然是Started——这种情况池状态检查永远发现不了。加这个检查项的路径很清晰实现ICheck接口加进调度列表然后在Arguments里补一个开关参数。核心是下面这个类它做的事很有限抓到站点的主绑定端口发一个 GET 到配置好的相对路径判断状态码。// Checks/SiteProbeCheck.cs internal sealed class SiteProbeCheck : ICheck { private readonly IisInspector _iis; private readonly string _site, _path; private readonly int _expect; public SiteProbeCheck(IisInspector iis, string site, string path, int expect 200) { _iis iis; _site site; _path path; _expect expect; } public CheckResult Run() { var site _iis.FindSite(_site); if (site null) return CheckResult.Critical($站点 {_site} 不存在); // BindingInformation 形如 *:8080:取中间那段端口 var port site.Bindings.Select(b b.BindingInformation.Split(:)) .Select(p p.Length 1 ? p[1] : null) .FirstOrDefault(p !string.IsNullOrEmpty(p)); var url $http://127.0.0.1:{port}{_path}; try { using (var client new WebClient { Timeout 5000 }) using (var resp client.OpenRead(url)) { // WebClient 不直接暴露 StatusCode用 HttpWebRequest 更干净这里示意 return CheckResult.Ok($站点 {_site} 探测 {url} 通过); } } catch (WebException wex) { var code (wex.Response as HttpWebResponse)?.StatusCode; return code (HttpStatusCode)_expect ? CheckResult.Ok($站点 {_site} 返回预期状态码 {_expect}) : CheckResult.Critical($站点 {_site} 探测失败: {code?.ToString() ?? wex.Status.ToString()}); } } }SiteProbeCheck里有一个容易忽略的点请求走的是127.0.0.1而不是站点绑定的主机名。原因很简单插件是跑在本机做探测的走回环地址不会经过外部 DNS 和防火墙减少了一半不确定因素。代价是拿不到 HTTPS 证书的错误但证书问题属于另一类检查项不该混在响应探测里。改造完的验证方法我的顺序是固定的步骤操作通过标准1在测试机上手工跑check_iis.exe -s 站点 -p /health输出OK%ERRORLEVEL%为 02把/health改成/notfound输出CRITICAL退出码 23手工停池再跑一次池检查先返回CRITICAL探测检查跟着返回CRITICAL但两条信息都要能看出是哪一层坏了4在 Icinga 上挂一个 service等一个 check interval告警文本能在 dashboard 上完整展示perfdata 有图第三步是最容易被跳过、也最容易出问题的一步。很多插件单独跑没问题一旦和别的检查组合起来状态码聚合就把真实原因盖掉了。Icinga 对同一主机多个服务的告警是有依赖关系的如果池检查已经CRITICAL你应该允许探测检查暂时不告警而不是让值班的人同时收到五条内容重复的短信。从那以后我每次给这类插件加新检查项都强制先在测试机上把「正常 / 异常 / 环境故障」三个场景各跑一遍确认退出码分别是 0、2、3 之后才允许它出现在生产监控里。我见过太多次插件上线之后一直在报警、业务却好得很的局面追根究底都是少了这一步自证。希望帮到你。本文还有配套的精品资源点击获取