0

我有一个在 Unix 套接字上运行的 ssh 服务,并且我有一个本地 TCP 服务器,我希望将它定向到 unix 套接字的通道。

基本上当我这样做时:

$ ssh root@localhost -p 2000

然后我的本地 TCP 服务器获取请求并将其通过管道传输到 Unix 套接字和 TCP 客户端,在本例中为 ssh,从 Unix 套接字获取回复。相关代码:

  let running_tunnel debug (tcp_ic, tcp_oc) () =
    Lwt_io.with_connection a_unix_addr begin fun (mux_ic, mux_oc) ->
      let%lwt _ = some_call with_an_arg
      and _ =

        (* Some setup code *)


      let rec forever () =
        Lwt_io.read_line tcp_ic >>= fun opening_message ->
        Lwt_io.write_from_string_exactly
        mux_oc opening_message 0 (String.length opening_message) >>= fun () ->
        Lwt_io.read_line mux_ic >>= fun reply ->
        Lwt_io.printl reply >>= fun () ->
        Lwt_io.write_line tcp_oc reply >>= fun () ->
        forever ()
      in
      forever ()
      in
      Lwt.return_unit
    end

而这种作品。当我在命令行上调用 ssh 时它会“卡住”,但我知道我正在获取一些数据,因为对方的 ssh 标头是正确的,SSH-2.0-OpenSSH_6.7. 我也让我的一方打印出初始 ssh 握手的更多部分,即我看到这个打印:

??^?W\zJ?~??curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1ssh-rsa,ssh-dss>aes128-ctr,aes192-ctr,aes256-ctr,chacha20-poly1305@openssh.com>aes128-ctr,aes192-ctr,aes256-ctr,chacha20-poly1305@openssh.com?umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1?umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1none,zlib@openssh.comnone,zlib@openssh.co 

等,这似乎是正确的。我认为挂起的原因是因为我正在使用Lwt_io.read_line所以我尝试了这个:

            let rec forever () =
              Lwt_io.read tcp_ic >>= fun opening_message ->
              Lwt_io.write_from_string_exactly
                mux_oc opening_message 0 (String.length opening_message) >>= fun () ->
              Lwt_io.read mux_ic >>= fun reply ->
              Lwt_io.printl reply >>= fun () ->
              Lwt_io.write tcp_oc reply >>= fun () ->
              forever ()
            in
            forever ()

实际上效果更糟,它甚至没有打印出最初的握手。我还尝试了专用的{write,read}_into... 功能,但成功有限。在 strace/dtruce 下运行,我看到的最终结果如下:

read(0x6, "SSH-2.0-OpenSSH_6.9\r\n\0", 0x1000)       = 21 0
write(0x1, "SSH-2.0-OpenSSH_6.9\n\0", 0x14)      = 20 0
read(0x7, "\0", 0x1000)      = -1 Err#35
write(0x7, "SSH-2.0-OpenSSH_6.9\0", 0x13)        = 19 0
select(0x9, 0x7FFF5484F880, 0x7FFF5484F800, 0x7FFF5484F780, 0x0)         = 1 0
read(0x7, "SSH-2.0-OpenSSH_6.7\r\n\0", 0x1000)       = 21 0
write(0x1, "SSH-2.0-OpenSSH_6.7\n\0", 0x14)      = 20 0
read(0x6, "\0", 0x1000)      = -1 Err#35
write(0x6, "SSH-2.0-OpenSSH_6.7\n\0", 0x14)      = 20 0
select(0x9, 0x7FFF5484F880, 0x7FFF5484F800, 0x7FFF5484F780, 0x0)         = 1 0
read(0x6, "\0", 0x1000)      = 1968 0
read(0x6, "\0", 0x1000)      = -1 Err#35
^C

其中 6.9 是我的本地机器的 ssh,而 6.7 是 Unix 套接字后面的远程机器。对我来说似乎很奇怪的一件事是它是如何\r被删除的,这会将读/写计数更改为 1。我不确定这是否是关键的区别。

理想情况下,我希望从 Lwt 中进行某种抽象,即只要此可读通道(TCP 套接字)上有可用数据,就将其直接写入可写通道(Unix 套接字),反之亦然。

4

1 回答 1

2

变体readline不起作用,因为数据流是二进制的,并且readline用于基于文本行的输入。该函数的第二个变体Lwt_io.read不起作用,因为该函数将读取所有输入直到最后,除非您指定了可选count参数。这意味着,该控制权将write仅在阅读器端的 EOF 之后传递。Lwt_io.read例如,使用一些计数Lwt_io.read ~count:1024 mux_ic并不是一个非常糟糕的主意。此外,如果您希望您的流是有限的,您不应该忘记检查返回值。read_into应该小心使用,因为与函数不同read,它不能保证它会读取您请求的确切数据量。换句话说,会有短读。对于write_into功能。这个函数的_exactly版本没有这个问题,所以最好改用它们。

还有一件事,你应该考虑。为Lwt_io缓冲输入和输出提供接口。这意味着,该模块中的所有函数都在向某个内部缓冲区写入和读取,而不是通过设备描述符直接与操作系统交互。这意味着,当您将数据从一个缓冲源传输到另一个缓冲源时,两端都会出现一些意外延迟。所以你应该预料到他们会使用冲洗。否则,当您进行双向交互时,可能会引入竞争条件。

此外,尽管缓冲 io 简化了很多事情,但它也有代价。事实上,你有几个不必要的缓冲区层,当你使用时Lwt_io,你也分配了很多不必要的数据,用垃圾来破坏你的内存。问题在于它Lwt_io有自己的内部缓冲区,它不会向临时用户显示,并且所有返回数据或使用数据的函数都需要执行额外的复制操作到内部函数或从内部函数复制。例如,使用Lwt_io.{read,write}, 将执行以下操作:

  1. 将数据从内核复制到内部缓冲区
  2. 分配一个字符串
  3. 将数据从内部缓冲区复制到分配的字符串
  4. (现在是write部分)将数据从字符串复制到内部缓冲区
  5. 将数据从内部缓冲区复制到内核。
  6. (在某个地方,有时在 GC 内部)将分配的字符串从次堆复制到主堆(如果字符串足够小以适合次堆)或将其从一个位置复制到另一个位置(如果压缩算法决定移动它,并且如果生产者超过消费者并且读取数据的生命周期变得很长,则字符串仍然存在,这是很有可能的)。

看起来我们可以去掉 2、3、4 和 6 中的副本。我们可以使用自己的缓冲区,将数据从内核复制到其中,然后将数据从该内核复制回内核。我们甚至可以通过使用splicetee系统调用来摆脱 1 和 5 中的副本,它们直接在内核缓冲区之间复制数据,而根本不涉及用户空间。但在这种情况下,我们将失去检查数据的能力,而这通常是我们想要的。

因此,让我们尝试从内核空间中删除除副本之外的所有副本。Lwt_io我们可以在, likedirect_access和新添加的函数中使用内部缓冲区的低级接口block,但这需要了解内部缓冲区,Lwt_io而且不是很琐碎,但仍然可行。相反,我们将使用使用Lwt_unix库的更简单的方法。这个库直接与内核交互,没有任何中间缓冲区,我们自己做缓冲。

open Lwt.Infix

let bufsiz = 32768

let echo ic oc =
  let buf = Lwt_bytes.create bufsiz in
  let rec loop p =
    let p = p mod bufsiz in
    Lwt_bytes.read ic buf p (bufsiz - p) >>= function
    | 0 -> Lwt.return ()
    | n -> Lwt_bytes.write oc buf p n >>= loop in
  loop 0

这将实现简单快速的数据复制,以与cat程序相同的速度复制数据。不过,仍有一些改进的余地。例如,为了鲁棒性(尤其是EINTR信号),应该添加错误处理。此外,此函数实现了同步复制,其中输入和输出是紧密锁定的。有时这不是一个选择。考虑以下示例,输入是一个 UDP 套接字,它可能很容易超过消费者,并且数据将被丢弃,即使平均而言生产者比消费者慢。要处理这个问题,您需要将读取器和写入器分成两个单独的线程,它们通过一些弹性队列进行通信。

Lwt是相当低级的库,它不会也不应该为您解决这些问题。它提供了机制,可用于为每种情况构建解决方案。有些库确实为一些常见模式提供了解决方案,0MQnanomessages就是很好的例子。

更新

我可能是太低级的家伙,可能是我挖的太深了。如果您真的在寻找高级方法,那么您应该使用Lwt_stream's,在这种情况下,您可以将节点等值编码foo.pipe(bar).pipe(foo)

let echo ic oc = Lwt_io.(write_chars oc (read_chars ic))

当然,这会慢得多,但这取决于您的任务。

是的,要执行双向重定向,您应该只运行两个线程,如下所示:echo ic oc <&> echo ic oc对于具有文件描述符的版本,它们都是可写的。如果您使用Lwt_io的是像管道一样单向的通道,那么您将获得每个部分的两个端点。让我们将它们命名为fifo用于相应的前端输入和输出,以及bi,bo用于后端部分。然后你需要像这样连接它:echo fo bi <&> echo bo fi,使用第二个版本的echo流。

性能成本

通常,高级抽象会带来性能成本。在我们的特殊情况下,使用第一个版本的 echo,吞吐量超过1Gb每秒。带有流的版本具有平均吞吐量5MB/s。根据您的设置,它可能会或可能不会起作用。对于常规的 ssh 会话来说已经足够了,但可能会对scp本地网络产生影响。

于 2015-12-17T15:20:45.350 回答