背景:
- 經常使用scp傳文件,發現它真的很給力,好奇心由來已久!
- 恰好接到一個移植SSH服務到專有網絡(非IP網絡)的小任務,完成工作又能滿足好奇心,何樂而不為!
- 我只從源碼淺淺的分析一下,后續有更多想法再補充
源碼賞析:
1、所有的故事都從main開始,也從main結束。(main也很無辜,它只是打開了計算機的一扇窗):
作為一個命令行工具,命令行是必須要處理的,這里scp也是采用常見的getopt來處理命令行。
1 while ((ch = getopt(argc, argv, "dfl:prtvBCc:i:P:q12346S:o:F:")) != -1)
上面的字符串就是可以使用的命令行選項,帶冒號的表示有參數,比如 d 表示可以在shell輸入 scp -d ...,l: 表示可以在shell輸入 scp -l 1000 ... ,當然這樣重點要提到 -r, 加上它就可以遞歸傳輸子目錄,非常實用,其他參數我就不再詳解了。
接下來會看到如下代碼:
1 /* Command to be executed on remote system using "ssh". */ 2 (void) snprintf(cmd, sizeof cmd, "scp%s%s%s%s", 3 verbose_mode ? " -v" : "", 4 iamrecursive ? " -r" : "", pflag ? " -p" : "", 5 targetshouldbedirectory ? " -d" : "");
可以看到,注釋里提到了,這是要通過ssh到遠程linux系統(你的目的電腦)去執行的一條命令,同樣也是scp喔,所以這是scp神奇又方便的原因之一!
它通過ssh連接到目的機器,同樣執行了一個scp程序來實現數據通路,完成數據接收或者發送。
注意:上面隱含了2個條件:
(1)你本機或者遠程機,兩者之間必須有一個ssh服務端
(2)兩者都必須有scp這個工具
2、文件的發送和接收
之所以來看scp的源碼,也就是對它的文件讀寫傳輸很感興趣,首先看的是如何選擇合適大小的塊來讀寫,如下函數:
1 BUF * allocbuf(BUF *bp, int fd, int blksize)
這個函數就會根據文件大小來分配一個合適的文件塊,來分塊讀寫,並網絡傳輸。
函數的返回值是一個結構,用來存放即將分配的內存地址和內存大小。
1 typedef struct { 2 size_t cnt; 3 char *buf; 4 } BUF;
而這個函數最核心的邏輯就是獲取文件的塊大小(基於文件系統I/O),並按照預定的塊大小來補齊,就像常見的64字節對齊一樣,如果你不是64字節的倍數,那就給你補齊。這里scp的預定塊大小的補齊是按16384字節來補齊的。
1 if (fstat(fd, &stb) < 0) { 2 run_err("fstat: %s", strerror(errno)); 3 return (0); 4 } 5 size = roundup(stb.st_blksize, blksize);
1 #ifndef roundup 2 # define roundup(x, y) ((((x)+((y)-1))/(y))*(y)) 3 #endif
這里,roundup就是按16384的倍數向上取整了,比如XFS的塊大小是512bytes到64KB,EXT3,EXT4的塊大小是4KB。
然后就是分配size大小的內存了。
1 if (bp->cnt >= size) 2 return (bp); 3 if (bp->buf == NULL) 4 bp->buf = xmalloc(size); 5 else 6 bp->buf = xreallocarray(bp->buf, 1, size); 7 memset(bp->buf, 0, size); 8 bp->cnt = size;
然后就是逐塊的發送文件了。

1 set_nonblock(remout); 2 for (haderr = i = 0; i < stb.st_size; i += bp->cnt) { 3 amt = bp->cnt; 4 if (i + (off_t)amt > stb.st_size) 5 amt = stb.st_size - i; 6 if (!haderr) { 7 if ((nr = atomicio(read, fd, 8 bp->buf, amt)) != amt) { 9 haderr = errno; 10 memset(bp->buf + nr, 0, amt - nr); 11 } 12 } 13 /* Keep writing after error to retain sync */ 14 if (haderr) { 15 (void)atomicio(vwrite, remout, bp->buf, amt); 16 memset(bp->buf, 0, amt); 17 continue; 18 } 19 if (atomicio6(vwrite, remout, bp->buf, amt, scpio, 20 &statbytes) != amt) 21 haderr = errno; 22 } 23 unset_nonblock(remout);
當然,如果設置了-r選項,就會遞歸處理子目錄以及子目錄的文件
文件接收方會收到發送方發過來的整個文件大小,然后整個過程就跟發送有點類似了:

1 set_nonblock(remin); 2 for (count = i = 0; i < size; i += bp->cnt) { 3 amt = bp->cnt; 4 if (i + amt > size) 5 amt = size - i; 6 count += amt; 7 do { 8 j = atomicio6(read, remin, cp, amt, 9 scpio, &statbytes); 10 if (j == 0) { 11 run_err("%s", j != EPIPE ? 12 strerror(errno) : 13 "dropped connection"); 14 exit(1); 15 } 16 amt -= j; 17 cp += j; 18 } while (amt > 0); 19 20 if (count == bp->cnt) { 21 /* Keep reading so we stay sync'd up. */ 22 if (wrerr == NO) { 23 if (atomicio(vwrite, ofd, bp->buf, 24 count) != count) { 25 wrerr = YES; 26 wrerrno = errno; 27 } 28 } 29 count = 0; 30 cp = bp->buf; 31 } 32 } 33 unset_nonblock(remin);
參考: