scp源码浅析
背景:
- 经常使用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);
参考: