【问题标题】:Read entire message from a TCPSocket without hanging从 TCPSocket 读取整个消息而不挂起
【发布时间】:2021-11-08 09:41:41
【问题描述】:

我在 Ruby 3.0.2 中组装了一个 TCPServer,我发现我似乎无法在不阻塞的情况下读取整个数据包(直到套接字关闭)。

编辑:我试图做的事情有些混乱 - 我的错 - 所以只是为了帮助澄清:我想阅读到目前为止通过 TCP 连接发送的所有内容。 (结束编辑)

我的第一次尝试是:

#!/snap/bin/ruby
require 'socket'

server = TCPServer.new('localhost', 4200)

loop {
  Thread.start(server.accept) do |connection|
    puts connection.gets  # The important line
  end
}

但这会一直挂起,直到客户端关闭连接。好的,所以我看一下 connection.methods 和 ruby​​ 文档,并尝试了一堆看起来很有希望的选项。基本上,有两种读取方法:阻塞和非阻塞。

我尝试的阻塞方法是.read.gets.readlines.readline.recv.recvmsg。现在.read.readlines.gets 都挂起(直到套接字关闭)——所以这没有帮助。其他的(例如.readlinerecv 方法)不会阅读整个消息。现在,我可以读取每一行,直到看到一个空行并从那里解析 HTTP 标头。但是必须有更好的方法;我不想担心收到损坏的消息并挂起,因为我没有读取标题末尾的空行。

所以我去看了非阻塞选项。特别是.recv_nonblock.recvmsg_nonblock。这两个抛出错误(资源暂时不可用 - recvfrom(2) 会阻塞资源暂时不可用 - recvmsg(2) 分别)。

对可能发生的事情有任何想法吗?我认为它与我使用 Ruby 3 有关系,因为在 Ruby 2.5 上尝试代码,client.gets 返回一行(不会挂起),尽管 .readlines 确实挂起 - 所以不会确定发生了什么。

理想情况下,我可以像 client.get_message 那样调用一些东西,我会得到已发送的整个消息,但我也可以在 TCP 级别工作并获取数据包大小,阅读大小,并从那里重建消息。

【问题讨论】:

  • TCP 没有消息的概念。它只是一个字节流。您必须在此字节流之上定义消息语义,例如使用长度前缀或消息结束标记等。
  • 我想我正在寻找的是一种阅读到目前为止已发送的所有内容的方法。我正在阅读 HTTP 消息,因此我可以解析字段并以这种方式进行操作 - 只是惊讶地没有一种简单的方法来读取它在需要挂起时返回。
  • "我正在阅读 HTTP 消息" - HTTP 具有明确定义的消息格式。如果正文是通过 Content-Length 在 HTTP 标头中给出的,或者在传输编码在每个正文块之前分块的情况下给出的长度。标头本身在其结束位置(空行)有一个清晰的标记。不要错误地猜测协议是如何工作的,然后想知道为什么它这么难。而是查看实际标准并使用为处理特定协议而创建的库。
  • “读取整个消息......读取整个数据包......” - 您混淆了概念,这些概念实际上与 TCP 无关。消息不是 TCP 中的数据包(尽管它会在 UDP 中)。同一条消息可以通过网络上的多个数据包传递,多个消息可以放入同一个数据包等。

标签: ruby server tcp ruby-3


【解决方案1】:

感谢所有评论/回答的人,但我找到了我认为是 Socket 类的创建者想要的解决方案!

recv_nonblock 方法接受一些可选参数 - 其中一个是 Socket 将存储它已读取内容的缓冲区。因此,像client.recv_nonblock(1000, 0, buffer) 这样的调用将多达 1000 个字符从 Socket 存储到 buffer,然后退出而不是阻塞。

为了方便起见,我为 TCPSocket 类做了一个猴子补丁:

class TCPSocket

  def eat_buffer
    contents = ''
    buffer = ''
    begin
    loop {
      recv_nonblock(256, 0, buffer)
      contents += buffer
    }
    rescue IO::EAGAINWaitReadable
      contents
    end
  end

end

Steffen 在 cmets 中提出的观点得到了很好的理解 - TCP 并非设计用于这种方式。这是一种 hacky(在不好的意义上)的方法,应该避免。

【讨论】:

  • 是的,我还没有研究过gets 的实现,但我想它会以类似的方式工作——读取字节直到遇到一个字符被认为是一个新行,然后返回.
  • 此代码依赖于本地套接字缓冲区中已经可用的所有数据,只需要从中读取即可。但是,所需的数据可能仍在进行中或什至没有由对等方发送 - 请查看 TCP 窗口之类的概念以了解详细信息。所以它会返回一些东西,但特别是对于较大的消息,它不会是对等方发送的完整消息。因此,它似乎可以解决您的问题,但会在极端情况下中断,从而提供不错的 heisenbugs。同样,TCP 没有隐含的消息概念,明确需要在 TCP 提供的字节流之上添加消息帧
【解决方案2】:

TCP 只是将您写入的字节传输到套接字,并保证按照发送顺序接收。如果您有“消息”的概念,那么您需要将其添加到您的服务器和客户端中。

.gets 特别会阻塞,直到它读取新的“行”,或者您定义为字符串分隔符的任何内容 - 请参阅文档IO#gets。这意味着在您的服务器从客户端接收到该字节之前,它将阻塞。

在您的客户端中查看您是如何写入数据的 - 如果您使用的是 ruby​​,那么 puts 会起作用,因为它将用新行终止字符串。如果您使用的是write,那么它只会写入没有新行的字符串

即。

# client.rb
c = TCPSocket.new 'localhost', 5000
c.puts "foo"
c.write "bar"
c.write "baz\n"

# server.rb
s = TCPServer.new 5000
loop do
  client = s.accept
  puts client.gets
  puts client.gets
end

会输出

foo
barbaz

【讨论】:

  • 我想我更熟悉实际 TCP 级别的 TCP,它确实发送不同的包并根据需要对它们进行分段以通过网络传输。有什么方法可以读取到目前为止发送的所有内容吗?还是一种检查缓冲区中是否有任何东西的干净方法?调用 non_blocking 读取和救援错误会起作用,只是很惊讶没有一个简单的方法可以做到这一点。
猜你喜欢
  • 2015-01-28
  • 1970-01-01
  • 1970-01-01
  • 2011-05-27
  • 2011-12-25
  • 1970-01-01
  • 1970-01-01
  • 2011-01-05
  • 2017-07-05
相关资源
最近更新 更多