为什么更改通过“ ssh -t”传输的二进制文件?


29

我正在尝试通过SSH复制文件,但是scp由于不知道我需要的确切文件名而无法使用。尽管小型二进制文件和文本文件可以很好地传输,但是大型二进制文件会被更改。这是服务器上的文件:

remote$ ls -la
-rw-rw-r--  1 user user 244970907 Aug 24 11:11 foo.gz
remote$ md5sum foo.gz 
9b5a44dad9d129bab52cbc6d806e7fda foo.gz

这是我将其移至文件之后的文件:

local$ time ssh me@server.com -t 'cat /path/to/foo.gz' > latest.gz

real    1m52.098s
user    0m2.608s
sys     0m4.370s
local$ md5sum latest.gz
76fae9d6a4711bad1560092b539d034b  latest.gz

local$ ls -la
-rw-rw-r--  1 dotancohen dotancohen 245849912 Aug 24 18:26 latest.gz

请注意,下载的文件大于服务器上的文件!但是,如果我对一个很小的文件执行相同的操作,那么一切都会按预期进行:

remote$ echo "Hello" | gzip -c > hello.txt.gz
remote$ md5sum hello.txt.gz
08bf5080733d46a47d339520176b9211  hello.txt.gz

local$ time ssh me@server.com -t 'cat /path/to/hello.txt.gz' > hi.txt.gz

真正的0m3.041s用户0m0.013s sys 0m0.005s

local$ md5sum hi.txt.gz
08bf5080733d46a47d339520176b9211  hi.txt.gz

在这种情况下,两个文件大小均为26字节。

为什么小文件可以很好地传输,而大文件却增加了一些字节?


10
这是-t选项,它会中断传输。除非出于特殊原因需要它们,否则不要使用-t-T。默认值在绝大多数情况下都适用,因此很少需要这些选项。
kasperd

3
从来没有想过我会在本世纪这样说,但是如果您ssh -t cat是传输文件的唯一方法,则可能要尝试使用uuencode和uudecode 。
Mark Plotnick 2014年

1
uuencode / uudecode的@MarkPlotnick现代版本现已命名为base64 / base64 -d
Archemar

Answers:


60

TL; DR

不要使用-t-t涉及远程主机上的伪终端,仅应用于从终端运行可视应用程序。

说明

换行符(也称为换行符或\n)是发送到终端时告诉终端将其光标向下移动的字符。

但是,当您seq 3在终端中运行时,即在其中seq写入1\n2\n3\n类似的内容/dev/pts/0,您不会看到:

1
 2
  3

1
2
3

这是为什么?

实际上,当seq 3(或ssh host seq 3为此)书写时1\n2\n3\n,终端会看到1\r\n2\r\n3\r\n。即,换行已转换为回车(终端将光标移回屏幕左侧)并换行。

这是由终端设备驱动程序完成的。更确切地说,根据终端(或伪终端)设备的线路规范,驻留在内核中的软件模块。

您可以使用stty命令来控制该行规程的行为。LF-> 的翻译用CRLF打开

stty onlcr

(通常默认情况下启用)。您可以使用以下方法将其关闭:

stty -onlcr

或者,您可以使用以下命令关闭所有输出处理:

stty -opost

如果执行该操作并运行seq 3,则将看到:

$ stty -onlcr; seq 3
1
 2
  3

如预期的那样。

现在,当您这样做时:

seq 3 > some-file

seq不再写入终端,而是写入文件,无需进行翻译。所以some-file确实包含1\n2\n3\n。仅在写入终端设备时才执行转换。而且仅用于显示。

同样,当您这样做时:

ssh host seq 3

ssh正在写,1\n2\n3\n而不管ssh输出到什么。

实际发生的是该seq 3命令在host其标准输出重定向到管道的情况下运行。ssh主机上的服务器读取管道的另一端,并通过加密通道将其发送到ssh客户端,然后ssh客户端将其写入其stdout中(在您的情况下是伪终端设备),将LFs转换为此值以CRLF进行显示。

当标准输出不是终端时,许多交互式应用程序的行为会有所不同。例如,如果您运行:

ssh host vi

vi不喜欢它,不喜欢它的输出到管道。它认为它不是在与能够理解例如光标定位转义序列的设备通信。

因此ssh可以-t选择。使用该选项,主机上的ssh服务器将创建一个伪终端设备,并使stdout(以及stdin和stderr)成为vi。什么vi写道,终端装置通过该远程伪终端线路规则并且由所读出的ssh服务器和发送过来的加密的信道的ssh客户机。除了使用管道代替之外,ssh服务器使用伪终端,与之前相同。

另一个区别是,在客户端,ssh客户端将终端设置为raw模式。这意味着在那里没有完成翻译(opost被禁用以及其他输入端行为)。例如,当您键入时,该字符被发送到远程端Ctrl-C,而不是打断ssh^C字符,远程伪终端的线路规则将中断发送到远程命令。

当您这样做时:

ssh -t host seq 3

seq 3写入1\n2\n3\n其stdout,这是一个伪终端设备。由于onlcr,它会在主机1\r\n2\r\n3\r\n转换为加密通道并通过加密通道发送给您。在您这一边,没有翻译(已onlcr禁用),因此1\r\n2\r\n3\r\n未显示(由于该raw模式),并且在终端仿真器的屏幕上正确显示。

