Тестируйте сетевые порты быстрее с помощью PowerShell

Вопрос или проблема

В Windows 8.1 командлет Test-NetConnection полезен для проверки состояния сетевого порта на удаленной системе. Однако иногда он может быть излишне медленным. Мне хотелось бы знать, есть ли какие-либо параметры, которые я мог бы настроить, или альтернативная команда PowerShell, которую я мог бы использовать, чтобы ускорить этот процесс.

Test-NetConnection может занять около 10 секунд для получения результатов, если удаленная система не отвечает. Всякий раз, когда указывается порт, выполняются два теста подключения, каждый из которых занимает около 5 секунд для таймаута. Первый тест — это базовая проверка ICMP эха. Этот тест завершится таймаутом, если система находится в оффлайне или если она (или любое промежуточное оборудование) настроено на блокировку или отсутствие ответа на ICMP эхо-запросы. Второй тест — это фактическая проверка указанного порта. Этот тест завершится таймаутом, если система находится в оффлайне или если на пути есть межсетевой экран, который блокирует порт.

В моем текущем случае удаленная система находится всего в двух прыжках по надежному соединению Gigabit Ethernet. Поэтому таймаут в пять секунд для любого запроса является чрезмерным – я, вероятно, все еще мог бы получать надежные результаты с таймаутом 30 мс или меньше! Более того, известно, что система не отвечает на ICMP эхо, хотя она может быть в сети и иметь все другие доступные сервисы. Поэтому было бы здорово, если бы я мог обойтись без теста ICMP эха и сократить таймаут для теста TCP-соединения, чтобы ускорить свои скрипты, использующие Test-NetConnection для этой цели.

Есть ли у Test-NetConnection параметры для изменения этого поведения? (Я прочитал детальный справочный файл, и ответ, кажется, отрицательный – но было бы хорошо, если бы мне сказали, что я что-то упустил.) Или есть ли другой способ, с помощью которого я могу использовать PowerShell для выполнения тех же проверок быстрее?

По различным причинам я предпочитаю ограничивать свои скрипты использованием встроенного функционала Операционной Системы, где это возможно. Предположим, что среда – это свежая версия Windows 8.1, с примененными всеми соответствующими обновлениями Windows, и сторонние инструменты не являются вариантом.

Очень простая (таймаут 100 мс):

function testport ($hostname="yahoo.com",$port=80,$timeout=100) {
  $requestCallback = $state = $null
  $client = New-Object System.Net.Sockets.TcpClient
  $beginConnect = $client.BeginConnect($hostname,$port,$requestCallback,$state)
  Start-Sleep -milli $timeOut
  if ($client.Connected) { $open = $true } else { $open = $false }
  $client.Close()
  [pscustomobject]@{hostname=$hostname;port=$port;open=$open}
}

testport

hostname  port  open
--------  ----  ----
yahoo.com   80  True

Самый короткий способ протестировать TCP порт, который я видел, это:

(New-Object System.Net.Sockets.TcpClient).ConnectAsync("google.com", 80).Wait(100)

или

[System.Net.Sockets.TcpClient]::new().ConnectAsync("google.com", 80).Wait(100) 

#Если вас беспокоит очистка, просто используйте код вроде этого:
$t = (New-Object System.Net.Sockets.TcpClient)
$t.ConnectAsync("google.com", 80).Wait(100)
$t.Close()

Wait в мс. Это метод, совместимый с PowerShell 2.0.

Вы можете использовать это для проверки соединения – Взято из Репозитория кода PowerShell (автор ‘BSonPosh’):

“Test-Port создает TCP соединение с указанным портом. По умолчанию он подключается к порту 135 с таймаутом 3 секунды.”

Param([string]$srv,$port=135,$timeout=3000,[switch]$verbose)

# Test-Port.ps1
# Выполняет TCP соединение на указанном порту (135 по умолчанию)

$ErrorActionPreference = "SilentlyContinue"

# Создаем TCP клиент
$tcpclient = new-Object system.Net.Sockets.TcpClient

# Указываем TCP клиенту подключиться к машине на порту
$iar = $tcpclient.BeginConnect($srv,$port,$null,$null)

# Установите время ожидания
$wait = $iar.AsyncWaitHandle.WaitOne($timeout,$false)

