【发布时间】: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,然后关闭连接。
服务器接受连接,调用select 和fcntl 以检查其状态,接收消息,尝试再读取1 个字节,休眠3 秒,然后调用select 和fcntl 并再次尝试读取 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 发送回来,与正在进行的消息发送分开,并且在我的测试用例中,我无法更改客户端来执行此操作。
【问题讨论】: