Windows клиент-сервер утилиталарының функционалдығы бар бағдарламалық жасақтаманы жазу, 02 бөлім

Windows консольдік утилиталарының реттелетін іске асыруларына арналған мақалалардың үздіксіз сериясын жалғастыра отырып, біз TFTP (Trivial File Transfer Protocol) - қарапайым файлдарды тасымалдау протоколына тоқталмаймыз.

Соңғы рет сияқты, теорияға қысқаша тоқталайық, қажеттіге ұқсас функционалдылықты жүзеге асыратын кодты көріп, оны талдаймыз. Толығырақ мәліметтер - кесу астында

Мен сілтемелері дәстүрлі түрде мақаланың соңында табуға болатын анықтамалық ақпаратты көшіріп-қоймаймын, тек TFTP - бұл FTP протоколының оңайлатылған нұсқасы, онда қол жеткізуді басқару параметрі бар екенін айтайын. жойылған және файлды қабылдауға және тасымалдауға арналған пәрмендерден басқа мұнда ештеңе жоқ. Дегенмен, біздің енгізуімізді біршама талғампаз және код жазудың қазіргі принциптеріне бейімдеу үшін синтаксис аздап өзгертілді - бұл жұмыс принциптерін өзгертпейді, бірақ интерфейс, IMHO, сәл қисынды және FTP және TFTP оң аспектілерін біріктіреді.

Атап айтқанда, іске қосылған кезде клиент сервердің IP-мекен-жайын және реттелетін TFTP ашық портты сұрайды (стандартты хаттамамен үйлеспейтіндіктен мен пайдаланушыға портты таңдау мүмкіндігін қалдыруды жөн деп санадым), содан кейін қосылу орын алады, нәтижесінде клиент пәрмендердің бірін жібере алады - алу немесе қою, файлды қабылдау немесе серверге жіберу. Логиканы жеңілдету үшін барлық файлдар екілік режимде жіберіледі.

Хаттаманы енгізу үшін мен дәстүрлі түрде 4 сыныпты қолдандым:

  • TFTPClient
  • TFTPS сервері
  • TFTPClientTester
  • TFTPS ServerTester

Тестілеу сабақтары тек негізгілерін түзету үшін болғандықтан, мен оларды талдамаймын, бірақ код репозиторийде болады, оған сілтемені мақаланың соңында табуға болады. Енді мен негізгі сабақтарды қарастырамын.

TFTPClient

Бұл класстың міндеті - қашықтағы серверге оның 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());
        }
    }

Осы код блогында шақырылған әдістерді қарастырайық:

Мұнда файл жіберіледі - сканердің көмегімен біз файлдың мазмұнын байт массиві ретінде көрсетеміз, біз оны розеткаға бір-бірлеп жазамыз, содан кейін оны жабамыз және қайта ашамыз (ең айқын шешім емес, бірақ ол ресурстарды босатуға кепілдік береді), содан кейін біз сәтті тасымалдау туралы хабарламаны көрсетеміз.

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 байт қабылданады, олар кейіннен бір санға түрлендіріледі. Бұл өте 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 функциясы шақырылады. Ұсақ-түйек болғандықтан, мен оны келтірмеймін. Кіріс жолын қабылдау және бөлу функциясы біршама қызықтырақ. Біз оған сканерді жібереміз, одан екі бос орынмен бөлінген және пәрменді, бастапқы мекенжайды және тағайындалған мекенжайды қамтитын жолды аламыз деп күтеміз.

    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++ функционалдық көрсеткіштері сияқты кейбір нәрселердің болмауы. кем дегенде қорқынышты және қорқынышты гото, бұл сізге мұны әдемі түрде жүзеге асыруға мүмкіндік береді. Егер сіз кодты сәл талғампаз етіп жасауды білсеңіз, түсініктемелерде сынды құптаймын. Менің ойымша, бұл жерде String-delegate сөздігі қажет, бірақ делегат жоқ...

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

TFTPS сервері

Сервердің функционалдығы клиенттің функционалдық мүмкіндіктерінен ерекшеленеді, тек командалар оған пернетақтадан емес, ұяшықтан келеді. Кейбір әдістер негізінен бірдей, сондықтан мен оларды келтірмеймін, тек айырмашылықтарға тоқталамын.

Бастау үшін портты кіріс ретінде қабылдайтын және розеткадан кіріс деректерін мәңгілік циклде өңдейтін іске қосу әдісі пайдаланылады.

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

Файлға жазу ағынын ашатын және ұяшықтан барлық кіріс байттарын жазатын writeToFileFromSocket әдісін орап алатын put әдісі жазу аяқталған кезде тасымалдаудың сәтті аяқталғанын көрсететін хабарды көрсетеді.

    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 әдісі сервер файлын шығарып алады. Бағдарламаның клиенттік жағындағы бөлімде айтылғандай, файлды сәтті тасымалдау үшін оның өлшемін білу керек, ұзын бүтін санда сақталады, сондықтан мен оны 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 туралы қысқаша
Дәл солай, бірақ орысша

Ақпарат көзі: www.habr.com

пікір қалдыру