Voortbouwend op de reeds begonnen cyclus artikelen over aangepaste implementaties van Windows-console-tools, kunnen we TFTP (Trivial File Transfer Protocol) niet ongemoeid laten - een eenvoudige bestandsoverdrachtprotocol.
Net als de vorige keer zullen we kort de theorie doornemen, de code bekijken die een functionaliteit implementeert die vergelijkbaar is met wat vereist is, en deze analyseren. Meer details zijn onder het kopje.
Ik zal geen referentie-informatie kopiëren, de links waarvan traditioneel aan het einde van het artikel te vinden zijn. Ik zal alleen zeggen dat TFTP in wezen een vereenvoudigde variant is van het FTP-protocol, waarbij de configuratie van toegangscontrole is verwijderd. Er is in feite niets meer dan commando's voor het ontvangen en verzenden van bestanden. Om onze implementatie iets eleganter te maken en aan te passen aan de huidige principes van coderen, is de syntaxis echter iets aangepast - de werkprincipes blijven hetzelfde, maar de interface, IMHO, wordt iets logischer en combineert de positieve aspecten van zowel FTP als TFTP.
Bij het opstarten vraagt de client om het IP-adres van de server en de poort waarop de aangepaste TFTP geopend is (vanwege incompatibiliteit met het standaardprotocol vond ik het gepast om de gebruiker de keuze van de poort te laten), waarna de verbinding tot stand komt, zodat de client een van de commando's kan verzenden - get of put, om een bestand naar de server te ontvangen of te verzenden. Alle bestanden worden in binaire modus verzonden - om de logica te vereenvoudigen.
Voor de implementatie van het protocol heb ik traditioneel 4 klassen gebruikt:
- TFTPClient
- TFTPServer
- TFTPClientTester
- TFTPServerTester
Aangezien de testklassen alleen bestaan voor het debuggen van de hoofdkoppen zal ik ze niet bespreken, maar de code zal beschikbaar zijn in de repository. Een link naar deze repository is aan het einde van het artikel te vinden. Nu zal ik de hoofdkoppen bespreken.
TFTPClient
De taak van deze klasse is om verbinding te maken met de externe server via zijn IP-adres en poortnummer, een commando van de invoerstream (in dit geval - het toetsenbord) te lezen, het te parseren, het naar de server te verzenden, en afhankelijk van of een bestand moet worden verzonden of ontvangen, dat te verzenden of te ontvangen.
De code voor het starten van de client om verbinding te maken met de server en te wachten op een commando van de invoerstream ziet er als volgt uit. Een reeks globale variabelen die hier worden gebruikt, worden buiten het artikel beschreven, in de volledige tekst van het programma. Vanwege hun trivialiteit geef ik ze niet weer om het artikel niet te overladen.
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());
}
}Laten we de methoden doornemen die in dit codeblok worden aangeroepen:
Hier vindt de verzending van het bestand plaats — met behulp van de scanner vertegenwoordigen we de inhoud van het bestand als een byte-array, die we een voor een naar de socket schrijven, waarna we deze sluiten en opnieuw openen (niet de meest voor de hand liggende oplossing, maar het garandeert hulpbronvrijgave), waarna we een bericht op het scherm weergeven over een succesvolle overdracht.
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());
}
}Dit codefragment beschrijft het ophalen van gegevens van de server. Alles is opnieuw triviaal, interessante aandacht gaat alleen uit naar het eerste codeblok. Om te begrijpen hoeveel bytes er precies van de socket moeten worden gelezen, moet je weten hoe groot het te verzenden bestand is. De bestandsgrootte op de server wordt weergegeven als een lang geheel getal, daarom wordt hier 4 bytes aangenomen, die vervolgens worden geconverteerd naar één nummer. Dit is niet echt een typische Java-aanpak, maar eerder iets dat lijkt op C, maar het lost wel zijn taak op.
Verder is alles triviaal — we ontvangen het bekende aantal bytes van de socket en schrijven deze naar een bestand, waarna we een succesbericht weergeven.
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());
}
}Als er een commando in het klantvenster is ingevoerd dat anders is dan get of put, zal de functie showErrorMessage worden aangeroepen, die de onjuistheid van de invoer weergeeft. Vanwege de trivialiteit laat ik het achterwege. Interessanter is de functie voor het verkrijgen en splitsen van de invoerstring. We geven een scanner door, waarvan we een string verwachten die is gescheiden door twee spaties en die een commando, een bronadres en een bestemmingsadres bevat.
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("Onjuiste invoer");
}
}Het verzenden van een commando — het doorgeven van het ingevoerde commando vanuit de scanner naar de socket en het geforceerd verzenden ervan.
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());
}
}De selector — een functie die de acties van het programma bepaalt op basis van de ingevoerde string. Het is hier allemaal niet zo mooi en er wordt een niet zo goed principe gebruikt met geforceerd verlaten van het codeblok, maar de belangrijkste reden hiervoor is het ontbreken van bepaalde dingen in Java, zoals delegaten in C#, functiewijzers uit C++ of tenminste het vreselijke goto, dat dit mooi zou kunnen implementeren. Als je weet hoe je de code iets eleganter kunt maken — ik kijk uit naar kritiek in de opmerkingen. Ik denk dat hier een woordenboek String-delegate nodig is, maar er zijn geen delegaten...
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}TFTPServer
De functionaliteit van de server verschilt in wezen niet van die van de client, behalve dat de commando's niet vanaf het toetsenbord komen, maar uit de socket. Een deel van de methoden komt helemaal overeen, dus ik geef ze niet, ik zal alleen de verschillen aansteken.
Voor de uitvoering wordt hier de methode run gebruikt, die een poort als invoer krijgt en de binnenkomende gegevens uit de socket in een oneindige lus verwerkt.
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}De put-methode, die een wrapper is voor de writeToFileFromSocket-methode, opent een schrijfstroom naar een bestand en schrijft alle bytes van de invoer uit de socket. Na voltooiing van het schrijven wordt er een bericht weergegeven over het succesvolle einde van de overdracht.
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());
}
}De get-methode zorgt voor het ophalen van een bestand van de server. Zoals al eerder vermeld in het gedeelte over de clientzijde van het programma, is het nodig om de bestandsgrootte te kennen die is opgeslagen als een lange integer. Daarom splits ik deze op in een array van 4 bytes, verzend deze byte voor byte naar de socket en daarna, ontvang en voeg ze weer samen op de client tot een getal, waarna ik alle bytes die het bestand vormen vanuit de invoerstroom lees.
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());
}
}; De getAndParseInput-methode komt overeen met de equivalente in de client, met het enige verschil dat het gegevens leest van de socket en niet van het toetsenbord. De code in de repository is hetzelfde als de selector.
In dit geval is de initialisatie in een aparte codeblok geplaatst, omdat in deze implementatie de hulpbronnen na het einde van de overdracht worden vrijgegeven en opnieuw worden toegewezen — opnieuw met als doel het beschermen tegen geheugenlekken.
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());
}
}Samenvattend:
We hebben zojuist onze variant van een eenvoudig protocol voor gegevensoverdracht geschreven en onderzocht hoe het zou moeten werken. In principe heb ik hier Amerika niet opnieuw ontdekt en heb ik niet echt iets nieuws geschreven, maar er waren geen vergelijkbare artikelen op Habr en in het kader van het schrijven van een cyclus artikelen over cmd-tools kon ik het niet negeren.
Links:
Bron: habr.com
