scp命令的那些坑儿

文章来源于 : 淘宝运维 文强

前两天发现scp命令的一个坑,分享 下,以免其他兄弟再踩这坑了~

前两天有个开发兄弟向我反馈他的任务跑失败了,原因是里面 的一条scp执行失败。后来我试 了试,发现xxxxx机 器下的数据不能通过tssfsdf帐户scp至其他机器,或从其 他机器scp过来,只输出’bashrc’然后就结 束了(顺便吐下槽,感谢XX同学在tscadmin下的~/.bashrc加的是echo ’bashrc’, 让俺能够快速定位问题)。

为什么bashrc里加一条echo语句,就会造成scp失败呢?

scp使用SSH连接,在两台服务器 之间通过一些特特殊的协议消息控制源端与宿端的数据传输。在实际数据传输前,源端需要先向宿端传输协议文本,协议文本传输结束 后,再向宿端传递字符'\0'来表示真正文件传 输的开始。当文件接收完成后,宿端会给源端发送一个'\0'表示文件传输结 束。然后而ssh login远程服 务器时,会source ~/.bashrc和/etc/bashrc文 件,所以当在 ~/.bashrc或/etc/bashrc执 行过程中产生了任何内容输出时,scp会把这些echo回来的东西认作协 议包进行解析,遇到'\0'则会当成表示传输 失败的错误信息,然后exit 1。

所以,一般对于有标准输出的语句最好是放到.bash_profile中。 以免引起不必要的错误。

 

以 下是scp协 议原理  (这 个是在网上找的,俺就直接贴了,

中 文翻译:http://teachmyself.blog.163.com/blog/static/188814229201242314917237/

英 文原版:https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works

=========================================

scp协议原理

scp [options] [user@]host1:]file1 []... [ [user@]host2:]file2

scp先解析命令行参数, 然后打开一个到远程服 务器的连接. 再通过这个连接起另一 个scp进程, 这个进程的运行方式可 以是源模式(source)也可以是宿模式(sink). (译者注: 前者是数据提供者, 源头, 以源模式运行的scp进程后面会被称作是源端. 后者是数据的目的地, 归宿, 以宿模式运行的scp进程后面会被称作是宿端)前者读取文件并通过SSH连接发送到另一端, 后者通过SSH连接接收文件. 源模式和宿模式是通过-f (from)和 -t (to)这 两个隐藏选项来启动的. 这两个参数仅供命令内 部使用, 因此没写进文档.

下图给出了一个简化后的scp源/宿模式工作示意图:

+-----------+   remote command: scp -t file2   +------+
| ssh hostB |-------------------------------->| sshd |
+-----------+                                      +---+--+
^                                                      |
|                                                      |
|fork()                                        fork()|
|                                                      |
+----+-----------------+              +-----------V--+
| scp file hostB:file2 |              | scp -t file2 |
+----------------------+              +--------------+

协议

下面介绍传输协议是如何工作的. 你不如先暂时忘了ssh, sshd以及两台 机器之间的连接这些东东. 如果我们只关注以源宿 两种模式工作的scp命令的话, 上图可以简化成:

data transfer
+------------------+   ___________   +--------------+
| scp fileX hostY: | ->___________-> | scp -t fileX |
+--------.---------+                 +-------.------+
|                                   |
|read()                             |write()
__....|....__                       __....|....__
=__  fileX  __:'                    =__  fileX  __:'
`''''''''                           `''''''''

需要注意的是, 永远不会有两个工作模 式一样的scp协同工作. (译者注: 你可以想象下两个源端 互相期待对方给自己传文件会是啥情况...) 远程服务器上的scp进程选定一种模式后, 本地的scp进程(就是本地用户命令行起的这个进程)会自动选定另一种模式, 因为这个本地进程会于 用户交互.

源端

协议信息是由文本和二进制数据混合构 成的. 例如, 当我们要传出一个普通 文件时, 协议消息的类型, 文件的权限位, 长度及文件名都会以文 本的方式发送, 接着在一个换行符后发 送文件的内容. 我们在后面会更详细地 讨论这一点. 协议消息内容可能类似:

C0644 299 group

二进制数据传输前需要传输的文本信息 可能更多. 源端会一直等宿端的回 应, 直到等到回应才会传输 下一条协议文本. 在送出最后一条协议文 本后, 源端会传出一个大小为 零的字符'\0'来表示真正文件传输的开始. 当文件接收完成后, 宿端会给源端发送一个'\0'.

宿端

来自源端的每条消息和每个传输完毕的 文件都需要宿端的确认和响应. 宿端会返回三种确认消 息: 0(正常), 1(警告)或2(严重错误, 将中断连接). 消息1和2可以跟一个字符串和一个换行符, 这个字符串将显示在scp的源端. 无论这个字符串是否为 空, 换行符都是不可缺少的.

