7

我正在构建一个客户端 Ruby 库,它连接到服务器并等待数据,但也允许用户通过调用方法来发送数据。

我使用的机制是有一个初始化套接字对的类,如下所示:

def initialize
  @pipe_r, @pipe_w = Socket.pair(:UNIX, :STREAM, 0)
end

我允许开发人员调用以向服务器发送数据的方法如下所示:

def send(data)
  @pipe_w.write(data)
  @pipe_w.flush
end

然后我在一个单独的线程中有一个循环,我从socket连接到服务器的一个和从@pipe_r

def socket_loop
  Thread.new do
    socket = TCPSocket.new(host, port)

    loop do
      ready = IO.select([socket, @pipe_r])

      if ready[0].include?(@pipe_r)
        data_to_send = @pipe_r.read_nonblock(1024)
        socket.write(data_to_send)
      end

      if ready[0].include?(socket)
        data_received = socket.read_nonblock(1024)
        h2 << data_received
        break if socket.nil? || socket.closed? || socket.eof?
      end
    end
  end
end

这很好用,TCPSocket在示例中正常使用。我需要改用一个OpenSSL::SSL::SSLSocket,但是根据IO.select 文档

使用 IO.select 的最佳方式是在 read_nonblock、write_nonblock 等非阻塞方法之后调用它。

[...]

尤其是对于像 OpenSSL::SSL::SSLSocket 这样的 IO 类对象,非阻塞方法和 IO.select 的组合是首选。

据此,我需要在非阻塞方法IO.select 之后调用,而在我的循环中我之前使用它,所以我可以从 2 个不同的 IO 对象中进行选择。

有关如何使用IO.selectSSL 套接字的给定示例是:

begin
  result = socket.read_nonblock(1024)
rescue IO::WaitReadable
  IO.select([socket])
  retry
rescue IO::WaitWritable
  IO.select(nil, [socket])
  retry
end

但是,这仅在IO.select单个IO 对象一起使用时才有效。

我的问题是:鉴于我需要从对象@pipe_rsocket对象中进行选择,我如何才能使我之前的示例与 SSL 套接字一起使用?

编辑:我尝试了@steffen-ullrich 的建议,但无济于事。我能够使用以下方法通过测试:

loop do

  begin
    data_to_send = @pipe_r.read_nonblock(1024)
    socket.write(data_to_send)
  rescue IO::WaitReadable, IO::WaitWritable
  end

  begin
    data_received = socket.read_nonblock(1024)
    h2 << data_received
    break if socket.nil? || socket.closed? || socket.eof?

  rescue IO::WaitReadable
    IO.select([socket, @pipe_r])

  rescue IO::WaitWritable
    IO.select([@pipe_r], [socket])

  end
end

这看起来还不错,但欢迎任何意见。

4

1 回答 1

4

我不熟悉 ruby​​ 本身,但对使用 select 和基于 SSL 的套接字的问题很熟悉。SSL 套接字的行为与 TCP 套接字不同,因为 SSL 数据以帧而不是数据流的形式传输,但流语义仍应用于套接字接口。

让我们用一个例子来解释一下,首先使用一个简单的 TCP 连接:

  • 服务器在一次写入中发送 1000 个字节。
  • 数据将被传递到客户端并放入内核套接字缓冲区。因此 select 将返回该数据可用。
  • 客户端应用程序首先只读取 100 个字节。
  • 其他 900 字节将保存在内核套接字缓冲区中。下一次调用 select 将再次返回可用的数据,因为 select 查看内核中的套接字缓冲区。

使用 SSL,这是不同的:

  • 服务器在一次写入中发送 1000 个字节。然后 SSL 堆栈会将这 1000 个字节加密为单个 SSL 帧,并将该帧发送给客户端。
  • 此 SSL 帧将被传递给客户端并放入内核套接字缓冲区。因此 select 将返回该数据可用。
  • 现在客户端应用程序只读取 100 个字节。由于 SSL 帧包含 1000 个字节,并且由于它需要完整的帧来解密数据,因此 SSL 堆栈将从套接字读取完整的帧,而在内核套接字缓冲区中没有任何内容。然后它将解密该帧并将请求的 100 个字节返回给应用程序。解密后的 900 字节的其余部分将保存在用户空间的 SSL 堆栈中。
  • 由于内核中的套接字缓冲区为空,因此下一次调用 select 将不会返回该数据可用。因此,如果应用程序只处理选择,那么现在仍有未读取的数据,即 SSL 堆栈中保留的 900 个字节。

如何处理这种差异:

  • 一种方法是始终尝试读取至少 32768 个数据,因为这是 SSL 帧的最大大小。通过这种方式,可以确保 SSL 堆栈中仍然没有数据(SSL 读取不会越过 SSL 帧边界读取)。
  • 另一种方法是在调用 select 之前检查 SSL 堆栈中已解密的数据。只有当 SSL 堆栈中没有数据时,才应调用 select。要检查此类“待处理数据”,请使用pending 方法
  • 尝试从非阻塞套接字读取更多数据,直到没有更多数据可用。通过这种方式,您可以确保 SSL 堆栈中没有数据仍处于待处理状态。但请注意,这也会对底层 TCP 套接字进行读取,因此与其他套接字上的数据相比,可能更喜欢 SSL 套接字上的数据(只有在成功选择后才会读取)。这是您引用的建议,但我更喜欢其他建议。
于 2016-04-23T21:51:26.083 回答