zshでネットワーク経由のファイル転送

zsh(とtar)を使ってネットワーク経由のファイル転送を効率的に行う方法についてです。
ARMボード(RaspberryPiとかPCduino等)を使っていると、scpなどでホスト間で大きなファイル転送をする際に、CPUネックになりがち。rsyncやscpするときに暗号をarcfour等にする*1ことでCPU使用率を低減する方法もありますが、そもそもLAN内等で暗号化しなくても良い場合もあります。こういうとき、単一のファイルであれば、

受信側(host1): nc -l 12345 >file
送信側(host2): nc host1 12345 <file

とncを使えばOK。(ポートがファイアウォールで塞がれていないことが前提ですが。)


複数のファイルだと

受信側(host1): nc -l 12345 | tar vxf -
送信側(host2): tar vcf - files/* | nc host1 12345

のように転送する方法がありますが、これだとパイプ経由でのデータコピーのためにncによってCPU使用率が上がってしまい、単発の場合と比べて性能が悪くなってしまいます。


bashであれば、送信側についてはncを経由せずに、

tar vcf - files >/dev/tcp/host1/12345

とすることで直接TCPソケットに書き出せます。が、受信側は知る限りこういった方法がありません。


zshであれば、標準でついてくるnet/tcpモジュールを使って、送受信側とも直接TCPソケットに読み書きさせることができます。

ただ、net/tcpが提供する各コマンドはシェルスクリプトTCPサーバを書くことを念頭に作られているようで、今回の目的にはローレベルのztcpコマンドを操作しないといけないようです。手動では若干面倒なので、.zshrc内にあらかじめ以下の関数を作っておきます。

function file_recv {
        if [ $# = 0 ]; then
                echo file_recv listen_port
                return
        fi
        (
        autoload -U tcp_open; tcp_open >/dev/null 2>/dev/null # define ztcp
        ztcp -l "$1"
        fd_listen=$REPLY
        echo "Waiting on port $1 (fd $fd_listen) ..."
        ztcp -a $fd_listen || echo failed.
        fd_accept=$REPLY
        echo "Connected. (fd $fd_accept)"
        tar vxf - <&$fd_accept
        ztcp -c $fd_listen
        ztcp -c $fd_accept
        )
}
function file_send {
        if [ $# -lt 3 ]; then
                echo file_send host port files ...
                return
        fi
        (
        autoload -U tcp_open; tcp_open >/dev/null 2>/dev/null # define ztcp
        ztcp "$1" "$2"
        shift 2
        tar vcf - "$@" >&$REPLY
        ztcp -c $REPLY
        )
}

あとは、

受信側(host1): file_recv 12345
送信側(host2): file_send host1 12345 files/*

とすればOKです。送信側は上記bashを使ってもOKです、その方が事前に準備要らないですからね。*2


しかしなんというか、ここまでするなら、tarも使わずsendfile(2)とかsplice(2)等を使ってユーザ空間でのコピーも避けられるようなさらに効率的な転送コマンドを用意する方がいい気がしてきた。


あと、tar vcf - ... の部分に任意のコマンドを指定できるようにした方が利用シーンが広がって有用かもしれない(効率のいいncとして使う)。

*1:参考: http://d.hatena.ne.jp/rx7/20101025/p1

*2:どちらから接続するかを入れ替えれば、"受信側は上記関数の準備不要"とするということもできるはず。