# Проверяем, завершено ли соединение
if(!$wait)
{
    # Закрываем соединение и сообщаем о таймауте
    $tcpclient.Close()
    if($verbose){Write-Host "Таймаут соединения"}
    Return $false
}
else
{
    # Закрываем соединение и сообщаем об ошибке, если она есть
    $error.Clear()
    $tcpclient.EndConnect($iar) | out-Null
    if(!$?){if($verbose){write-host $error[0]};$failed = $true}
    $tcpclient.Close()
}

# Возвращаем $true, если соединение установлено, в противном случае $False
if($failed){return $false}else{return $true}

Вы можете перейти на страницу этого репозитория для дальнейших вопросов (этот ответ уже слишком скопирован).

Взяв ответ @Jan, я сделал его менее громоздким, и теперь он работает для меня в запущенных задачах.
Хорошо выбрасывать исключения и не полагаться на $error/Пользователей API, использующих нестандартные вещи “verbose” (при получении .Connected создается SocketException, что удобно).

        Function TestTCP { Param($address, $port, $timeout=2000)
            $socket=New-Object System.Net.Sockets.TcpClient
            try {
                $result=$socket.BeginConnect($address, $port, $NULL, $NULL)
                if (!$result.AsyncWaitHandle.WaitOne($timeout, $False)) {
                    throw [System.Exception]::new('Таймаут соединения')
                }
                $socket.EndConnect($result) | Out-Null
                $socket.Connected
            }
            finally {
                $socket.Close()
            }
        }

Я искал супер быстрый способ пинговать много IP-адресов и наткнулся на этот вопрос (среди прочих).

В конце концов, я нашел скрипт, который было легко интегрировать в то, что я хотел сделать. Парень называет это Быстрый асинхронный пинг.

Даже будучи новичком в PowerShell, я смог пропустить его вывод, а затем модифицировать его вывод, чтобы включить только то, что я хотел. Я наткнулся на другие скрипты, но не смог расшифровать их скрипты, чтобы изменить их для своих нужд.

Я не уверен, какая версия PowerShell это требует, но это работает на v4 и v5.

Я видел большинство скриптов сканеров IP в Powershell, пингующих скриптов
но ни один из них не использует метод PingASync. Проблема с
синхронными скриптами в том, что они должны ждать ответа узла или
таймаута, прежде чем продолжить к следующему адресу. Использование этого подхода
может занять больше времени.

