Windows クライアント/サーバー ユーティリティの機能を備えたソフトウェアの作成、パート 02

Windows コンソール ユーティリティのカスタム実装に特化した継続的な記事シリーズを続けると、単純なファイル転送プロトコルである TFTP (Trivial File Transfer Protocol) について触れずにはいられません。

前回と同様に、理論を簡単に説明し、必要な機能と同様の機能を実装するコードを確認し、分析してみましょう。 詳細 - アンダーザカット

参照情報をコピーして貼り付けることはしません。従来の記事の最後にあるリンクへのリンクは、本質的に、TFTP は FTP プロトコルの簡略化されたバリエーションであり、アクセス制御設定には次のような特徴があることだけを述べます。実際、ここにはファイルの受信と転送を行うコマンド以外は何もありません。 ただし、実装をもう少しエレガントにし、現在のコード記述原則に適合させるために、構文がわずかに変更されました。これは動作原理を変更しませんが、インターフェイスは、私の意見では、もう少し論理的になり、 FTP と TFTP の良い面を組み合わせたものです。

特に、クライアントは起動時にサーバーの IP アドレスと、カスタム TFTP が開いているポートを要求します (標準プロトコルとの互換性がないため、ユーザーがポートを選択できるようにすることが適切であると考えました)。接続が発生すると、クライアントはコマンド get または put の XNUMX つを送信して、サーバーとファイルを送受信することができます。 ロジックを簡素化するために、すべてのファイルはバイナリ モードで送信されます。

プロトコルを実装するために、私は従来、次の 4 つのクラスを使用していました。

  • TFTPクライアント
  • TFTPサーバー
  • TFTPクライアントテスター
  • TFTPサーバーテスター

テスト クラスは主要なクラスをデバッグするためにのみ存在するため、分析はしませんが、コードはリポジトリにあります。そのリンクは記事の最後にあります。 次に、主要なクラスを見ていきます。

TFTPクライアント

このクラスのタスクは、IP とポート番号によってリモート サーバーに接続し、入力ストリーム (この場合はキーボード) からコマンドを読み取り、それを解析してサーバーに転送し、ファイルを送受信したり、転送したり、取得したりする必要があります。

