今天是小L的PS进化论,批量telnet小程序的最后一期,打破所有禁锢,自己使用函数去测试端口
因为$tcpClient变量没有被初始化。在调用BeginConnect方法之前,需要先创建TcpClient对象。
这里是代码:
$remotetradeIP = @("1800.1069.300.1850") $ports = 3123 foreach ($forbideIP in $remotetradeIP) { foreach ($port in $ports) { # 为每个连接创建新的 TcpClient 实例 $tcpClient = New-Object System.Net.Sockets.TcpClient $asyncResult = $tcpClient.BeginConnect($forbideIP, $port, $null, $null) # 如果需要等待连接完成,可以添加以下代码 # 但注意:BeginConnect 是异步的,不会阻塞执行 } }重要说明:
异步连接:
BeginConnect是异步方法,它会立即返回,不会等待连接完成。如果需要等待连接完成,应该使用同步的Connect方法或处理异步回调。资源管理:这个代码会创建多个
TcpClient实例但没有关闭它们。如果需要更完整的管理,可以添加超时和关闭逻辑:
$tcpClient = $null try { $tcpClient = New-Object System.Net.Sockets.TcpClient $asyncResult = $tcpClient.BeginConnect($forbideIP, $port, $null, $null) # 等待连接完成(带超时) $success = $asyncResult.AsyncWaitHandle.WaitOne($timeout, $false) if ($success) { $tcpClient.EndConnect($asyncResult) Write-Host "成功连接到 $forbideIP`:$port" } else { Write-Host "连接到 $forbideIP`:$port 超时" } } catch { Write-Host "连接到 $forbideIP`:$port 失败: $_" } finally { if ($tcpClient -ne $null) { $tcpClient.Close() } }我们遇到了一个新的错误:同一个套接字上正在进行另一个异步操作时,不能调用 BeginConnect。
这是因为我们在同一个TcpClient实例上连续调用BeginConnect,而前一个异步操作还没有完成。
根据你的需求,你想要同时(异步地)尝试连接到多个IP和端口。但是,一个TcpClient实例一次只能处理一个连接操作。
有几种解决方案:
为每个连接创建一个新的TcpClient实例。
使用同步连接,但这样会阻塞线程,并且不能同时尝试多个连接。
使用异步连接,但每个连接使用单独的TcpClient实例。
同时尝试连接多个IP和端口,所以应该为每个连接创建一个新的TcpClient实例。
注意你之前将TcpClient实例创建放在了循环外部,这会导致所有连接尝试都使用同一个TcpClient实例,从而引发上述错误。这个错误是因为你在同一个TcpClient实例上连续调用BeginConnect方法。BeginConnect是异步操作,在第一个连接尝试完成之前,不能在同一个TcpClient实例上开始另一个连接。由于我们为每个连接尝试都创建了一个TcpClient实例,所以不会出现同一个套接字上多个异步操作的问题。
思路如下:
我将代码修改为每个连接尝试都使用新的TcpClient实例。同时,为了管理异步操作,我们可以存储所有的异步结果,然后在之后等待它们完成。
但是注意:异步连接操作是立即返回的,我们可能需要在之后检查连接是否成功。如果创建一个TcpClient实例,并异步连接,然后立即丢弃(不等待结果)。但这样可能会导致资源泄露,因为连接可能还在进行中,而我们已经离开了using块。请注意:异步操作需要被妥善管理,否则可能会造成资源浪费和程序不稳定。
更好的做法是使用异步操作,并在完成后关闭连接。比如端口扫描,那么可以使用以下方式:
使用异步连接,只是想要尝试连接,而不需要保持连接。所以我们可以这样做:异步连接尝试,并在连接尝试后立即关闭(无论成功与否)的代码。
但是,注意:BeginConnect是异步的,我们无法立即知道连接是否成功
使用回调函数的示例,在连接尝试完成后关闭连接:
但是,TcpClient的BeginConnect没有直接提供超时取消的方法,所以我们可以通过等待WaitHandle来模拟超时,然后关闭TcpClient。
以下代码可以实现:
代码1:为每个连接创建新的 TcpClient 实例
$timeout = 1000 $remotetradeIP = @("10.185.10.185") $ports = 3113 foreach ($forbideIP in $remotetradeIP) { foreach ($port in $ports) { # 为每个连接创建新的 TcpClient 实例 $tcpClient = New-Object System.Net.Sockets.TcpClient try { $asyncResult = $tcpClient.BeginConnect($forbideIP, $port, $null, $null) $success = $asyncResult.AsyncWaitHandle.WaitOne($timeout, $false) if ($success) { $tcpClient.EndConnect($asyncResult) Write-Host "成功连接到 $forbideIP`:$port" } else { Write-Host "连接到 $forbideIP`:$port 超时" } } catch { Write-Host "连接到 $forbideIP`:$port 失败: $_" } finally { if ($tcpClient -ne $null) { $tcpClient.Close() } } } }代码2:使用同步连接(更简单)
$timeout = 1000 $remotetradeIP = @("100.10609.300.850") $ports = 3113 foreach ($forbideIP in $remotetradeIP) { foreach ($port in $ports) { $tcpClient = $null try { $tcpClient = New-Object System.Net.Sockets.TcpClient # 使用同步连接 $connectTask = $tcpClient.ConnectAsync($forbideIP, $port) $completed = $connectTask.Wait($timeout) if ($completed -and $tcpClient.Connected) { Write-Host "成功连接到 $forbideIP`:$port" } else { Write-Host "连接到 $forbideIP`:$port 超时或失败" } } catch { Write-Host "连接到 $forbideIP`:$port 失败: $_" } finally { if ($tcpClient -ne $null) { $tcpClient.Close() } } } }代码3:并行测试连接
$remotetradeIP = @("4080.2609.300.285") $ports = 33 # 创建并行任务列表 $jobs = @() foreach ($forbideIP in $remotetradeIP) { foreach ($port in $ports) { $jobs += Start-Job -ScriptBlock { param($ip, $port) $result = Test-NetConnection -ComputerName $ip -Port $port -WarningAction SilentlyContinue -ErrorAction SilentlyContinue [PSCustomObject]@{ IP = $ip Port = $port Status = if ($result.TcpTestSucceeded) { "成功" } else { "失败" } TcpTestSucceeded = $result.TcpTestSucceeded PingSucceeded = $result.PingSucceeded } } -ArgumentList $forbideIP, $port } } # 等待所有任务完成 $results = $jobs | Wait-Job | Receive-Job # 清理作业 $jobs | Remove-Job # 显示结果 $results | Format-Table -AutoSize建议:
如果需要批量快速测试,使用代码3
1..20 | % { $a = $_; write-host "------"; write-host "10.1.74.$a"; 3389,53,80,445 | % {echo ((new-object Net.Sockets.TcpClient).Connect("10.1.74.$a",$_)) "Port $_ is open!"} 2>$null} // 针对某IP段 & 多个端口的扫描器
————————————————————————————————
实际测试中遇到的问题:
结果文字重叠的问题是因为在循环中同时有多个异步连接尝试,并且它们可能几乎同时完成,导致多个Write-Host输出同时进行,从而在控制台上产生重叠。
在PowerShell中,如果你同时启动多个异步操作(比如使用BeginConnect),然后使用WaitOne等待它们完成,但是等待时间很短,那么多个操作可能几乎同时完成,然后同时调用Write-Host,从而造成输出重叠。
为了解决这个问题,我们可以使用锁机制来确保每次只有一个线程可以执行Write-Host。但是,在PowerShell中,我们可以使用同步上下文或者使用线程安全的方式来输出。一个简单的方法是将输出操作序列化,例如使用一个锁对象。
更好的方法是使用同步锁来确保每次只有一个输出操作进行。
但是,在PowerShell中,我们可以使用[Mutex]或[System.Threading.Monitor]来锁定,但更简单的方法是使用一个共享的锁对象。
修改代码,在输出时加锁:
# 创建一个锁对象 $lockObj = New-Object System.Object# 使用锁来确保同一时间只有一个线程在执行Write-Host [System.Threading.Monitor]::Enter($lockObj) try { } finally { [System.Threading.Monitor]::Exit($lockObj) } } catch { [System.Threading.Monitor]::Enter($lockObj) try { } finally { [System.Threading.Monitor]::Exit($lockObj) } } finally { if ($tcpClient -ne $null) { $tcpClient.Close() } }但是,请注意,这个脚本实际上是在主线程上顺序执行每个连接的,所以理论上不会出现多线程同时写的问题。那么为什么会出现重叠呢?可能是因为PowerShell控制台在处理快速连续的输出时会出现显示问题。另一种可能是,在等待异步连接完成时,多个异步操作可能同时完成,然后主线程依次处理它们,但Write-Host输出可能被缓冲,然后同时显示,导致重叠。
另一种更简单的解决方案是使用Out-Host来自动处理同步,或者使用Start-Sleep稍微延迟一下输出,但这并不是保证有效的,而且不够优雅。
我怀疑问题可能不是由于多线程,而是由于控制台应用程序本身的特性。你可以尝试使用另一种输出方式,比如使用累计字符串然后一次性输出,或者使用其他方法。
使用更简单的同步连接方法,并确保每次输出之间没有其他操作干扰。
这个文字重叠的问题是由于 PowerShell 控制台输出缓冲导致的。当多个异步连接几乎同时完成时,它们的输出会同时写入控制台,造成字符重叠。以下是几种解决方案:
方案1:使用线程锁确保输出同步
$timeout = 1000 $remotetradeIP = @("180.169.70.181", "180.169.70.182", "180.169.70.183", "180.169.70.184", "140.206.102.21", "140.206.102.22", "140.206.102.23", "140.206.102.24", "180.169.70.179", "180.169.70.180", "140.206.102.19", "140.206.102.20", "180.169.30.185") $ports = 31205, 31213 # 创建同步锁对象 $lockObject = New-Object System.Object foreach ($forbideIP in $remotetradeIP) { foreach ($port in $ports) { $tcpClient = $null try { $tcpClient = New-Object System.Net.Sockets.TcpClient $asyncResult = $tcpClient.BeginConnect($forbideIP, $port, $null, $null) $success = $asyncResult.AsyncWaitHandle.WaitOne($timeout, $false) # 使用锁确保输出同步 [System.Threading.Monitor]::Enter($lockObject) try { if ($success -and $tcpClient.Connected) { $tcpClient.EndConnect($asyncResult) Write-Host "成功连接到 $forbideIP`:$port" -ForegroundColor Green } else { Write-Host "连接到 $forbideIP`:$port 超时" -ForegroundColor Yellow } } finally { [System.Threading.Monitor]::Exit($lockObject) } } catch { [System.Threading.Monitor]::Enter($lockObject) try { Write-Host "连接到 $forbideIP`:$port 失败: $($_.Exception.Message)" -ForegroundColor Red } finally { [System.Threading.Monitor]::Exit($lockObject) } } finally { if ($tcpClient -ne $null) { $tcpClient.Close() } } } }方案2:使用更简单的同步方法(推荐)
$timeout = 1000 $remotetradeIP = @("180.169.70.181", "180.169.70.182", "180.169.70.183", "180.169.70.184", "140.206.102.21", "140.206.102.22", "140.206.102.23", "140.206.102.24", "180.169.70.179", "180.169.70.180", "140.206.102.19", "140.206.102.20", "180.169.30.185") $ports = 31205, 31213 $results = @() foreach ($forbideIP in $remotetradeIP) { foreach ($port in $ports) { $tcpClient = $null try { $tcpClient = New-Object System.Net.Sockets.TcpClient # 使用同步连接避免异步问题 $connectTask = $tcpClient.ConnectAsync($forbideIP, $port) # 等待连接完成 if ($connectTask.Wait($timeout)) { if ($tcpClient.Connected) { Write-Host "成功连接到 $forbideIP`:$port" -ForegroundColor Green $results += [PSCustomObject]@{ IP = $forbideIP Port = $port Status = "成功" Timestamp = Get-Date } } else { Write-Host "连接到 $forbideIP`:$port 失败" -ForegroundColor Red } } else { Write-Host "连接到 $forbideIP`:$port 超时" -ForegroundColor Yellow $results += [PSCustomObject]@{ IP = $forbideIP Port = $port Status = "超时" Timestamp = Get-Date } } } catch { Write-Host "连接到 $forbideIP`:$port 异常: $($_.Exception.Message)" -ForegroundColor Red $results += [PSCustomObject]@{ IP = $forbideIP Port = $port Status = "异常" Error = $_.Exception.Message Timestamp = Get-Date } } finally { if ($tcpClient -ne $null) { $tcpClient.Close() } } } } # 显示汇总结果 Write-Host "`n连接测试汇总:" -ForegroundColor Cyan $results | Format-Table -AutoSize方案3:使用 Test-Connection
使用方案3,因为:
Test-Connection是 PowerShell 内置命令但是需要最新的powershell7.5及以上,因为他添加了新的参数-protocol TCP -count 123
自动处理了各种异常情况。
最后,比较一下我写的小程序和我摒弃了的telnet工具的原理是什么?
下面分别进行说明:
1. 小程序原理
我们的小程序使用的是PowerShell脚本,通过创建TcpClient对象来尝试与目标IP和端口建立TCP连接。具体步骤:
创建一个
TcpClient实例。调用
BeginConnect方法(异步)或Connect方法(同步)尝试连接。设置一个超时时间,如果在指定时间内连接成功,则说明该端口是可达的;如果超时或抛出异常,则说明连接失败。
在异步连接的情况下,我们使用BeginConnect开始一个异步连接,然后通过AsyncWaitHandle.WaitOne等待连接完成,并设置超时。如果是同步连接,则使用Connect方法并设置超时。
2. Telnet工具原理
Telnet是一个应用层协议,不要忘了,同时也是一个用于远程登录的命令行工具。当我们使用telnet工具来测试端口连通性时,它实际上使用的是TCP协议来尝试与目标主机和端口建立一个TCP连接。
在连接建立后,则会进入telnet协议交互。但我们通常只用它来测试连通性,所以一旦连接建立,我们就会断开。
# Telnet 客户端工作流程: # 1. 建立 TCP 连接(三次握手) # 2. 发送 Telnet 协议选项协商(IAC, DO, WILL, WONT 等) # 3. 等待服务器响应 # 4. 进入交互模式(如果端口是 Telnet 服务) # 当用于端口测试时: telnet 192.168.1.1 80 # 连接建立后会立即看到连接状态关键点:
工作在应用层(Telnet 协议,端口 23)
连接建立后会发送 Telnet 协议协商数据
可用于测试任何 TCP 端口
会显示连接建立的详细信息
使用场景差异
您的小程序适合:批量端口扫描,快速连通性检查(不关心协议)
一、代码示例对比
您的小程序(优化版):
# 专门用于端口连通性测试 $results = foreach ($ip in $ipList) { foreach ($port in $ports) { $tcp = New-Object Net.Sockets.TcpClient try { $task = $tcp.ConnectAsync($ip, $port) if ($task.Wait(1000)) { [PSCustomObject]@{ IP=$ip; Port=$port; Status="Open" } } else { [PSCustomObject]@{ IP=$ip; Port=$port; Status="Timeout" } } } catch { [PSCustomObject]@{ IP=$ip; Port=$port; Status="Error"; Message=$_.Message } } finally { $tcp.Close() } } }Telnet 等价功能:
# 需要结合其他工具实现批量测试 for ip in 192.168.1.1 192.168.1.2; do for port in 80 443; do timeout 1 telnet $ip $port 2>&1 | grep -q "Connected" if [ $? -eq 0 ]; then echo "$ip:$port - Open" else echo "$ip:$port - Closed" fi done done二、安全性和隐蔽性
方面 | 您的小程序 | Telnet |
|---|---|---|
流量特征 | 只建立TCP连接,无数据交换,较隐蔽 | 发送Telnet选项,易被识别 |
日志记录 | 只留下连接记录,无协议日志 | 可能留下协议协商日志 |
检测难度 | 较难被应用层IDS检测 | 容易被识别为Telnet客户端 |
三、实际使用
使用您的小程序当:
需要批量测试成百上千个端口,自动化、批量的端口连通性测试
不希望发送任何应用层数据
使用 Telnet:
需要查看服务原始响应
测试文本协议(如HTTP、SMTP)
输出结果到文本:您的小程序可以将结果保存到变量,然后输出到文本文件。例如,可以将结果保存在一个数组中,最后使用
Export-Csv命令将结果导出为CSV文件。
Telnet 不能直接输出结果到文本文件。理解telnet工具的原理,讨论不能输出结果到txt文本
三、为什么不能直接输出到文本?
1. Telnet 的交互式设计
# Telnet 的三种模式: 1. 交互模式(默认)- 等待用户输入 2. 输入模式 - 从管道接收输入 3. 脚本模式 - 但功能有限 # 问题:输出混合了控制字符 $ echo "QUIT" | telnet example.com 80 Trying 93.184.216.34... Connected to example.com. Escape character is '^]'. <HTML>...</HTML> Connection closed by foreign host. # 包含了: # - 连接状态信息 # - 转义字符提示 # - 实际数据 # - 关闭信息2. Telnet 输出到文本的问题
# 尝试输出到文件: telnet example.com 80 > output.txt # 问题1:仍然会等待用户输入 # 问题2:包含控制字符(颜色、转义序列) # 问题3:需要手动发送数据 # 问题4:不会自动退出 # 稍微好一点的方法: echo -e "GET / HTTP/1.0\r\n\r\n" | timeout 5 telnet example.com 80 2>&1 | grep -v "^Escape" > output.txt # 但还是很麻烦四、Telnet 与您的小程序对比
性能对比表:
方面 | Telnet | 您的小程序 |
|---|---|---|
连接建立 | 完整TCP握手+协议协商 | 仅TCP握手 |
默认超时 | 75-93秒 | 可自定义(如1秒) |
重传机制 | 系统TCP栈(5次重传) | 可控制的重试逻辑 |
输出控制 | 混合控制字符和用户提示 | 纯文本,格式可控 |
自动化 | 需要复杂管道和超时控制 | 原生支持自动化 |
错误处理 | 基本错误信息 | 结构化错误信息 |
1. nc - 更快的替代品
# 基本用法 nc -z -v -w 1 example.com 80 # -z: 只扫描,不发送数据 # -v: 详细输出 # -w 1: 1秒超时 # 输出到文件 nc -z -v -w 1 example.com 80 2>&1 | tee output.txt2. 您的小程序改进版
由于之前使用异步连接时出现了文字重叠的问题,我可以使用同步连接,设置较短的超时时间(1秒)来避免长时间等待。同时,将结果保存到变量,并最终导出到CSV文件。但通过作业(Jobs)或运行空间(Runspaces)来实现并行处理,以提高速度。不过,考虑到简单性,这里先使用同步连接,但会记录每个连接的结果,并最终输出到文件。下面是一个使用PowerShell作业的并行版本,并将结果输出到CSV文件:
建议使用Runspaces(更轻量级)。
# 添加超时控制 $timeout = 1000 # 1秒 # 批量测试并输出到文件 $results = @() foreach ($ip in $ipList) { foreach ($port in $ports) { $result = Test-ConnectionAsync -IP $ip -Port $port -Timeout 1000 $results += $result } } # 输出到CSV $results | Export-Csv -Path "results.csv" -NoTypeInformation七、Telnet 输出到文本的变通方案
1. 使用 timeout 命令控制
# Linux/Mac timeout 5 telnet example.com 80 2>&1 | grep -v "^Escape" > output.txt # Windows (PowerShell) $process = Start-Process telnet -ArgumentList "example.com 80" -NoNewWindow -PassThru $process | Wait-Process -Timeout 5 if (!$process.HasExited) { $process | Stop-Process } Get-Content $outputFile2. 使用 expect 自动化
#!/usr/bin/expect spawn telnet example.com 80 expect "Escape character is '^]'." send "GET / HTTP/1.0\r\n\r\n" expect timeout send "\x1D" # Ctrl+] send "quit\r" # 输出可以重定向到文件五、性能测试对比
测试脚本对比:
# 您的小程序测试 100 个端口 Measure-Command { foreach ($port in 1..100) { $tcp = New-Object Net.Sockets.TcpClient $task = $tcp.ConnectAsync("example.com", $port) $task.Wait(1000) # 1秒超时 $tcp.Close() } } # 结果: ~100 秒(并行可更快) # Telnet 测试 100 个端口 Measure-Command { foreach ($port in 1..100) { telnet example.com $port 2>&1 | Out-Null } } # 结果: ~7500 秒(75秒×100)Telnet 慢的原因:
历史包袱:为 1969 年的网络设计,当时技术落后, 因此设置了长的超时和多重重试机制,使用操作系统默认的 TCP 栈,参数无法自定义
设计目标:为交互式使用设计,非批量测试工具
综上所述:注意区别
端口测试用我的小程序或 nc
协议调试用 telnet 或专门工具,注意,他们的设计目的和我们完全不同
注意代码的elegant,不要写出那些愚笨可笑的代码。