我读过一些东西,暗示由于TCP的设计,这可能是不可能的(例如:Java socket API: How to tell if a connection has been closed?),但是我正在尝试寻找明确的确认。我有一个接受连接的基本TCP服务器,以及一个启动连接,发送消息然后关闭连接的客户端。服务器是否有办法知道客户端已关闭连接?
我发现了一些建议,可以使用内核select
命令(源:How to check if a given file descriptor stored in a variable is still valid?)以及使用{来检查套接字的文件描述符(源:https://bytes.com/topic/c/answers/866296-detecting-if-file-descriptor-closed) {1}}检查客户端是否返回0(源:http://man7.org/linux/man-pages/man2/recv.2.html#RETURN_VALUE),但是它们似乎不起作用,至少在Ruby调用时不起作用。为了测试这一点,我编写了一个基本的服务器和客户端:
recv
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
客户端发送30字节的消息,等待1秒,然后关闭连接。
服务器接受连接,在其上调用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)
和select
来检查其状态,接收消息,尝试再读取1个字节,休眠3秒,然后调用fcntl
和{ {1}},然后再次尝试读取1个字节。目的是检查客户端关闭连接之前和之后服务器是否可以看到任何更改(因此需要3秒钟的睡眠时间)。我从运行服务器然后获得客户端代码的结果是:
select
在客户端关闭连接之前和之后,fcntl
仍然认为套接字是可读的,基础文件描述符未更改,并且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!
返回空字符串(内核调用可能返回0就像手册页中指定的一样,但是Ruby正在捕获它,如果是这样,我不知道怎么看。)因此,这些似乎都不是连接是否从另一侧关闭的可靠指标。有什么我想念的吗?
我还看到了一些其他建议,可以将定期的心跳恢复给客户端,但是我想知道是否有办法避免这种情况。原因是我正在尝试解决一种情况,即客户端可能会以延迟(例如,每个字节1秒的100个字节)分隔的几段形式发送消息。如果服务器在该操作过程中发送了心跳消息并侦听OK,我认为客户端也必须也在侦听心跳并将其OK发送回,与正在进行的消息发送分开,在我的测试案例中,我无法更改客户端来执行此操作。
答案 0 :(得分:0)
我还看到了一些其他建议,可以将定期的心跳恢复给客户,但我想知道是否有办法避免这种情况。
心跳(ping)是唯一可行的解决方案。
除了试图通过有线发送数据之外,没有其他方法可以可靠地知道连接是否处于活动状态。
由于在不发送(或接收)数据时TCP / IP不需要任何流量,因此TCP堆栈(甚至在OS内核中)也无法知道连接是否处于“活动”状态,而无需尝试通过网络交换数据。
某些连接将正常关闭,从而使TCP堆栈能够识别该连接已关闭-但这并不总是正确的(您可以阅读有关"half-open" or "half-closed" connections的更多信息)。
由于这个原因,所有服务器都实施了超时/ ping机制来测试丢失的连接。
我正在尝试解决一种情况,即客户端可能会以延迟(例如,每个字节1秒的100个字节)分隔几段来发送消息...
请记住,TCP / IP是基于流的协议,而不是基于消息的协议。
这意味着您的100个字节可能会碎片化,或者可能与上一条消息合并。
如果要发送消息(而不是流数据),则需要-设计使标记消息边界。
由于必须标记这些消息边界,因此添加消息类型标记(标记ping / pong消息)变得相对容易。
您可以观察WebSocket协议消息格式,以了解有关使用TCP / IP(流式)连接的基于消息的协议设计的更多信息。