如何在 Ruby 中创建双向 SSL 套接字
How can I create a two-way SSL socket in Ruby
我正在构建一个连接到服务器并等待数据的客户端 Ruby 库,但也允许用户通过调用方法发送数据。
我使用的机制是有一个 class 来初始化套接字对,如下所示:
def initialize
@pipe_r, @pipe_w = Socket.pair(:UNIX, :STREAM, 0)
end
我允许开发者调用来向服务器发送数据的方法是这样的:
def send(data)
@pipe_w.write(data)
@pipe_w.flush
end
然后我在一个单独的线程中有一个循环,其中我 select 从连接到服务器的 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
代替,但是根据 the IO.select docs:
The best way to use IO.select is invoking it after nonblocking methods such as read_nonblock, write_nonblock, etc.
[...]
Especially, the combination of nonblocking methods and IO.select is preferred for IO like objects such as OpenSSL::SSL::SSLSocket.
据此,我需要调用 IO.select
after 非阻塞方法,而在我的循环中我使用它 before所以我可以 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 对象一起使用时有效。
我的问题是:鉴于我需要从 @pipe_r
和 socket
对象 select,我如何才能使我之前的示例使用 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 本身,但不熟悉使用 select 和基于 SSL 的套接字的问题。 SSL 套接字的行为与 TCP 套接字不同,因为 SSL 数据以帧的形式传输,而不是作为数据流传输,但仍然将流语义应用于套接字接口。
让我们用一个例子来解释这一点,首先使用一个简单的 TCP 连接:
- 服务器单次写入发送 1000 个字节。
- 数据将传递给客户端并放入内核套接字缓冲区。因此 select 将 return 数据可用。
- 客户端应用程序首先只读取 100 个字节。
- 其他 900 个字节将保存在内核套接字缓冲区中。 select 的下一次调用将再次 return 数据可用,因为 select 查看内核套接字缓冲区。
对于 SSL,这是不同的:
- 服务器单次写入发送 1000 个字节。然后,SSL 堆栈会将这 1000 个字节加密为一个 SSL 帧,并将该帧发送给客户端。
- 此 SSL 帧将传送到客户端并放入内核套接字缓冲区中。因此 select 将 return 数据可用。
- 现在客户端应用程序只读取了 100 个字节。由于 SSL 帧包含 1000 个字节,并且需要完整的帧来解密数据,SSL 堆栈将从套接字中读取完整的帧,而不会在内核套接字缓冲区中留下任何内容。然后它将解密帧和 return 向应用程序请求的 100 字节。其余解密的 900 字节将保存在用户 space.
的 SSL 堆栈中
- 由于内核套接字缓冲区为空,因此 select 的下一次调用不会 return 数据可用。因此,如果应用程序仅处理 select,则现在不会有未读数据,即保存在 SSL 堆栈中的 900 字节。
如何处理这种差异:
- 一种方法是始终尝试读取至少 32768 个数据,因为这是 SSL 帧的最大大小。这样可以确保没有数据仍然保留在 SSL 堆栈中(SSL 读取不会读取 SSL 帧边界)。
- 另一种方法是在调用 select 之前检查 SSL 堆栈中是否有已解密的数据。仅当 SSL 堆栈中没有数据时才应调用 select。要检查这样的 "pending data" 使用 the pending method.
- 尝试从非阻塞套接字读取更多数据,直到没有更多数据可用。通过这种方式,您可以确保 SSL 堆栈中没有任何数据处于未决状态。但请注意,这也会在底层 TCP 套接字上进行读取,因此与其他套接字上的数据(仅在成功 select 后才读取)相比,可能更喜欢 SSL 套接字上的数据。这是您引用的建议,但我更喜欢其他建议。
我正在构建一个连接到服务器并等待数据的客户端 Ruby 库,但也允许用户通过调用方法发送数据。
我使用的机制是有一个 class 来初始化套接字对,如下所示:
def initialize
@pipe_r, @pipe_w = Socket.pair(:UNIX, :STREAM, 0)
end
我允许开发者调用来向服务器发送数据的方法是这样的:
def send(data)
@pipe_w.write(data)
@pipe_w.flush
end
然后我在一个单独的线程中有一个循环,其中我 select 从连接到服务器的 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
代替,但是根据 the IO.select docs:
The best way to use IO.select is invoking it after nonblocking methods such as read_nonblock, write_nonblock, etc.
[...]
Especially, the combination of nonblocking methods and IO.select is preferred for IO like objects such as OpenSSL::SSL::SSLSocket.
据此,我需要调用 IO.select
after 非阻塞方法,而在我的循环中我使用它 before所以我可以 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 对象一起使用时有效。
我的问题是:鉴于我需要从 @pipe_r
和 socket
对象 select,我如何才能使我之前的示例使用 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 本身,但不熟悉使用 select 和基于 SSL 的套接字的问题。 SSL 套接字的行为与 TCP 套接字不同,因为 SSL 数据以帧的形式传输,而不是作为数据流传输,但仍然将流语义应用于套接字接口。
让我们用一个例子来解释这一点,首先使用一个简单的 TCP 连接:
- 服务器单次写入发送 1000 个字节。
- 数据将传递给客户端并放入内核套接字缓冲区。因此 select 将 return 数据可用。
- 客户端应用程序首先只读取 100 个字节。
- 其他 900 个字节将保存在内核套接字缓冲区中。 select 的下一次调用将再次 return 数据可用,因为 select 查看内核套接字缓冲区。
对于 SSL,这是不同的:
- 服务器单次写入发送 1000 个字节。然后,SSL 堆栈会将这 1000 个字节加密为一个 SSL 帧,并将该帧发送给客户端。
- 此 SSL 帧将传送到客户端并放入内核套接字缓冲区中。因此 select 将 return 数据可用。
- 现在客户端应用程序只读取了 100 个字节。由于 SSL 帧包含 1000 个字节,并且需要完整的帧来解密数据,SSL 堆栈将从套接字中读取完整的帧,而不会在内核套接字缓冲区中留下任何内容。然后它将解密帧和 return 向应用程序请求的 100 字节。其余解密的 900 字节将保存在用户 space. 的 SSL 堆栈中
- 由于内核套接字缓冲区为空,因此 select 的下一次调用不会 return 数据可用。因此,如果应用程序仅处理 select,则现在不会有未读数据,即保存在 SSL 堆栈中的 900 字节。
如何处理这种差异:
- 一种方法是始终尝试读取至少 32768 个数据,因为这是 SSL 帧的最大大小。这样可以确保没有数据仍然保留在 SSL 堆栈中(SSL 读取不会读取 SSL 帧边界)。
- 另一种方法是在调用 select 之前检查 SSL 堆栈中是否有已解密的数据。仅当 SSL 堆栈中没有数据时才应调用 select。要检查这样的 "pending data" 使用 the pending method.
- 尝试从非阻塞套接字读取更多数据,直到没有更多数据可用。通过这种方式,您可以确保 SSL 堆栈中没有任何数据处于未决状态。但请注意,这也会在底层 TCP 套接字上进行读取,因此与其他套接字上的数据(仅在成功 select 后才读取)相比,可能更喜欢 SSL 套接字上的数据。这是您引用的建议,但我更喜欢其他建议。