首页 > 解决方案 > 如何判断一个 TCP 套接字是否已被 Ruby 中的客户端关闭?

问题描述

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

我发现了一些建议来检查套接字的文件描述符(来源:如何检查存储在变量中的给定文件描述符是否仍然有效?),使用内核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 发送回来,与正在进行的消息发送分开,并且在我的测试用例中,我无法更改客户端来执行此操作。

标签: rubytcpserver

解决方案


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

心跳(ping)是唯一可行的解​​决方案。

除非尝试通过线路发送数据,否则无法可靠地知道连接是否处于活动状态。

由于 TCP/IP 在不发送(或接收)数据时不需要任何流量,因此 TCP 堆栈(甚至在 OS 内核中)无法在不尝试交换的情况下知道连接是否“活动”数据通过电线。

一些连接将正常关闭,允许 TCP 堆栈识别连接已关闭 - 但这并不总是正确的(您可以阅读有关“半打开”或“半关闭”连接的更多信息)。

出于这个原因,所有服务器都实现了超时/ping 机制来测试丢失的连接。

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

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

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

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

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

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


推荐阅读