概要
ファイル取得処理を書くにあたって、scpとSFTPのどちらを使うのが良いか検討したのでメモ書き。scpよりSFTPの方が信頼できるというような記事 1 もあるが、実際SFTPはどういう処理をしているのか分からなかったので調べてみた。
関心事は SFTP GET がどこまで信頼できるのか?
ここで言う「信頼できるか?」は、GETの結果がエラーでなければ、確実に全データがファイルに書き込まれていることが保証されているか?のこと。
結論
- 最後まで読み込んで最後まで書き込んでいることを確認しているので、信頼できる。
- チェックサムの比較等は行っていないが、途中が抜けたり逆転したりしないように工夫しているので大丈夫。
- 下の方に書いているが、scpも特に信頼性に問題はない。
対象ソース
RHEL6で採用されているOpenSSH 4.3で確認してみる。
https://github.com/openssh/openssh-portable
tags/V_4_3_P2
SFTPソース読んだメモ
GETに行くまで
main()
は、sftp.c にある。
int
main(int argc, char **argv)
{
//引数解釈など
(略)
//sshコマンドを起動してサーバへ接続
connect_to_server(sftp_direct, args.list, &in, &out);
(略)
//ここから各SFTPコマンドの処理
err = interactive_loop(in, out, file1, file2);
(略)
}
main()
の中で connect_to_server()
することで、子プロセスとしてsshコマンドを動作させる。プロセス間通信は PIPE を繋いで実施。
interactive_loop()
で各行のSFTPコマンドを実行。GETコマンドが入った場合、 parse_dispatch_command(conn, cmd, &pwd, 1)
へ行って、その中で
case I_GET:
err = process_get(conn, path1, path2, *pwd, pflag);
で、process_get()
へ。
引数の conn
は、struct sftp_conn
型変数で、fd_in
, fd_out
がssh子プロセスへのパイプ。
struct sftp_conn {
int fd_in;
int fd_out;
u_int transfer_buflen;
u_int num_requests;
u_int version;
u_int msg_id;
};
引数 path1
、path2
はGETコマンドの引数。リモートパスとローカルパス。
引数 pwd
はリモート側のカレントディレクトリ。
GET処理
process_get()
では path1
をGLOB展開して、ファイル毎に sftp-client.c
の do_download(conn, g.gl_pathv[i], abs_dst, pflag)
を呼び出す。
g.gl_pathv[i]
はリモート側ファイル名。
abs_dst
はローカル側ファイル名。
int
do_download(struct sftp_conn *conn, char *remote_path, char *local_path,
int pflag)
{
(略)
//
}
do_download()の処理
-
TAILQ_HEAD, TAILQ_INIT でキューを準備
-
do_stat(conn, remote_path, 0)
で、SFTPサーバにSSH2_FXP_STAT
要求を送り、ファイル情報を取得。戻り値は Attrib 構造体。
/* File attributes */
struct Attrib {
u_int32_t flags;
u_int64_t size;
u_int32_t uid;
u_int32_t gid;
u_int32_t perm;
u_int32_t atime;
u_int32_t mtime;
};
サーバ側では、SSH2_FXP_STAT
が来ると、process_stat()
を呼び出し、それは process_do_stat(0)
を呼び出し、そこからさらに stat_to_attrib(&st, &a)
が呼び出される。
stat_to_attrib()
では、SSH2_FILEXFER_ATTR_SIZE
、SSH2_FILEXFER_ATTR_UIDGID
等がONに設定される。つまり、ファイルサイズやUID/GIDはサーバのものが設定される。(なお、V6.0以降は-pオプションでここの値を変えられるようになっているようだ。)
/* Convert from struct stat to filexfer attribs */
void
stat_to_attrib(const struct stat *st, Attrib *a)
{
attrib_clear(a);
a->flags = 0;
a->flags |= SSH2_FILEXFER_ATTR_SIZE;
a->size = st->st_size;
a->flags |= SSH2_FILEXFER_ATTR_UIDGID;
a->uid = st->st_uid;
a->gid = st->st_gid;
a->flags |= SSH2_FILEXFER_ATTR_PERMISSIONS;
a->perm = st->st_mode;
a->flags |= SSH2_FILEXFER_ATTR_ACMODTIME;
a->atime = st->st_atime;
a->mtime = st->st_mtime;
}
-
SFTPサーバに
SSH2_FXP_OPEN
を要求。SSH2_FXF_READ
を付けて、Readモードでの読み取りを指示。 -
handle = get_handle(conn->fd_in, id, &handle_len)
でSFTPサーバから結果を取得。SFTPサーバからの戻りメッセージについて、IDが送信したIDと違ったり、タイプがSSH2_FXP_HANDLE
でなかったらNG。 -
ローカルファイルを作成。Write openできなければNG。
local_fd = open(local_path, O_WRONLY | O_CREAT | O_TRUNC,
mode | S_IWRITE);
- buflenサイズ毎のチャンクに分けてReqd要求をSFTPサーバに送信。チャンクにはIDとlengthとoffsetがあるので、再送などで同じ部分を二回読んでしまうことは無い。
サーバ側は要求電文を受け取り、lseek(fd, off, SEEK_SET)
で seek して、ret = read(fd, buf, len)
で読み取り。読み込めた場合は send_data(id, buf, ret); status = SSH2_FX_OK
でクライアントに電文応答。EOF の場合はstatus = SSH2_FX_EOF; send_status(id, status)
で EOF をクライアントに返却。それ以外はエラー status を返却。
- atomicio()でSFTPサーバの応答を読み込む。
-
応答電文タイプが
SSH2_FXP_DATA
の場合
想定サイズより大きい量が返ってきたらNG。想定サイズ以内のデータがちゃんと返ってきていれば、atomicio()でローカルファイルに書き込み。想定サイズがちゃんと書き込めないとNG。 -
応答電文タイプが
SSH2_FXP_STATUS
の場合
StatusがSSH2_FX_EOF
(=サーバ側が最後まで読み込んだ)であればOK。それ以外はNG。
ついでに scp について
なんかソースがすごく読みにくいが、苦労して読んでみた。
処理概要
scp の引数解釈して、Dest が remote の場合は、toremote()
、Dest が local の場合は tolocal()
を呼び出し。
tolocal()
では、まずdo_cmd(host, suser, bp, &remin, &remout, argc)
を呼び出す。これは、sshコマンドを scp -f remoteファイル名
で起動する処理。ここでサーバ側でもscpが動く。
サーバ側では、scpコマンド起動時に -f
オプションが付いていると、iamaremote
フラグと fflag
が立つ。
case 'f': /* "from" */
iamremote = 1;
fflag = 1;
break;
fflag
が立っていると、response()
でとりあえず応答を返したあと、source(argc, argv)
が呼ばれる。
source()
では、ヘッダとしてタイムスタンプ情報、パーミッション情報、ファイルサイズ、ファイル名を返したあと、ファイルの中身を読みながらデータをばーっと送信して、エラーが無ければ最後に\0
を送信する。エラーがあれば run_err("%s: %s", name, strerror(haderr))
でエラー情報を送信する。
一方のクライアント側は、do_cmd()
でサーバからデータが送られてくるのを、sink(1, argv + argc - 1)
で受け取って、ファイルに書き込む。sink()
のシグネチャは
void
sink(int argc, char **argv)
{
(略)
}
であり、sink(1, argv + argc - 1)
というのは、scpコマンド引数の最後だけを引数として引き渡している、つまりローカル側ファイル名だけ渡している、ということですね。
sink()
では、ヘッダを見た後に、書き込み用にファイルをOpenする。
その後、ヘッダに書かれているファイルサイズ分、(4,096 Byte毎に)データを読み込んでファイルに書き込む。最後まで書き込めたらOK。途中で書き込めなくなるとNG。ファイルサイズ分読み込んだ後に、サーバからの結果コードを受け取る。\0
であればOK。それ以外はNG。
SFTPと比べると?
ファイルサイズ分全部が書き込まれたのをチェックしているのはどちらも同じだが、どことなくSFTPの方がチャンク単位で処理しているので信頼性が高そう。ソースのキレイさから言っても scp と SFTP を比較すると SFTP に軍配が上がりそうだが、とはいえ実績から考えると scp に問題があるとも思えず、SFTP の方が良い場面があるかは自信が持てないな。。
実際のところ、相手先が確実に *nix なのであればどちらでも変わらなそう。
違いとしては、
- scpは基本的に1ファイル転送1プロセス起動にしがちなので、小さいファイルを大量に取得するときにプロセス起動オーバーヘッドがそれなりに掛かる。(ワイルドカードで転送する場合は1プロセスで処理可能)
- scp は相手サーバが *nix であることが前提となっている。リモート側でscpコマンドを起動するので。
- scp はレジュームができない。
- scp の方がコマンド1行で呼び出しやすい。
というあたりかな。
将来的に相手サーバが増えそうだったり、レジュームが必要だったりするのでなければ scp の方が使い勝手が良さそうではある。
scpには4GBの制限がある、というのも言われているみたいだが、ソース読む限り4GBの制限がある場所がよく分からない。ファイルサイズを int で処理している場所があるので、32bit OSだと2GB MAXになりそうだが、64bit OSであれば問題なさそうだし。