现在,如果您这样做:

ssh -t host seq 3 > some-file

与上面没有区别。ssh会写同样的东西:1\r\n2\r\n3\r\n,但是这次变成了some-file

因此,基本上所有的LF输出seq都已转换CRLFsome-file

如果执行以下操作,则相同:

ssh -t host cat remote-file > local-file

所有LF字符(0x0a字节)都将转换为CRLF(0x0d 0x0a)。

这可能是文件损坏的原因。对于第二个较小的文件,恰好该文件不包含0x0a字节,因此没有损坏。

请注意,使用不同的tty设置可能会导致不同类型的损坏。与之相关的另一种潜在损坏类型-t是,您在host~/.bashrc~/.ssh/rc...)上的启动文件将内容写入其stderr,因为-t远程外壳程序的stdout和stderr最终合并到了sshstdout中(它们都进入了伪指令)。 -终端设备)。

您不希望遥控器cat输出到那里的终端设备。

你要:

ssh host cat remote-file > local-file

您可以这样做:

ssh -t host 'stty -opost; cat remote-file` > local-file

这将起作用(除了上面讨论的stderr损坏案例中的写法之外),但是即使那样也将是次优的,因为您将在上运行不必要的伪终端层host


更有趣:

$ ssh localhost echo | od -tx1
0000000 0a
0000001

好。

$ ssh -t localhost echo | od -tx1
0000000 0d 0a
0000002

LF 翻译成 CRLF

$ ssh -t localhost 'stty -opost; echo' | od -tx1
0000000 0a
0000001

再次确定。

$ ssh -t localhost 'stty olcuc; echo x'
X

这是终端线路部门可以完成的另一种输出后处理形式。

$ echo x | ssh -t localhost 'stty -opost; echo' | od -tx1
Pseudo-terminal will not be allocated because stdin is not a terminal.
stty: standard input: Inappropriate ioctl for device
0000000 0a
0000001

ssh当其自己的输入不是终端时,拒绝告诉服务器使用伪终端。您可以通过以下方式强制执行-tt

$ echo x | ssh -tt localhost 'stty -opost; echo' | od -tx1
0000000   x  \r  \n  \n
0000004

线规在输入方面做得更多。

在这里,echo既不读取其输入,也不要求其输出,x\r\n\n那么它是从哪里来的呢?这是echo远程伪终端(stty echo)的本地地址。所述ssh服务器被供给x\n它从客户端读取到远程伪终端的主侧。并且该行的规则将其回显(在stty opost运行之前,这就是为什么我们看到a CRLF和not的原因LF)。这与远程应用程序是否从stdin读取任何内容无关。

$ (sleep 1; printf '\03') | ssh -tt localhost 'trap "echo ouch" INT; sleep 2'
^Couch

0x3字符被回送为^C^C),因为stty echoctl和壳和睡眠接收SIGINT因为stty isig

因此,当:

ssh -t host cat remote-file > local-file

够糟糕的,但是

ssh -tt host 'cat > remote-file' < local-file

以另一种方式传输文件会更糟。你会得到一些CR - > LF翻译,而且问题的所有特殊字符(^C^Z^D^?^S...),也是远程cat不会EOF看到,当年底local-file达到,只有当^D被后发送\r\n或其他^D喜欢做的时候cat > file在你的终端。


5

使用该方法复制文件时,文件似乎有所不同。

远端伺服器

ls -l | grep vim_cfg
-rw-rw-r--.  1 slm slm 9783257 Aug  5 16:51 vim_cfg.tgz

本地服务器

运行ssh ... cat命令:

$ ssh dufresne -t 'cat ~/vim_cfg.tgz' > vim_cfg.tgz

该文件在本地服务器上的结果:

$ ls -l | grep vim_cfg.tgz 
-rw-rw-r--. 1 saml saml 9820481 Aug 24 12:13 vim_cfg.tgz

调查原因?

在本地调查生成的文件表明该文件已损坏。如果您-tssh命令中删除该开关,则它将按预期工作。

$ ssh dufresne 'cat ~/vim_cfg.tgz' > vim_cfg.tgz

$ ls -l | grep vim_cfg.tgz
-rw-rw-r--. 1 saml saml 9783257 Aug 24 12:17 vim_cfg.tgz

校验和现在也可以工作:

# remote server
$ ssh dufresne "md5sum ~/vim_cfg.tgz"
9e70b036836dfdf2871e76b3636a72c6  /home/slm/vim_cfg.tgz

# local server
$ md5sum vim_cfg.tgz 
9e70b036836dfdf2871e76b3636a72c6  vim_cfg.tgz

谢谢辛。尽管实际上您是第一个发布正确答案的人,但由于他的解释深入,我确实选择了Stéphane作为所选择的答案。不用担心,您有很长的帖子历史,可以从中学习,当然,我也支持那些从中学习的帖子。谢谢。
dotancohen

@dotancohen-不用担心,您会接受A感觉是最能帮助您成为OP的人8-)。除了吉尔斯(Gilles)以外,他解释事情发生原因的能力无与伦比。
slm
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.