Pokračováním v cyklu článků věnovaných vlastním realizacím konzolových utilit Windows nelze opomenout TFTP (Trivial File Transfer Protocol) — jednoduchý protokol pro přenos souborů.
Stejně jako minule si stručně projdeme teorii, uvidíme kód, který realizuje funkčnost podobnou požadované, a provedeme jeho analýzu. Podrobněji — pod textem.
Nebudu kopírovat referenční informace, jejichž odkazy lze tradičně najít na konci článku, řeknu jen, že TFTP je v podstatě zjednodušená varianta protokolu FTP, kde byla odstraněna konfigurace kontroly přístupu, a v podstatě tu není nic kromě příkazů pro příjem a odeslání souboru. Abychom však naší realizaci dali trochu elegantnější a modernější podobu v duchu současných kódovacích principů, byl syntakticky mírně upraven — principy funkce to nemění, ale uživatelské rozhraní, IMHO, se stává logičtějším a kombinuje pozitivní aspekty FTP a TFTP.
Konkrétně při spuštění klient žádá o IP adresu serveru a port, na kterém je otevřen vlastní TFTP (vzhledem k nekompatibilitě se standardním protokolem jsem považoval za vhodné ponechat uživatelům možnost výběru portu), poté dochází k připojení, v jehož důsledku může klient poslat jeden z příkazů — get nebo put, pro příjem nebo odeslání souboru na server. Všechny soubory se odesílají v binárním režimu — pro zjednodušení logiky.
Pro realizaci protokolu jsem tradičně použil 4 třídy:
- TFTPClient
- TFTPServer
- TFTPClientTester
- TFTPServerTester
Vzhledem k tomu, že testovací třídy existují pouze pro ladění hlavních, nebudu je podrobně rozebírat, ale kód bude dostupný v repozitáři, na jehož odkaz lze nalézt na konci článku. Hlavní třídy nyní podrobněji rozeberu.
TFTPClient
Úkolem této třídy je připojit se k vzdálenému serveru podle jeho IP adresy a čísla portu, přečíst příkaz ze vstupního proudu (v tomto případě — klávesnice), analyzovat ho, poslat serveru a v závislosti na tom, zda je třeba odeslat nebo přijmout soubor, ho odeslat nebo přijmout.
Kód spouštějící klienta pro připojení k serveru a čekání na příkaz ze vstupního proudu vypadá takto. Řada globálních proměnných, které jsou zde použity, je popsána mimo článek, v úplném textu programu. Vzhledem k jejich triviálnosti je nezmiňuji, abych nepřeplnil článek.
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());
}
}Projdeme si metody volané v tomto bloku kódu:
Zde dochází k odeslání souboru — pomocí skeneru představujeme obsah souboru jako pole bytů, které postupně zapisujeme do socketu, po čemž jej zavřeme a znovu otevřeme (nejde o nejjasnější řešení, ale zaručuje uvolnění zdrojů), poté na obrazovku vyvedeme zprávu o úspěšném přenosu.
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("nHotovo");
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}Tento fragment kódu popisuje získání dat ze serveru. Všechno je opět triviální, zajímavý je pouze první blok kódu. Abychom rozuměli, kolik bytů je třeba přečíst ze socketu, musíme znát velikost přenášeného souboru. Velikost souboru na serveru je představena jako dlouhé celé číslo, proto se zde přijímají 4 byty, které jsou následně převedeny na jedno číslo. To není příliš Java přístup, spíše se podobá C, ale splňuje svůj úkol.
Dále už je všechno triviální — získáme známý počet bytů ze socketu a zapíšeme je do souboru, po čemž vyvedeme zprávu o úspěchu.
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("nHotovo");
}
catch (Exception e){
System.out.println(e.getMessage());
}
}Pokud byl do klientského okna zadán příkaz jiný než get nebo put, bude vyvolána funkce showErrorMessage, která signalizuje nesprávný vstup. Z důvodu jednoduchosti jej neuvádím. Zajímavější je funkce pro získání a rozdělení vstupního řetězce. Do ní předáváme skener, od kterého očekáváme, že nám poskytne řetězec, oddělený dvěma mezerami a obsahující příkaz, zdrojovou adresu a cílovou adresu.
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("Špatný vstup");
}
}Odeslání příkazu – přenos zadaného příkazu ze skeneru do socketu a jeho vynucené odeslání.
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());
}
}Selector – funkce, která určuje akce programu na základě zadaného řetězce. Tady to není příliš krásné a používá se zde ne zcela elegantní způsob vynuceného výstupu za blok kódu, ale hlavním důvodem je nedostatek určitých věcí v Javě, jako delegáty v C#, ukazatele na funkci z C++ nebo alespoň hrozné goto, které umožňují to realizovat elegantně. Pokud víte, jak udělat kód o něco estetičtější – očekávám kritiku v komentářích. Myslím, že zde potřebujeme slovník String-delegate, ale delegát tu není...
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}TFTPServer
Funkcionalita serveru se od funkcionality klienta liší zásadně jen tím, že příkazy na něj přicházejí ne z klávesnice, ale ze socketu. Některé metody se dokonce shodují, proto je neuvádím, dotknu se jen rozdílů.
Pro spuštění se zde používá metoda run, která přijímá port a zpracovává vstupní data ze socketu v nekonečné smyčce.
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}Metoda put, která je wrapperem pro metodu writeToFileFromSocket, otevírá zápisový proud do souboru a zapisuje všechny bajty vstupu ze socketu. Po dokončení zápisu vypíše zprávu o úspěšném skončení přenosu.
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());
}
}Metoda get zajišťuje získání souboru ze serveru. Jak již bylo zmíněno v části o klientské straně programu, pro úspěšný přenos souboru je potřeba znát jeho velikost, která se uchovává jako dlouhé celé číslo. Proto ji rozdělím na pole o 4 bajtech, bajt po bajtu je odesílám do socketu a poté, co je obdržím a zrekonstruuji na klientovi zpět do čísla, přenesu všechny bajty, které tvoří soubor, přečtené z vstupního proudu.
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());
}
}; Metoda getAndParseInput se shoduje s obdobnou metodou na klientovi, s tím rozdílem, že čte data ze socketu, nikoli z klávesnice. Kód v repozitáři je podobný jako selector.
V tomto případě byla inicializace vyčleněna do samostatného bloku kódu, protože v rámci této implementace po dokončení přenosu zdroje jsou uvolněny a znovu obsazeny — opět s cílem zajistit ochranu proti úniku paměti.
private void inicialization()
{
try {
serverSocket = new ServerSocket(port);
socket = serverSocket.accept();
sin = socket.getInputStream();
sout = socket.getOutputStream();
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}Shrnuto:
Právě jsme napsali svou variantu jednoduchého protokolu přenosu dat a podívali se na to, jak by měl fungovat. V zásadě jsem tady neobjevil Ameriku a nevymyslel nic převratného, ale podobné články na Habru zatím nebyly, a v rámci psaní cyklu článků o utilitách cmd jsem ho nemohl minout.
Odkazy:
Zdroj: habr.com