function Global:Ping-IPRange {
    <#
    .SYNOPSIS
        Отправляет пакеты ICMP эхо-запросов на диапазон IPv4 адресов между двумя указанными адресами.

    .DESCRIPTION
        Эта функция позволяет вам отправлять пакеты ICMP эхо-запросов ("пинги") на 
        диапазон IPv4 адресов с использованием асинхронного метода.

        Поэтому эта техника очень быстрая, но приходит с предупреждением.
        Пингование большого подсети или сети с множеством переключателей может привести к 
        всплеску широковещательного трафика.
        Используйте параметр -Interval, чтобы настроить время между каждым запросом пинга.
        Например, интервал в 60 миллисекунд подходит для беспроводных сетей.
        Параметр RawOutput переключает вывод на неформатированный
        [System.Net.NetworkInformation.PingReply[]].

    .INPUTS
        Ничего
        Вы не можете передать ввод в эту функцию.

    .OUTPUTS
        Функция возвращает вывод только от успешных пингов.

        Тип: System.Net.NetworkInformation.PingReply

        Параметр RawOutput переключает вывод на неформатированный
        [System.Net.NetworkInformation.PingReply[]].

    .NOTES
        Автор  : G.A.F.F. Jakobs
        Создан : 30 августа 2014
        Версия : 6

    .EXAMPLE
        Ping-IPRange -StartAddress 192.168.1.1 -EndAddress 192.168.1.254 -Interval 20

        IPAddress                                 Bytes                     Ttl           ResponseTime
        ---------                                 -----                     ---           ------------
        192.168.1.41                                 32                      64                    371
        192.168.1.57                                 32                     128                      0
        192.168.1.64                                 32                     128                      1
        192.168.1.63                                 32                      64                     88
        192.168.1.254                                32                      64                      0

        В этом примере все IP-адреса между 192.168.1.1 и 192.168.1.254 пингуются с 
        20 миллисекундным интервалом между каждым запросом.
        Все адреса, которые отвечают на запрос пинга, перечислены.

    .LINK
        http://gallery.technet.microsoft.com/Fast-asynchronous-ping-IP-d0a5cf0e

    #>
    [CmdletBinding(ConfirmImpact="Low")]
    Param(
        [parameter(Mandatory = $true, Position = 0)]
        [System.Net.IPAddress]$StartAddress,
        [parameter(Mandatory = $true, Position = 1)]
        [System.Net.IPAddress]$EndAddress,
        [int]$Interval = 30,
        [Switch]$RawOutput = $false
    )

    $timeout = 2000

    function New-Range ($start, $end) {

        [byte[]]$BySt = $start.GetAddressBytes()
        [Array]::Reverse($BySt)
        [byte[]]$ByEn = $end.GetAddressBytes()
        [Array]::Reverse($ByEn)
        $i1 = [System.BitConverter]::ToUInt32($BySt,0)
        $i2 = [System.BitConverter]::ToUInt32($ByEn,0)
        for($x = $i1;$x -le $i2;$x++){
            $ip = ([System.Net.IPAddress]$x).GetAddressBytes()
            [Array]::Reverse($ip)
            [System.Net.IPAddress]::Parse($($ip -join '.'))
        }
    }

    $IPrange = New-Range $StartAddress $EndAddress

    $IpTotal = $IPrange.Count

    Get-Event -SourceIdentifier "ID-Ping*" | Remove-Event
    Get-EventSubscriber -SourceIdentifier "ID-Ping*" | Unregister-Event

    $IPrange | foreach{

        [string]$VarName = "Ping_" + $_.Address

        New-Variable -Name $VarName -Value (New-Object System.Net.NetworkInformation.Ping)

        Register-ObjectEvent -InputObject (Get-Variable $VarName -ValueOnly) -EventName PingCompleted -SourceIdentifier "ID-$VarName"

        (Get-Variable $VarName -ValueOnly).SendAsync($_,$timeout,$VarName)

        Remove-Variable $VarName

        try{

            $pending = (Get-Event -SourceIdentifier "ID-Ping*").Count

        }catch [System.InvalidOperationException]{}

        $index = [array]::indexof($IPrange,$_)

        Write-Progress -Activity "Отправка пинга на" -Id 1 -status $_.IPAddressToString -PercentComplete (($index / $IpTotal)  * 100)

        Write-Progress -Activity "Ожидаются ICMP запросы" -Id 2 -ParentId 1 -Status ($index - $pending) -PercentComplete (($index - $pending)/$IpTotal * 100)

        Start-Sleep -Milliseconds $Interval
    }

    Write-Progress -Activity "Завершено отправка пинг запросов" -Id 1 -Status 'Ожидание' -PercentComplete 100 

    While($pending -lt $IpTotal){

        Wait-Event -SourceIdentifier "ID-Ping*" | Out-Null

        Start-Sleep -Milliseconds 10

        $pending = (Get-Event -SourceIdentifier "ID-Ping*").Count

        Write-Progress -Activity "Ожидаются ICMP запросы" -Id 2 -ParentId 1 -Status ($IpTotal - $pending) -PercentComplete (($IpTotal - $pending)/$IpTotal * 100)
    }

    if($RawOutput){

        $Reply = Get-Event -SourceIdentifier "ID-Ping*" | ForEach { 
            If($_.SourceEventArgs.Reply.Status -eq "Success"){
                $_.SourceEventArgs.Reply
            }
            Unregister-Event $_.SourceIdentifier
            Remove-Event $_.SourceIdentifier
        }

    }else{

        $Reply = Get-Event -SourceIdentifier "ID-Ping*" | ForEach { 
            If($_.SourceEventArgs.Reply.Status -eq "Success"){
                $_.SourceEventArgs.Reply | select @{
                      Name="IPAddress"   ; Expression={$_.Address}},
                    @{Name="Bytes"       ; Expression={$_.Buffer.Length}},
                    @{Name="Ttl"         ; Expression={$_.Options.Ttl}},
                    @{Name="ResponseTime"; Expression={$_.RoundtripTime}}
            }
            Unregister-Event $_.SourceIdentifier
            Remove-Event $_.SourceIdentifier
        }
    }
    if($Reply -eq $Null){
        Write-Verbose "Ping-IPrange : Ни один IP адрес не ответил" -Verbose
    }

    return $Reply
}

