Folytatva a folyamatban lévő cikksorozatot, amely a Windows konzol segédprogramjainak egyedi implementációiról szól, nem tehetjük meg a TFTP-t (Trivial File Transfer Protocol) – egy egyszerű fájlátviteli protokollt.
Mint legutóbb, most is nézzük meg röviden az elméletet, nézzük meg a szükségeshez hasonló funkcionalitást megvalósító kódot, és elemezzük azt. További részletek - a vágás alatt
Nem fogok másolni-beilleszteni hivatkozási információkat, amelyek linkjei hagyományosan a cikk végén találhatók, csak annyit mondok, hogy lényegében a TFTP az FTP protokoll egy egyszerűsített változata, amelyben a hozzáférés-vezérlési beállítás el lett távolítva, és valójában itt nincs más, mint a fájl fogadására és átvitelére szolgáló parancsok. Annak érdekében azonban, hogy implementációnkat egy kicsit elegánsabbá és a jelenlegi kódírási elvekhez igazodva tegyük, a szintaxist némileg módosítottuk - ez a működési elveken nem változtat, viszont az interfész, az IMHO, kicsit logikusabbá válik, ill. ötvözi az FTP és a TFTP pozitív aspektusait.
A kliens különösen indításkor lekéri a szerver IP címét és azt a portot, amelyen az egyéni TFTP nyitva van (a szabványos protokollal való összeférhetetlenség miatt célszerűnek tartottam meghagyni a felhasználónak a port kiválasztásának lehetőségét), ami után a kapcsolat jön létre, melynek eredményeként a kliens elküldheti az egyik parancsot - get or put, hogy fogadjon vagy küldjön egy fájlt a szervernek. Az összes fájl bináris módban kerül elküldésre a logika egyszerűsítése érdekében.
A protokoll megvalósításához hagyományosan 4 osztályt használtam:
- TFTPClient
- TFTPServer
- TFTPClientTester
- TFTPServerTester
Tekintettel arra, hogy a tesztelő osztályok csak a főbbek hibakeresésére léteznek, nem fogom elemezni őket, de a kód a repository-ban lesz, egy linket a cikk végén találsz. Most megnézem a főbb osztályokat.
TFTPClient
Ennek az osztálynak az a feladata, hogy csatlakozzon egy távoli szerverhez annak ip-je és portszáma alapján, beolvassa a parancsot a bemeneti adatfolyamból (jelen esetben a billentyűzetről), elemzi, átviszi a szerverre, és attól függően, hogy fájlt kell küldeni vagy fogadni, át kell vinni vagy meg kell szerezni.
A kliens elindításához szükséges kód a szerverhez való csatlakozáshoz és a bemeneti adatfolyamból érkező parancs megvárásához így néz ki. Számos itt használt globális változó leírása a cikken kívül, a program teljes szövegében található. Trivialitásuk miatt nem idézem őket, nehogy túlterheljem a cikket.
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());
}
}
Nézzük át az ebben a kódblokkban nevezett metódusokat:
Itt elküldésre kerül a fájl - egy szkenner segítségével a fájl tartalmát egy bájttömbként mutatjuk be, amit egyenként írunk a foglalatba, majd bezárjuk és újra megnyitjuk (nem a legkézenfekvőbb megoldás, de garantálja az erőforrások felszabadítását), amely után üzenetet jelenítünk meg a sikeres átvitelről.
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());
}
}
Ez a kódrészlet az adatok lekérését írja le a szerverről. Minden megint triviális, csak az első kódblokk az érdekes. Annak megértéséhez, hogy pontosan hány bájtot kell kiolvasni a socketből, tudnia kell, mennyi az átvitt fájl súlya. A kiszolgálón lévő fájl mérete hosszú egész számként van ábrázolva, így itt 4 bájt fogadható el, amelyeket utólag egy számmá alakítanak át. Ez nem túl Java megközelítés, inkább hasonló az SI-hez, de megoldja a problémáját.
Ezután minden triviális - egy ismert számú bájtot kapunk a socketből, és kiírjuk egy fájlba, ami után megjelenítünk egy sikerüzenetet.
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());
}
}
Ha a get vagy put parancstól eltérő parancsot adtunk meg az ügyfélablakban, a showErrorMessage függvény meghívódik, jelezve, hogy a bevitel helytelen volt. A trivialitás miatt nem idézem. Valamivel érdekesebb a bemeneti karakterlánc fogadásának és felosztásának funkciója. Ebbe adjuk át a szkennert, amelytől egy két szóközzel elválasztott sort kapunk, amely tartalmazza a parancsot, a forráscímet és a célcímet.
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");
}
}
Parancs küldése – továbbítja a beírt parancsot a lapolvasóból a foglalatba, és kényszeríti az elküldésre
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());
}
}
A kiválasztó egy olyan funkció, amely a beírt karakterlánctól függően határozza meg a program műveleteit. Itt minden nem túl szép, és a használt trükk nem a legjobb a kódblokkon kívüli kényszerített kilépéssel, de ennek fő oka az, hogy Java-ban hiányoznak olyan dolgok, mint a delegáltak a C#-ban, a funkciómutatók a C++-ból vagy a legalábbis a szörnyű és szörnyű goto, amely lehetővé teszi, hogy ezt gyönyörűen megvalósítsa. Ha tudja, hogyan lehet egy kicsit elegánsabbá tenni a kódot, szívesen fogadom a kritikákat a megjegyzésekben. Nekem úgy tűnik, hogy szükség van ide egy String-delegate szótárra, de nincs delegált...
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}
TFTPServer
A szerver funkcionalitása nagyjából annyiban tér el a kliens funkcionalitásától, hogy a parancsok nem a billentyűzetről, hanem a socketről érkeznek hozzá. A módszerek egy része általában megegyezik, ezért nem idézem őket, csak a különbségekre térek ki.
Kezdésként a run metódust használjuk, amely egy portot kap bemenetként, és örök hurokban dolgozza fel a socket bemeneti adatait.
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}
A put metódus, amely becsomagolja a writeToFileFromSocket metódust, amely megnyit egy írási adatfolyamot egy fájlba, és kiírja az összes bemeneti bájtot a socketből, üzenetet jelenít meg, amely jelzi az átvitel sikeres befejezését, amikor az írás befejeződik.
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());
}
}
A get metódus lekéri a kiszolgálófájlt. Ahogy a program kliens oldali részében már említettük, egy fájl sikeres átviteléhez ismerni kell a méretét, hosszú egész számban tárolva, ezért 4 bájtos tömbre bontom, bájtonként átmásolom. a socketbe, majd miután megkaptam és összeraktam őket a kliensen egy számba vissza, átviszem a fájlt alkotó összes bájtot, a bemeneti folyamból kiolvasva a fájlból.
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());
}
};
A getAndParseInput metódus ugyanaz, mint a kliensben, az egyetlen különbség az, hogy az adatokat a socketből olvassa be, nem pedig a billentyűzetről. A kód a tárolóban van, akárcsak a választó.
Ebben az esetben az inicializálás egy külön kódblokkba kerül, mert ezen a megvalósításon belül az átvitel befejezése után az erőforrások felszabadulnak, és újra lefoglalják – ismét a memóriaszivárgás elleni védelem érdekében.
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());
}
}
Összefoglalni:
Éppen most írtuk meg a saját változatunkat egy egyszerű adatátviteli protokollra, és kitaláltuk, hogyan kell működnie. Amerikát elvileg nem itt fedeztem fel, és nem írtam sok újat, de Habréról nem voltak hasonló cikkek, és a cmd segédprogramokról szóló cikksorozat részeként nem lehetett nem nyúlni hozzá.
referenciák:
Forrás: will.com