クライアントを起動してサーバーに接続し、入力ストリームからのコマンドを待機するコードは次のようになります。 ここで使用される多数のグローバル変数は、記事の外、プログラムの全文で説明されています。 些細なことなので、記事が過負荷にならないように引用しません。

 public void run(String ip, int port)
    {
        this.ip = ip;
        this.port = port;
        try {
            inicialization();
            Scanner keyboard = new Scanner(System.in);
            while (isRunning) {
                getAndParseInput(keyboard);
                sendCommand();
                selector();
                }
            }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

このコード ブロックで呼び出されるメソッドを見てみましょう。

ここではファイルが送信されます - スキャナーを使用して、ファイルの内容をバイト配列として提示し、それを XNUMX つずつソケットに書き込み、その後ファイルを閉じて再度開きます (最も明白な解決策ではありませんが、リソースの解放が保証されます)、その後、転送の成功に関するメッセージが表示されます。

private  void put(String sourcePath, String destPath)
    {

        File src = new File(sourcePath);
        try {

            InputStream scanner = new FileInputStream(src);
            byte[] bytes = scanner.readAllBytes();
            for (byte b : bytes)
                sout.write(b);
            sout.close();
            inicialization();
            System.out.println("nDonen");
            }

        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

このコード部分は、サーバーからのデータの取得について説明しています。 ここでもすべては簡単で、コードの最初のブロックのみが重要です。 ソケットから何バイト読み取る必要があるかを正確に理解するには、転送されるファイルの重さを知る必要があります。 サーバー上のファイル サイズは長整数として表されるため、ここでは 4 バイトが受け入れられ、その後 XNUMX つの数値に変換されます。 これは Java らしいアプローチではなく、むしろ SI に似ていますが、問題は解決されます。

その後、すべては簡単です。ソケットから既知のバイト数を受け取り、それらをファイルに書き込み、その後成功メッセージを表示します。

   private void get(String sourcePath, String destPath){
        long sizeOfFile = 0;
        try {


            byte[] sizeBytes = new byte[Long.SIZE];
           for (int i =0; i< Long.SIZE/Byte.SIZE; i++)
           {
               sizeBytes[i] = (byte)sin.read();
               sizeOfFile*=256;
               sizeOfFile+=sizeBytes[i];
           }

           FileOutputStream writer = new FileOutputStream(new File(destPath));
           for (int i =0; i < sizeOfFile; i++)
           {
               writer.write(sin.read());
           }
           writer.close();
           System.out.println("nDONEn");
       }
       catch (Exception e){
            System.out.println(e.getMessage());
       }
    }

get または put 以外のコマンドがクライアント ウィンドウに入力された場合、showErrorMessage 関数が呼び出され、入力が正しくなかったことを示します。 些細なことなので引用しません。 さらに興味深いのは、入力文字列を受け取って分割する関数です。 スキャナーをそこに渡します。そこから、XNUMX つのスペースで区切られ、コマンド、送信元アドレス、宛先アドレスが含まれる行を受け取ることが期待されます。

    private void getAndParseInput(Scanner scanner)
    {
        try {

            input = scanner.nextLine().split(" ");
            typeOfCommand = input[0];
            sourcePath = input[1];
            destPath = input[2];
        }
        catch (Exception e) {
            System.out.println("Bad input");
        }
    }

コマンドの送信 - スキャナーから入力されたコマンドをソケットに送信し、強制的に送信します。

    private void sendCommand()
    {
        try {

            for (String str : input) {
                for (char ch : str.toCharArray()) {
                    sout.write(ch);
                }
                sout.write(' ');
            }
            sout.write('n');
        }
        catch (Exception e) {
            System.out.print(e.getMessage());
        }
    }

セレクターは、入力された文字列に応じてプログラムの動作を決定する関数です。 ここにあるものはすべてあまり美しくなく、使用されているトリックもコード ブロックの外で強制終了するという最善のものではありませんが、その主な理由は、Java には C# のデリゲート、C++ の関数ポインタ、または少なくとも、これを美しく実装できるようにするひどいひどい goto は除きます。 コードをもう少しエレガントにする方法をご存知の場合は、コメントでの批判を歓迎します。 ここでは文字列とデリゲートの辞書が必要なように思えますが、デリゲートがありません...

    private void selector()
    {
        do{
            if (typeOfCommand.equals("get")){
                get(sourcePath, destPath);
                break;
            }
            if (typeOfCommand.equals("put")){
                put(sourcePath, destPath);
                break;
            }
            showErrorMessage();
        }
        while (false);
    }
}

TFTPサーバー

サーバーの機能は、概して、コマンドがキーボードからではなくソケットからサーバーに送信されるという点でのみクライアントの機能と異なります。 いくつかの方法はほぼ同じなので、引用はせず、違いについてのみ触れます。

まず、run メソッドが使用されます。このメソッドはポートを入力として受け取り、ソケットからの入力データを永遠ループで処理します。

    public void run(int port) {
            this.port = port;
            incialization();
            while (true) {
                getAndParseInput();
                selector();
            }
    }

put メソッドは、ファイルへの書き込みストリームを開き、ソケットからすべての入力バイトを書き込む writeToFileFromSocket メソッドをラップし、書き込みが完了すると、転送が正常に完了したことを示すメッセージを表示します。

    private  void put(String source, String dest){
            writeToFileFromSocket();
            System.out.print("nDonen");
    };
    private void writeToFileFromSocket()
    {
        try {
            FileOutputStream writer = new FileOutputStream(new File(destPath));
            byte[] bytes = sin.readAllBytes();
            for (byte b : bytes) {
                writer.write(b);
            }
            writer.close();
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
    }

get メソッドはサーバー ファイルを取得します。 プログラムのクライアント側のセクションですでに述べたように、ファイルを正常に転送するには、long 整数で保存されたファイルのサイズを知る必要があるため、ファイルを 4 バイトの配列に分割し、バイトごとに転送します。ソケットに送信し、クライアント上でそれらを受信して​​数値に組み立ててから、ファイルの入力ストリームから読み取ったファイルを構成するすべてのバイトを転送します。


 private  void get(String source, String dest){
        File sending = new File(source);
        try {
            FileInputStream readFromFile = new FileInputStream(sending);
            byte[] arr = readFromFile.readAllBytes();
            byte[] bytes = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(sending.length()).array();
            for (int i = 0; i<Long.SIZE / Byte.SIZE; i++)
                sout.write(bytes[i]);
            sout.flush();
            for (byte b : arr)
                sout.write(b);
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
    };

getAndParseInput メソッドはクライアントのメソッドと同じですが、唯一の違いは、キーボードからではなくソケットからデータを読み取ることです。 コードはセレクターと同様にリポジトリにあります。
この場合、初期化はコードの別のブロックに配置されます。 この実装では、転送が完了した後、メモリ リークに対する保護を提供するために、リソースが解放され、再び再占有されます。

    private void  incialization()
    {
        try {
            serverSocket = new ServerSocket(port);
            socket = serverSocket.accept();
            sin = socket.getInputStream();
            sout = socket.getOutputStream();
        }
        catch (Exception e) {
            System.out.print(e.getMessage());
        }
    }

要約すると:

私たちは単純なデータ転送プロトコルの独自のバリエーションを作成し、それがどのように機能するかを理解したところです。 基本的に、私はここでアメリカを発見したことはなく、新しいことはあまり書きませんでしたが、ハブレに関する同様の記事はなく、cmd ユーティリティに関する一連の記事を書く一環として、ハブレに触れないわけにはいきませんでした。

リンク:

ソースコードリポジトリ
TFTP について簡単に説明します
同じことですが、ロシア語では

出所: habr.com

コメントを追加します