Еще один более быстрый способ может быть:

param($ip,$port)
New-Object System.Net.Sockets.TCPClient -ArgumentList $ip, $port

Результат будет:

Client              : System.Net.Sockets.Socket
Available           : 0
Connected           : True
ExclusiveAddressUse : False
ReceiveBufferSize   : 65536
SendBufferSize      : 65536
ReceiveTimeout      : 0
SendTimeout         : 0
LingerState         : System.Net.Sockets.LingerOption
NoDelay             : False

Интересующее значение – “Connected”

редактировать: еще одна причина: Test-NetConnection работает только с PowerShell v5 (если я правильно помню), в то время как это решение работает с v2 🙂

Вот мой ввод, спасибо за код, который помог мне начать. Весь код задокументирован внутри

PS> help Test-PortScan -examples

function Test-PortScan
{
<#
    .SYNOPSIS
        Проверяет, открыт ли порт или диапазон
    
    .DESCRIPTION
        Подробное описание функции Test-PortScan.
    
    .PARAMETER Devices
        IP или имя хоста устройств для тестирования. Он будет принимать значение из конвеера, чтобы вы могли тестировать несколько устройств одновременно.
    
    .PARAMETER StartPort
        Начальный номер порта для тестирования. По умолчанию это 1
    
    .PARAMETER EndPort
        Последний порт диапазона для сканирования. По умолчанию это то же, что и начальный порт, так что если вы хотите ввести только 1 порт, вам не нужно указывать эту переменную.
    
    .PARAMETER Timeout
        Длина таймаута для тестирования соединения в миллисекундах. По умолчанию 100 миллисекунд.
    
    .EXAMPLE
        Test-PortScan -devices 192.168.1.1 -StartPort 20 -EndPort 82
        Тестирует устройство на 192.168.1.1 на портах 20 - 82
    
    .EXAMPLE
        Test-PortScan -Devices 192.168.1.1 -StartPort 80
            Тестирует устройство на 192.168.1.1 для порта 80 (также Test-PortScan 192.168.1.1 80)
    
    .EXAMPLE
        '192.168.1.1','192.168.1.2','122.168.1.3' | % {Test-PortScan -Devices $_ StartPort 22 25}
        Тестирует каждый из этих времен IP-адресов на портах 22 - 25
    
    .NOTES
        # Исходный код найден здесь:  https://superuser.com/questions/805621/test-network-ports-faster-with-powershell
    #>
    
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true,
                   ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 0,
                   HelpMessage="Вы должны предоставить как минимум имя устройства или IP")]
        [Alias('Name')]
        [string]$Devices,
        [Parameter(Position = 1)]
        [int]$StartPort = 1,
        [Parameter(Position = 2)]
        [int]$EndPort = $StartPort,
        [Parameter(Position = 3)]
        [int]$Timeout = 100
    )

    BEGIN
    {
        $PSObject = @()
        $Output = @()
    }
    PROCESS
    {
        foreach ($Device in $Devices)
        {
            Write-Verbose "Еще не дошли до цикла портов"
            $Port = $StartPort
            do
            {
                #($Port = $StartPort; $Port -gt $EndPort; $Port++)              
                Write-Verbose "Дошли до цикла портов"
                $requestCallback = $state = $null
                $client = New-Object System.Net.Sockets.TcpClient
                $beginConnect = $client.BeginConnect($Device, $Port, $requestCallback, $state)
                Start-Sleep -Milliseconds $Timeout
                if ($client.Connected) { $Open = $true }
                else { $Open = $false }
                $client.Close()
                [pscustomobject]@{
                    'Компьютер' = $Device
                    'Порт'     = $Port
                    'Открыт'     = $Open
                }
                $Port += 1
            }
            until ($Port -gt $EndPort)
        }
        
    }
    END
    {
        
    }
}

Расширяя почти идеальный ответ @Brian McMahon, который имеет проблему открытия TCP соединения, но никогда не закрывает его. Google может это вынести, но другие не могут и блокируют после нескольких соединений, пока все соединения не истекут. Продукты Shelly — один из примеров, не закрывая соединение, вы вызываете поведение «Нет нового TCP соединения для вас!» , пока еще открыт таймаут.
Таким образом, исправление выглядит следующим образом:

$Shelly = "192.168.123.45"
$TCPClient = [System.Net.Sockets.TcpClient]::new()
$Result = $TCPClient.ConnectAsync($Shelly, 80).Wait(250)
$TCPClient.Close()

Где $Result — это либо True либо False.

И еще одна версия на выбор 🙂

Эта версия проверит, смогут ли несколько различных исходных хостов установить подключение к конкретному удаленному хосту.

'server1', 'server2' | foreach {
  Invoke-Command $_ {
    $socket = New-Object System.Net.Sockets.TcpClient
    try {
      @{'TcpConnect' = $socket.ConnectAsync('remotehost',80).Wait(100)}
    }
    finally {
      $socket.close > $null
    }
  }
} | ConvertTo-Json | ConvertFrom-Json | ft

Пример benefited from a catch statement as well.

Ответ или решение

Вопрос о том, как быстрее тестировать сетевые порты с помощью PowerShell, актуален для многих специалистов в области IT. В частности, в Windows 8.1 часто используется командлет Test-NetConnection, однако его скорость может быть не самой оптимальной, особенно в ситуациях, когда требуется мгновенная проверка состояния порта на удаленной системе. Давайте разберем, как улучшить производительность тестирования портов и какие альтернативы можно использовать.

1. Проблемы с производительностью Test-NetConnection

Основная проблема, с которой сталкиваются пользователи командлета Test-NetConnection, заключается в двух последовательных тестах:

  • Проверка ICMP: Этот тест занимает примерно 5 секунд и может не срабатывать, если удаленная система или промежуточная сеть блокирует ICMP-эхо-запросы.
  • TCP-подключение: Следующий тест также может затянутся до 5 секунд, если порт закрыт или заблокирован.

С учетом того, что система находится всего в двух хопах на надежном гигабитном Ethernet-соединении, эти тайм-ауты кажутся чрезмерными. Некоторые пользователи уверены, что достаточно установить тайм-аут в 30 мс или меньше для получения надежных результатов, что подчеркивает необходимость в оптимизации.

2. Альтернативный подход с использованием TcpClient

Чтобы обойти ограничения Test-NetConnection, мы можем использовать класс TcpClient из .NET для выполнения асинхронных подключений. Вот пример функции, которая проверяет доступность порта:

function Test-Port {
    param (
        [string]$hostname = "yahoo.com",
        [int]$port = 80,
        [int]$timeout = 100
    )

    $client = New-Object System.Net.Sockets.TcpClient
    $result = $client.ConnectAsync($hostname, $port).Wait($timeout)

    $client.Close()
    return $result
}

# Пример использования
Test-Port -hostname "google.com" -port 80

3. Использование асинхронного подключения

Если нужно протестировать несколько портов или устройств, можно расширить логику с помощью асинхронного подключения:

function Test-PortScan {
    param (
        [string[]]$Devices,
        [int]$StartPort = 1,
        [int]$EndPort = 65535,
        [int]$Timeout = 100
    )

    foreach ($Device in $Devices) {
        for ($Port = $StartPort; $Port -le $EndPort; $Port++) {
            $client = New-Object System.Net.Sockets.TcpClient
            try {
                $result = $client.ConnectAsync($Device, $Port).Wait($Timeout)
                [PSCustomObject]@{Host = $Device; Port = $Port; Open = $result}
            }
            catch {
                [PSCustomObject]@{Host = $Device; Port = $Port; Open = $false}
            }
            finally {
                $client.Close()
            }
        }
    }
}

# Пример использования
Test-PortScan -Devices "192.168.1.1" -StartPort 80 -EndPort 82

4. Улучшение производительности скрипта

  • Используйте параллельные вызовы с помощью Start-Job или Parallel в PowerShell 7+, чтобы одновременно проверять несколько хостов и портов.
  • Разумно устанавливать минимальные тайм-ауты, чтобы избежать лишней нагрузки на сетевую инфраструктуру.

Заключение

Использование TcpClient вместо Test-NetConnection позволяет значительно ускорить проверку доступности портов на удаленных системах. Быстрая проверка портов не только улучшает общую производительность скриптов, но и упрощает диагностику проблем. Для достижения наилучших результатов рекомендуется использовать асинхронные методы и оптимизировать сценарии, учитывая архитектуру вашей сети.

Оцените материал
Добавить комментарий

Капча загружается...