Ruby - 检查端口是否打开

48

我需要一个快速的方法来确定一个给定的端口是否在Ruby中打开。目前我正在尝试以下代码:

require 'socket'

def is_port_open?(ip, port)
  begin
    TCPSocket.new(ip, port)
  rescue Errno::ECONNREFUSED
    return false
  end
  return true
end

如果端口是开放的,它能很好地工作。但这种方法的缺点是,有时会出现程序长时间等待10-20秒然后超时,抛出一个ETIMEOUT异常(如果端口关闭)。我的问题如下:

这段代码能否被修改为只等待一秒钟(如果在那之前没有收到任何返回,则返回false),或者是否有更好的方法来检查给定主机上的给定端口是否开放?

编辑:调用bash代码也是可以接受的,只要它能跨平台运行(例如:Mac OS X,*nix和Cygwin),尽管我更喜欢Ruby代码。

8个回答

55

可能以下类似的代码可以实现:

require 'socket'
require 'timeout'

def is_port_open?(ip, port)
  begin
    Timeout::timeout(1) do
      begin
        s = TCPSocket.new(ip, port)
        s.close
        return true
      rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
        return false
      end
    end
  rescue Timeout::Error
  end

  return false
end

我在这个阻塞问题上遇到了一些麻烦(我想)。基本上,超时并没有真正超时。不确定为什么,但是netcat解决方案在这种情况下效果很好。 - Mat Schaffer
2
这个答案的解决方案也适用于Windows:http://stackoverflow.com/a/3473208/362951 - mit
2
真/假不应该交换吗? - Leopd
加入一些命令行选项,你就有一个功能完备的程序了! - FilBot3
请注意这段代码。Timeout::timeout非常糟糕,虽然在较小的脚本中它可能“工作”,但在更大的应用程序中会引入难以跟踪的错误。一些阅读材料:https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/ - zeroDivisible

30

7
这段代码对我来说完美运行:port_is_open = Socket.tcp(host, port, connect_timeout: 5) { true } rescue false。很容易从这个一行命令扩展出所需的特定异常捕获。 - anothermh

27

更多Ruby惯用语法:

require 'socket'
require 'timeout'

def port_open?(ip, port, seconds=1)
  Timeout::timeout(seconds) do
    begin
      TCPSocket.new(ip, port).close
      true
    rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
      false
    end
  end
rescue Timeout::Error
  false
end

这对于输入的'192.0.2.0',80,10给出了一个错误的正面结果,这些应该是无效的(根据https://dev59.com/zmkv5IYBdhLWcg3wpCTJ)。我在Mac上使用Ruby 1.9.3p448和2.0.0p195得到了相同的结果。在什么情况下,这种方法能够返回false?(我甚至尝试在关闭它之前向套接字写入,但仍然返回true!) - glyn
我认为这个函数定义在超时之前缺失了一个begin语句,或者说那是可选的吗?(rescue Timeout::Error 应该在begin块中,不是吗?) - nash
Ruby允许救援子句属于方法本身。实际上,该方法是一个隐式块。 - Wayne Conrad
1
Timeout块放在内部的begin...rescue块中,以相同的缩进级别分析错误,这样不是更好吗! - caesarsol

16

我最近提出了这个解决方案,利用了unix的lsof命令:

def port_open?(port)
  !system("lsof -i:#{port}", out: '/dev/null')
end

2
这对我来说非常好。我想引入一个将端口分配给Vagrant虚拟机的系统,并编写了以下一行代码来检查要分配的端口是否已开放:vms ['port'] + = 1,同时ports.include?vms ['port']或system(“ lsof -i:#{vms ['port']}”) - Dannid
1
这仅适用于已登录的用户。要在整个系统中运行,请使用 sudo lsof -i:<port> - alpinweis
我必须删除 !(非运算符)才能使其正常工作。 - max pleaner
“open” 在这里的意思是 “正在使用”,因此当端口“空闲”时,它将返回 false。 - grosser

9

为了完整起见,Bash 脚本可能是这样的:

$ netcat $HOST $PORT -w 1 -q 0 </dev/null && do_something

-w 1 指定了1秒的超时时间,-q 0 表示当连接成功后,只要 stdin 给出 EOF,就关闭连接(/dev/null 可以直接返回 EOF)。

Bash 还有自己内置的 TCP/UDP 服务,但这是编译时选项,我没有编译 Bash 这个选项 :P


1
它们非常简单:只需假装 /dev/{tcp}/HOST/PORT 是文件 :) - ephemient
2
为了以后参考,我在我的系统上发现这个命令是 nc 而不是 netcat - HXCaine
1
警告:在MacOS X上,这会出现错误“nc:无效选项--q”。以下内容适用于MacOS X和Ubuntu,并且对我来说似乎更简单:nc -z $HOST $PORT - mercurial

1

所有*nix平台:

尝试使用以下命令的nc / netcat命令。

`nc -z -w #{timeout_in_seconds} -G #{timeout_in_seconds} #{host} #{port}`
if $?.exitstatus == 0
  #port is open
else
  #refused, port is closed
end

-z选项可用于告诉nc报告打开的端口,而不是发起连接。

-w选项表示连接和最终网络读取的超时时间

-G选项是连接超时时间,以秒为单位

使用-n选项可以使用IP地址而不是主机名。

示例:

# `nc -z -w 1 -G 1 google.com 80`
# `nc -z -w 1 -G 1 -n 123.234.1.18 80`

1

我对Chris Rice的答案稍作修改。仍然可以处理单次尝试超时,但也允许多次重试直到放弃。

    def is_port_open?(host, port, timeout, sleep_period)
      begin
        Timeout::timeout(timeout) do
          begin
            s = TCPSocket.new(host, port)
            s.close
            return true
          rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
            sleep(sleep_period)
            retry
          end
        end
      rescue Timeout::Error
        return false
      end
    end

1
我的解决方案源自已发布的解决方案。
require 'socket'
def is_port_open?(ip, port)
  begin
    s = Socket.tcp(ip, port, connect_timeout: 5)
    s.close
    return true
  rescue => e
    # possible exceptions:
    # - Errno::ECONNREFUSED
    # - Errno::EHOSTUNREACH
    # - Errno::ETIMEDOUT
    puts "#{e.class}: #{e.message}"
    return false
  end
end

这是一个更好的答案,谢谢! - jrochkind

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接