协议消息类型列表

Cmmmm

表示传输单个文件, mmmm是文件的 权限位. 实例: C0644 299 group

Dmmmm

表示开始整个目录的递归复制. 此处文件长度将会忽略, 但是不可缺少. 实例: D0755 0 docs

E

表示目录的结束(D-E这一对可以嵌套使用, 这也是我们能正常递归 复制目录树的原因.)

T 0  0

当命令行给出-p选项时, 这一类协议消息用来传 输所传递的文件的修改时间和访问时间(我猜你应该知道为啥我们不把文件创建时间传到 宿端吧?). 时间记录了从UTC 1970.01.01 00:00:00到现在所经历的秒数. 这一类协议消息在最初 的rcp实现中并未出现. 实例: T1183828267 0 1183828267 0

传完了这些消息后就开始传文件数据了. 宿端从数据流中读取之 前协议消息中指定的文件长度. D和T需要在其他消息之前指定. 这是因为如果这两类消 息放在其他消息之后, 这两类消息的内容具体 是消息还是数据就不清楚了. 我们可以总结如下:

  • 传完了C类消息后开始传输文件数据.
  • 在传完了D类消息后, 要么出现C类消息, 要么出现E类消息.

最大文件大小和文件完整性

scp所能传输的最大文件大小是由scp协议, scp软件, 操作系统以及文件系统 综合决定的. 由于OpenSSH用long long int来 放文件大小, 因此理论上可以传输的 最大文件大小是2^63 Byte. 给一个参考值, 2^40 Byte的 大小是1T. 这意味着我们可以认为 协议本身没有文件大小的限制.

scp本身不提供对文件完整性的保护, 这一特性是在ssh协议那一层完成的. 你可以参考我之前写的博客文章, 也可以直接去围观RFC43253.

例子

讲协议是扯不清楚的, 直接看例子更直观更形 象.

1. 本地文件复制到另一位 置

$ rm -f /tmp/test
$ { echo C0644 6 test; printf "hello\\n"; } | scp -t /tmp
test                 100% |***************************| 6       00:00
$ cat /tmp/test
hello

好玩吧? 我用了printf命令, 这样我们能够很清楚地 看见为什么文件长度为6. 接下来我们试试复制一 个目录.

2. 本地目录复制到另一位 置

我们准备将一个名为testdir的目录, 内含一个名为test的文件, 递归地复制到/tmp下去.

$ rm -rf /tmp/testdir
$ { echo D0755 0 testdir; echo C0644 6 test;
printf "hellon"; echo E; } | scp -rt /tmp
test                 100% |****************************| 6       00:00
$ cat /tmp/testdir/test
hello

请注意, 我们在此处用了-r参数, 因为我们要复制整个目 录.

3. 将另一位置的目录复制 到本地

之前的例子中, 管道里的scp都是充当宿端. 这个例子里面, scp进程的角色 是源端. 就像前面说的那样, 我们必须要对每个成功 的消息和文件传输加以应答. 另外, 这个例子只是模拟应答 的过程, 而没有真正去创建文件 和文件夹. 因为要创建的东西都已 经在你的终端里打印出来了.

$ cd /tmp
$ rm -rf testdir
$ mkdir testdir
$ echo hello > testdir/test
$ printf '\000\000\000\000\000\000' | scp -qprf testdir
T1183832947 0 1183833773 0
D0700 0 testdir
T1183833773 0 1183833762 0
C0600 6 test
hello
E

解释下, 这次没有进度条了, 这是因为我们用了-q选项. 你可以看到传输了文件 时间信息, 这是因为我们是用了-p选项. 另外, -f表示这一次scp进程是源端. 你可以发现我们丢了六 个'\000'给scp, 这是我们模拟的传输过 程中的应答. 第一个来开始传输过程, 四个响应消息, 一个响应文件传输结束. 对了吗? 不对, 我们还没响应最后的E呢. 此时看看退出状态:

$ echo $?
1

如果我们用七个'\000', 就不会有问题了:

$ printf '\000\000\000\000\000\000\000' | scp -qprf testdir
T1183832947 0 1183833956 0
D0700 0 testdir
T1183833773 0 1183833956 0
C0600 6 test
hello
E
$ echo $?
0

4. 发送错误消息

下面这个例子中, 我们将会返回2给scp, 你可以看到即使我们在 这个2后面又发送了几个'\000', scp命令也不接 受后面的这些确认信息了.

$ printf '\000\000\002n\000\000' | scp -qprf testdir

T1183895689 0 1183899084 0
D0700 0 testdir

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注