【问题标题】:How to tell if a TCP socket has been closed by the client in Ruby?如何判断一个 TCP 套接字是否已被 Ruby 中的客户端关闭?
【发布时间】:2020-01-02 23:22:40
【问题描述】:

我读过一些东西,暗示由于 TCP 的设计,这可能是不可能的(例如:Java socket API: How to tell if a connection has been closed?),但我正在努力寻找明确的确认。我有一个接受连接的基本 TCP 服务器,以及一个启动连接、发送消息然后关闭连接的客户端。有没有办法让服务器知道客户端关闭了连接?

我发现了一些建议来检查套接字的文件描述符(来源:How to check if a given file descriptor stored in a variable is still valid?),使用内核select 命令(来源:https://bytes.com/topic/c/answers/866296-detecting-if-file-descriptor-closed)以及使用recv 检查是否客户端返回 0(来源:http://man7.org/linux/man-pages/man2/recv.2.html#RETURN_VALUE),但这些似乎不起作用,至少在 Ruby 调用时不起作用。为了测试这一点,我编写了一个基本的服务器和客户端:

test_server.rb

require 'socket'
require 'fcntl'

TIMEOUT = 5
server = TCPServer.new('localhost', 8080)

puts "Starting server"
loop do
  client = server.accept
  puts "New client: #{client}"
  puts "** before closed #{Time.now.to_i} closed=#{client.closed?}"
  result = IO.select([client], nil, nil, TIMEOUT)
  puts "select result=#{result}"

  fd = client.fcntl(Fcntl::F_GETFD, 0)
  puts "client fd=#{fd}"

  stuff = client.recv(30)
  puts "received '#{stuff}'"

  begin
    r = client.recv(1)
  rescue => e
  end
  puts "received #{r} nil?=#{r.nil?}"

  sleep 3

  puts "** after closed #{Time.now.to_i} closed=#{client.closed?}"
  result = IO.select([client], nil, nil, TIMEOUT)
  puts "select result=#{result}"

  fd = client.fcntl(Fcntl::F_GETFD, 0)
  puts "client fd=#{fd}"

  begin
    r = client.recv(1)
  rescue => e
  end
  puts "received #{r} nil?=#{r.nil?}"
  puts "done!"
end

test_client.rb

require 'socket'

class Client
  def initialize
    @socket = tcp_socket
  end

  def tcp_socket
    Thread.current[:socket] = TCPSocket.new("localhost", 8080)
  end

  def send(s, args={})
    puts "sending str '#{s}'"
    nbytes = @socket.send(s, 0)
    puts "received #{nbytes} bytes"

    sleep 1
    @socket.close
    puts "done at #{Time.now.to_i}: #{@socket.closed?}"
  end
end

msg = 'hello world this is my message'
server = Client.new
server.send(msg)

客户端发送一个 30 字节的消息,等待 1s,然后关闭连接。 服务器接受连接,调用selectfcntl 以检查其状态,接收消息,尝试再读取1 个字节,休眠3 秒,然后调用selectfcntl 并再次尝试读取 1 个字节。这里的目的是检查在客户端关闭连接之前和之后服务器是否可以看到任何变化(因此是 3 秒睡眠)。我运行服务器然后客户端代码得到的结果是:

Starting server
New client: #<TCPSocket:0x00007fa0930f0880>
** before closed 1578005539 closed=false
select result=[[#<TCPSocket:fd 10>], [], []]
client fd=1
received 'hello world this is my message'
received  nil?=false
** after closed 1578005543 closed=false
select result=[[#<TCPSocket:fd 10>], [], []]
client fd=1
received  nil?=false
done!

在客户端关闭连接之前和之后,select 仍然认为套接字是可读的,底层文件描述符没有改变,recv 返回空字符串(内核调用可能返回 0,如手册页,但 Ruby 正在捕获它,如果是这样,我不知道如何查看它。)。因此,这些似乎都不是连接是否从另一端关闭的可靠指标。我有什么遗漏吗?

我已经看到了一些其他建议,将定期心跳合并回客户端,但我想知道是否有办法避免这种情况。原因是我试图适应这样一种情况,即客户端可能会发送由延迟分隔的几条消息(例如,100 字节,每个字节 1 秒)。如果服务器在该操作中间发送心跳消息并侦听 OK,我假设客户端也必须侦听心跳并将其 OK 发送回来,与正在进行的消息发送分开,并且在我的测试用例中,我无法更改客户端来执行此操作。

【问题讨论】:

    标签: ruby tcp server


    【解决方案1】:

    我看到了一些其他建议,可以将定期心跳合并回客户端,但我想知道是否有办法避免这种情况。

    检测信号 (ping) 是唯一可行的解​​决方案。

    除非尝试通过网络发送数据,否则无法可靠地知道连接是否有效。

    由于 TCP/IP 在不发送(或接收)数据时不需要任何流量,因此 TCP 堆栈(甚至在操作系统内核中)无法知道连接是否“有效”试图通过网络交换数据。

    一些连接会正常关闭,允许 TCP 堆栈识别连接已关闭 - 但这并不总是正确的(您可以阅读更多关于 "half-open" or "half-closed" connections 的信息)。

    因此,所有服务器都实施了超时/ping 机制来测试丢失的连接。

    我正在尝试适应这样一种情况,即客户端可能会以延迟分隔的几段发送消息(例如,100 个字节,每个字节 1 秒)...

    请记住,TCP/IP 是基于流的协议,而不是基于消息的协议。

    这意味着您的 100 个字节可能会分段到达,或者它们可能与之前的消息组合在一起。

    如果您要发送消息(而不是流式数据),则需要 - 根据设计 - 标记消息边界。

    由于必须标记这些消息边界,因此添加消息类型标记(用于标记 ping/pong 消息)变得相对容易。

    您可以观察 WebSocket 协议消息格式,以了解有关使用 TCP/IP(流式)连接的基于消息的协议设计的更多信息。

    【讨论】:

      猜你喜欢
      • 2010-10-15
      • 2012-10-07
      • 1970-01-01
      • 2014-02-16
      • 2010-12-20
      • 1970-01-01
      • 1970-01-01
      • 2013-12-31
      • 1970-01-01
      相关资源
      最近更新 更多