Continuing the series of articles on custom implementations of Windows console utilities, TFTP (Trivial File Transfer Protocol) is a simple file transfer protocol.
Like last time, we briefly go over the theory, see a code that implements a functional similar to that required, and analyze it. Read more - under the cut
I won’t copy-paste the reference information, links to which can be traditionally found at the end of the article, I’ll just say that in essence TFTP is a simplified variation of the FTP protocol in which the access control setting is removed, and in fact there is nothing other than commands to receive and transfer a file . However, in order to make our implementation a little more elegant and adapted to the current principles of writing code, the syntax is slightly changed - it does not change the working principles, but the interface, IMHO, becomes a little more logical and combines the positive aspects of FTP and TFTP.
')
In particular, when starting up, the client requests the server’s IP address and the port on which custom TFTP is open (due to incompatibility with the standard protocol, I considered it appropriate to leave the option to select the port to the user), after which a connection occurs, as a result of which the client can send one of the commands - get or put, to receive or send a file to the server. All files are sent in binary mode - in order to simplify the logic.
For the implementation of the proto, I have traditionally used 4 classes:
- TFTPClient
- TFTPServer
- TFTPClientTester
- TFTPServerTester
Due to the fact that testing classes exist only for debugging the main ones, I will not analyze them, but the code will be in the repository, a link to it can be found at the end of the article. And now I will understand the main classes.
TFTPClient
The task of this class is to connect to the remote server by its ip and port number, read a command from the input stream (in this case, the keyboard), parse it, transfer it to the server, and depending on whether you want to transfer or receive the file, transfer it or receive.
The client launch code for connecting to the server and waiting for a command from the input stream looks like this. A number of global variables that are used here are described outside the article, in the full text of the program. Due to their triviality, I do not quote in order not to overload the article.
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()); } }
Let's go over the methods called in this block of code:
Here the file is sent - using the scanner, we present the contents of the file as an array of bytes, which we write to the socket one by one, then close it and open it again (not the most obvious solution, but it guarantees the release of resources), after which we display a message about the success transmission.
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("\nDone\n"); } catch (Exception e) { System.out.println(e.getMessage()); } }
This code fragment describes the receipt of data from the server. Everything is trivial again, only the first block of code is of interest. In order to understand exactly how many bytes you need to read from the socket, you need to know how much the transferred file weighs. The file size on the server appears to be a long integer, so 4 bytes are accepted here, which are subsequently converted to a single number. This is not a very Java approach, it is rather similar for SI, but it solves its problem.
Then everything is trivial - we get the known number of bytes from the socket and write them to a file, after which we display a success message.
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("\nDONE\n"); } catch (Exception e){ System.out.println(e.getMessage()); } }
If a command other than get or put was entered into the client window, the showErrorMessage function will be called, showing the incorrectness of the input. Due to triviality - I do not quote. Somewhat more interesting is the function of getting and splitting the input string. We pass a scanner to it, from which we expect to receive a line separated by two spaces and containing a command, source address and destination address.
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"); } }
Sending a command - sending a command entered from a scanner to a socket and forcing it to be sent
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 is a function that determines the actions of a program depending on the input string. Everything is not very beautiful here and not the best technique is used with forcing the code block beyond the limits, but the main reason for this is the lack of some things in Java like delegates in C #, function pointers from C ++, or at least scary and terrible goto, which let you realize it beautifully. If you know how to make the code a little more elegant - I'm waiting for criticism in the comments. It seems to me that a String-delegate dictionary is needed here, but there is no delegate ...
private void selector() { do{ if (typeOfCommand.equals("get")){ get(sourcePath, destPath); break; } if (typeOfCommand.equals("put")){ put(sourcePath, destPath); break; } showErrorMessage(); } while (false); } }
TFTPServer
The functionality of the server differs from the functionality of the client by and large only in that the commands to it come not from the keyboard, but from the socket. Some of the methods coincide, so I won’t give them; I’ll only mention the differences.
To run here, the run method is used, which receives a port for input and processes input data from the socket in an eternal loop.
public void run(int port) { this.port = port; incialization(); while (true) { getAndParseInput(); selector(); } }
The put method, which is a wrapper of the writeToFileFromSocket method, which opens the stream of writing to the file and writes all input bytes from the socket, displays a message after the completion of the transfer.
private void put(String source, String dest){ writeToFileFromSocket(); System.out.print("\nDone\n"); }; 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()); } }
The get method provides a server file. As already mentioned in the section on the client side of the program, to successfully transfer a file, you need to know its size, stored in a long integer, so I split it into an array of 4 bytes, transfer them to the socket byte, and then, having received and collected them on the client back to the number, I transfer all the bytes that make up the file, read from the input stream from the file.
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()); } };
The getAndParseInput method is the same as in the client, with the only difference being that it reads data from the socket, and not from the keyboard. The code in the repository, like selector.
In this case, the initialization is made in a separate block of code, because in the framework of this implementation, after the transfer is completed, the resources are freed and re-occupied again, again with the aim of providing protection against memory leaks.
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()); } }
In summary:
We just wrote our variation on the topic of a simple data transfer protocol and figured out how it should work. In principle, I did not discover America and did not write much new, but - there were no similar articles on Habré, and as part of writing a series of articles about cmd utilities it was impossible not to touch on him.
References:
Source code repositoryBriefly about TFTPSame thing, but in Russian