如何在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_r
和socket
对象中进行选择,我如何使我的前一个示例使用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套接字上的数据与其他套接字上的数据(仅在成功选择后才读取)。 这是你引用的推荐,但我更喜欢其他推荐。