如何在Ruby中创建双向SSL套接字

我正在构建一个客户端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.select与SSL套接字一起使用的给定示例是:

 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对象一起使用时,此方法才IO.select

我的问题是:如果我需要从@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 

这看起来并不那么糟糕,但欢迎任何输入。

我不熟悉ruby本身,但是使用基于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将不会返回该数据可用。 因此,如果应用程序仅处理select,那么现在还不会有未读数据,即保留在SSL堆栈中的900字节。

如何处理这种差异:

  • 一种方法是始终尝试读取至少32768个数据,因为这是SSL帧的最大大小。 这样就可以确保SSL堆栈中仍然没有数据保存(SSL读取不会通过SSL帧边界读取)。
  • 另一种方法是在调用select之前检查已经解密的数据的SSL堆栈。 只有在SSL堆栈中没有保留数据时才应调用select。 要检查此类“待处理数据”,请使用pending方法 。
  • 尝试从非阻塞套接字读取更多数据,直到没有更多数据可用。 这样,您可以确保SSL堆栈中没有待处理的数据。 但请注意,这也将对底层TCP套接字进行读取,因此可能更喜欢SSL套接字上的数据与其他套接字上的数据(仅在成功选择后才读取)。 这是你引用的推荐,但我更喜欢其他推荐。