Folytatva a Windows parancssori segédprogramok testreszabott megvalósításainak szentelt cikksorozatot, nem lehet figyelmen kívül hagyni a TFTP-t (Trivial File Transfer Protocol) — egy egyszerű fájlátviteli protokollt.
Ahogy az előző alkalommal, most is röviden áttekintjük az elméletet, megnézzük a funkciót megvalósító kódot, és elemezzük azt. Részletesebben — a cikk alján.
Nem szeretném másolni a hagyományos referenciainformációt, amelyet általában a cikk végén találhatunk, csupán annyit mondanék, hogy a TFTP alapvetően az FTP protokoll egyszerűsített változata, amelyből eltávolították a hozzáférési kontroll beállítást, és tulajdonképpen csak fájlok fogadására és küldésére szolgáló parancsok vannak benne. Azonban annak érdekében, hogy megvalósításunk egy kicsit elegánsabb és a mai kódírási elvekhez alkalmazkodó legyen, a szintaxis kissé megváltozott — a működési elveket nem befolyásolja, de a felület, IMHO, kicsit logikusabbá válik, kombinálva az FTP és TFTP pozitív tulajdonságait.
Különösen, amikor elindul, a kliens megkéri a szerver IP-címét és a portot, amelyen a testreszabott TFTP nyitva áll (a standard protokollal való inkompatibilitás miatt helyénvalónak tartottam, hogy lehetőséget adjak a felhasználónak a portválasztásra), majd létrejön a kapcsolat, amelynek eredményeként a kliens egyik parancsot — get vagy put — adhat ki a fájl fogadására vagy a szerverre történő küldésére. Minden 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
Mivel a tesztelő osztályok csak az alapvető osztályok hibakeresésére szolgálnak, azokat nem fogom elemezni, de a kód a tárolóban megtalálható, amelynek hivatkozása a cikk végén elérhető. Most azonban a fő osztályokat fogom áttekinteni.
TFTPClient
Ennek az osztálynak a feladata, hogy csatlakozzon a távoli szerverhez annak IP-címén és portszámán, beolvassa a bemeneti áramlásból (ebben az esetben a billentyűzetről) a parancsot, elemezze azt, átadja a szervernek, és a fájl küldésének vagy fogadásának szükségessége szerint vagy átadja vagy megkapja azt.
A kliens szerverhez való csatlakozásának kezdeti kódja és a bemeneti áramlásból származó parancs várakozása így néz ki. Az itt használt globális változók leírását a cikken kívül találják meg, a program teljes szövegében. Triviális voltuk miatt nem sorolom fel őket, hogy ne terheljem túl 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 a kódblokkban meghívott metódusokat:
Itt a fájl küldése történik — a szkenner segítségével a fájl tartalmát bájtok tömbjeként ábrázoljuk, amelyeket sorban írunk a socketbe, majd bezárjuk és újra megnyitjuk (nem a legnyilvánvalóbb megoldás, de garantálja az erőforrások felszabadítását), majd sikeres átvitelről szóló üzenetet írunk ki a képernyőre.
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 a szerverről történő adatok megszerzését írja le. Újra csak a triviális, az érdekesség az első kódrészletben rejlik. Ahhoz, hogy tudjuk, hány bájtot kell olvasni a socketből, tudnunk kell, hogy mekkora a továbbított fájl. A fájl mérete a szerveren hosszú egész számként van ábrázolva, ezért itt 4 bájtot veszünk, amelyeket később egy számra konvertálunk. Ez nem igazán Java megközelítés, inkább C-re hasonlít, de a feladatát megoldja.
A továbbiakban minden triviális — a socketből egy ismert számú bájtot kapunk és fájlba írjuk őket, majd üzenetet írunk ki a sikerességről.
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 kliens ablakába olyan parancs került beírásra, amely eltér a get vagy put parancstól, akkor a showErrorMessage függvény kerül meghívásra, amely jelzi a bemenet helytelenségét. A triviális okok miatt ezt nem részletezem. Egy kicsit érdekesebb a bemeneti szöveg megszerzésének és feldarabolásának a függvénye. Itt egy szkenner fog belekerülni, amelytől azt várjuk, hogy egy két szóközre elválasztott sort adjon vissza, amely tartalmazza a parancsot, a forráspathoz és a célpathoz.
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("Rossz bemenet");
}
}A parancs küldése – a szkennerből beírt parancs átküldése a socketre és annak kényszerített elküldése.
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 selector – függvény, amely meghatározza a program működését a beírt szöveg alapján. Itt a dolog nem túl szép, és nem a legjobb megoldást használja a kódblokk kényszerített elhagyásával, de ennek fő oka, hogy a Java nem rendelkezik olyan dolgokkal, mint a C# delegáltak, vagy a C++-ban lévő függvénymutatók, vagy legalább a rémisztő és borzalmas goto, amelyek lehetővé tennék a szép megvalósítást. Ha tudja, hogyan lehetne a kódot egy kicsit elegánsabbá tenni – várom a kritikákat a megjegyzések között. Úgy tűnik, itt szükség lenne egy String-delegált 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 alapvetően csak annyiban különbözik az ügyfél funkcionalitásától, hogy a parancsok nem a billentyűzetről érkeznek, hanem socketből. Néhány módszer egyáltalán megegyezik, ezért nem sorolom fel őket, csupán a különbségeket említem meg.
A futáshoz itt a run metódus használatos, amely egy portot kap bemenetként és folyamatosan kezeli az adatokat a socketből.
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}A put módszer, amely a writeToFileFromSocket módszer köré épül, egy írási folyamatot nyit meg egy fájlhoz, és az összes bejövő bájtot a socketen keresztül írja, majd a művelet befejezése után sikeres átviteli üzenetet küld.
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 módszer biztosítja a fájl letöltését a szerverről. Ahogy a program kliens oldaláról már említettem, a fájl sikeres átviteléhez ismerni kell a méretét, amely egy hosszú egész számként van tárolva, ezért ezt egy 4 bájtos tömbé bontom, bájtonként küldöm a socketen, majd miután visszanyertem és összeraktam a kliensen a számot, átadom az összes bájtot, amelyek a fájlból lettek olvasva.
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 módszer megfelel az ügyfélben található hasonlónak, azzal a különbséggel, hogy az adatokat socketből olvassa be, nem pedig a billentyűzetről. A kód a repositoryban, ahogy a selector.
Ebben az esetben a inicializálás külön blokkba került, mivel a megvalósítás keretein belül a források felszabadulnak az átadás befejezésével, és újra elérhetővé válnak, ismételten a memória szivárgás elleni védelem érdekében.
private void initialization()
{
try {
serverSocket = new ServerSocket(port);
socket = serverSocket.accept();
sin = socket.getInputStream();
sout = socket.getOutputStream();
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}Összefoglalva:
Most írtuk meg a saját változatunkat az egyszerű adatátviteli protokollról, és megértettük, hogy annak hogyan kell működnie. Alapvetően nem hoztam újítást, és nem írtam semmi különöset, de – ilyen jellegű cikkek nem voltak a Hubon, és a cmd eszközökről szóló cikksorozat írása során nem lehetett ezt a témát figyelmen kívül hagyni.
Linkek:
Forrás: habr.com
