PowerShell WinForm端口扫描脚本卡顿及格式化输出问题
端口扫描脚本问题排查与优化
问题描述
端口扫描结果需输出至$TextBoxResultScan,当前存在两个核心问题:
- 端口开放时可输出结果,但格式不符合预期;
- 端口关闭时,脚本及Windows Form窗体出现卡顿冻结现象。
需解决以下两点:
- 分析卡顿冻结的原因并提供修复方案;
- 配置扫描结果按服务器地址、端口号、扫描结果三列格式化输出。
问题1:卡顿冻结的原因与修复
原因分析
- UI线程阻塞:
Test-NetConnection在端口关闭时会等待默认超时(通常数秒),而扫描逻辑直接在窗体的UI线程中执行,导致UI无法响应任何操作,出现冻结。 - 串行执行效率低:所有端口扫描是逐个串行执行的,累计等待时间过长,进一步加剧了UI阻塞。
修复方案
- 后台线程执行扫描:将扫描逻辑放到后台线程中,避免占用UI线程,同时通过
Control.Invoke安全更新UI控件(WinForm控件仅允许创建它的线程修改)。 - 缩短超时时间:给
Test-NetConnection添加-TimeoutMs参数,将超时设置为1000毫秒(可根据需求调整),减少等待时长。 - 可选优化:并行扫描:使用
ForEach-Object -Parallel(PowerShell 7+)或Start-Job实现并行端口扫描,大幅提升扫描速度。
问题2:按列格式化输出结果
当前问题
直接将Select-Object的输出赋值给TextBoxResultScan.Text,PowerShell会自动将对象转换为默认字符串格式,导致排版混乱,且每次赋值会覆盖之前的结果,最终仅显示最后一条扫描记录。
实现方法
- 添加表头:扫描开始前先写入列标题,用制表符
\t分隔列,保证对齐。 - 逐行追加结果:使用
+=将每条扫描结果追加到文本框,而非覆盖。 - 格式化每行内容:手动拼接服务器地址、端口号、扫描结果(将布尔值转换为更直观的"开放"/"关闭"),用制表符分隔保证列对齐;同时使用等宽字体进一步优化排版效果。
修改后的完整脚本
Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing <#------======== Define Variables ========------#> $MyForest = ([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).Name $MyDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name $NetworkPorts = @('21','22','23','25','38','42','53','65','69','80','118','123','135','137','139','143','145','156','161','170','389','443','445','647','993','995','1433','1540','1541','3389','8080') <#------======== Define Variables ========------#> <#------======== Common Form ========------#> $CommonForm = New-Object System.Windows.Forms.Form $CommonForm.AutoSize = $true $CommonForm.Text = "Servers Scanning" <#------======== Common Form ========------#> <#------======== Label List Enable Servers ========------#> $LabelListeServers = New-Object System.Windows.Forms.Label $LabelListeServers.Location = '2,10' $LabelListeServers.AutoSize = $true $LabelListeServers.Text = 'Enable Servers Scanning' $CommonForm.Controls.Add($LabelListeServers) <#------======== Label List Enable Servers ========------#> <#------======== ListBox List Enable Servers ========------#> $ListboxListeServers = New-Object System.Windows.Forms.ListBox $ListboxListeServers.Location = '2,30' $ListboxListeServers.Size = New-Object System.Drawing.Size(200,125) $CommonForm.Controls.Add($ListboxListeServers) <#------======== ListBox List Enable Servers ========------#> <#------======== Button Search Enable Servers ========------#> $ButtonSearcheServers = New-Object System.Windows.Forms.Button $ButtonSearcheServers.Location = '2, 165' $ButtonSearcheServers.Text = 'Find Servers' $ButtonSearcheServers.AutoSize = $true $ButtonSearcheServers.add_Click({ $script:eADWindowsServers = @((Get-ADComputer -Server $MyDomain -Filter {(OperatingSystem -like '*Windows Server*') -and (Enabled -eq $true)} | sort Name).DnsHostName) $ListboxListeServers.Items.Clear() Foreach($eADWindowsServer in $eADWindowsServers){ $ListboxListeServers.Items.Add($eADWindowsServer) } }) $CommonForm.Controls.Add($ButtonSearcheServers) <#------======== Button Search Enable Servers ========------#> <#------======== Label List Disable Servers ========------#> $LabelListdServers = New-Object System.Windows.Forms.Label $LabelListdServers.Location = '2,195' $LabelListdServers.AutoSize = $true $LabelListdServers.Text = 'Disable Servers Scanning' $CommonForm.Controls.Add($LabelListdServers) <#------======== Label List Disable Servers ========------#> <#------======== ListBox List Disable Servers ========------#> $ListboxListdServers = New-Object System.Windows.Forms.ListBox $ListboxListdServers.Location = '2,220' $ListboxListdServers.Size = New-Object System.Drawing.Size(195,125) $CommonForm.Controls.Add($ListboxListdServers) <#------======== ListBox List Disable Servers ========------#> <#------======== Button Search Disable Servers ========------#> $ButtonSearchdServers = New-Object System.Windows.Forms.Button $ButtonSearchdServers.Location = '2, 350' $ButtonSearchdServers.Text = 'Find Servers' $ButtonSearchdServers.AutoSize = $true $ButtonSearchdServers.add_Click({ $dADWindowsServers = @((Get-ADComputer -Server $MyDomain -Filter {(OperatingSystem -like '*Windows Server*') -and (Enabled -eq $false)} | sort Name).DnsHostName) $ListboxListdServers.Items.Clear() Foreach($dADWindowsServer in $dADWindowsServers){ $ListboxListdServers.Items.Add($dADWindowsServer) } }) $CommonForm.Controls.Add($ButtonSearchdServers) <#------======== Button Search Disable Servers ========------#> <#------======== Label Port Scan ========------#> $LabelPortScan = New-Object System.Windows.Forms.Label $LabelPortScan.Location = '225,10' $LabelPortScan.AutoSize = $true $LabelPortScan.Text = 'Ports scanning. Port:' $CommonForm.Controls.Add($LabelPortScan) <#------======== TextBox Result Scan ========------#> $TextBoxResultScan = New-Object System.Windows.Forms.TextBox $TextBoxResultScan.Location = '225,30' $TextBoxResultScan.Size = New-Object System.Drawing.Size(565,200) $TextBoxResultScan.Multiline = $true $TextBoxResultScan.Font = New-Object System.Drawing.Font("Consolas", 10) # 使用等宽字体保证列对齐 $CommonForm.Controls.Add($TextBoxResultScan) <#------======== TextBox Result Scan ========------#> <#------======== TextBox Enter Port ========------#> $TextBoxPortScan = New-Object System.Windows.Forms.TextBox $TextBoxPortScan.Location = '360,7' $TextBoxPortScan.Size = New-Object System.Drawing.Size(40,20) # 修正高度,原高度5看不见 $CommonForm.Controls.Add($TextBoxPortScan) <#------======== TextBox Enter Port ========------#> <#------======== Button Port Scan ========------#> $ButtonPortScan = New-Object System.Windows.Forms.Button $ButtonPortScan.Location = '400, 5' $ButtonPortScan.Text = 'Check' $ButtonPortScan.AutoSize = $true $ButtonPortScan.add_Click({ # 禁用按钮防止重复点击 $ButtonPortScan.Enabled = $false # 清空结果框并添加表头 $TextBoxResultScan.Invoke({ $this.Text = "服务器地址`t端口号`t扫描结果`n" }) # 确定扫描目标:服务器和端口 $targetServers = if($ListboxListeServers.SelectedItem -eq $null) { $script:eADWindowsServers } else { @($ListboxListeServers.SelectedItem.ToString()) } $targetPorts = if($TextBoxPortScan.Text -ne '') { @($TextBoxPortScan.Text) } else { $NetworkPorts } # 启动后台线程执行扫描 $job = Start-Job -ScriptBlock { param($Servers, $Ports, $TimeoutMs) $results = @() foreach($server in $Servers){ foreach($port in $Ports){ $result = Test-NetConnection -ComputerName $server -Port $port -TimeoutMs $TimeoutMs -ErrorAction SilentlyContinue -WarningAction SilentlyContinue $status = if($result.TcpTestSucceeded) { "开放" } else { "关闭" } # 处理无法解析地址的情况 $remoteAddr = if($result.RemoteAddress) { $result.RemoteAddress } else { "无法解析" } $results += [PSCustomObject]@{ Server = $server Port = $port Status = $status RemoteAddress = $remoteAddr } } } return $results } -ArgumentList $targetServers, $targetPorts, 1000 # 轮询作业状态,刷新UI防止假死 while($job.State -eq 'Running'){ Start-Sleep -Milliseconds 100 $CommonForm.Refresh() } # 获取作业结果并更新UI $scanResults = Receive-Job -Job $job Remove-Job -Job $job foreach($item in $scanResults){ $line = "$($item.RemoteAddress)`t$($item.Port)`t$($item.Status)`n" $TextBoxResultScan.Invoke({ $this.Text += $line }) } # 重新启用按钮 $ButtonPortScan.Enabled = $true }) $CommonForm.Controls.Add($ButtonPortScan) <#------======== Button Port Scan ========------#> <#------======== Call Form ========------#> $CommonForm.ShowDialog() <#------======== Call Form ========------#>
内容的提问来源于stack exchange,提问作者Catcher Rem
相关产品推荐
相关产品推荐

