Development of software with client-server utilities functionality for Windows, part 02

Continuing the ongoing series of articles dedicated to custom implementations of Windows console utilities, we cannot overlook TFTP (Trivial File Transfer Protocol) — a simple file transfer protocol.

As in the previous time, let's briefly go over the theory, see the code that implements functionality similar to what is required, and analyze it. More details — below the cut.

I won’t copy-paste reference information, the links to which can traditionally be found at the end of the article. I will only say that TFTP is essentially a simplified variation of the FTP protocol, where access control configuration is removed, and essentially there is nothing here except commands for receiving and sending files. However, to make our implementation a bit more elegant and adapted to current coding principles, the syntax has been slightly altered — this does not change the working principles, but in my opinion, the interface becomes a little more logical and combines the positive aspects of both FTP and TFTP.

In particular, when starting, the client requests the server's IP address and the port on which the custom TFTP is open (due to incompatibility with the standard protocol, I found it appropriate to leave the option for the user to choose the port), after which a connection is established, as a result of which the client can send one of the commands — get or put, for receiving or sending a file to the server. All files are sent in binary mode — for simplicity of logic.

To implement the protocol, I traditionally used 4 classes:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Since the testing classes exist only for debugging the main ones, I won’t discuss them, but the code will be available in the repository, the link to which can be found at the end of the article. Now I will discuss the main classes.

TFTPClient

The task of this class is to connect to a remote server by its IP and port number, read the command from the input stream (in this case — the keyboard), parse it, send it to the server, and depending on whether a file transfer or retrieval is required, transfer it or receive it.

The client startup code for connecting to the server and waiting for a command from the input stream looks like this. A number of global variables used here are described outside the article, in the complete program text. Due to their trivial nature, I won't provide them to avoid overloading 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 through the methods called in this block of code:

Here, the file is being sent—using the scanner, we represent the file content as a byte array, which we write to the socket one at a time. After that, we close it and reopen it (not the most obvious solution, but it ensures resource release), and then we display a message about successful 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("nDonen");
            }

        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

This code fragment describes receiving data from the server. Again, everything is trivial; the only interesting part is the first block of code. To understand how many bytes need to be read from the socket, we need to know the size of the transmitted file. The file size on the server is represented as a long integer, so we take 4 bytes here, which are later converted into a single number. This is not a very 'Java' approach; it resembles something more typical for C, but it solves its task.

Next, it's straightforward—we read the known number of bytes from the socket and write them into 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("nDONEn");
       }
       catch (Exception e){
            System.out.println(e.getMessage());
       }
    }

If a command other than get or put is entered in the client window, the showErrorMessage function will be called, indicating incorrect input. Given its triviality, I won't elaborate. A bit more interesting is the function for receiving and parsing the input string. It takes a scanner from which we expect to receive a line, separated by two spaces and containing the 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 means transmitting the command entered via the scanner to the 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());
        }
    }

The selector function defines the program's actions based on the input string. It isn't particularly elegant and uses a less-than-ideal approach by forcing an exit outside of the code block, but the main reason for this is the lack of certain features in Java, such as delegates from C#, function pointers from C++, or even the dreaded goto, which would allow for a more graceful implementation. If you know how to make the code a bit more elegant, I welcome criticism in the comments. I think a String-delegate dictionary is needed here, but there isn't a 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 that of the client mainly in that commands come from a socket rather than from a keyboard. Some methods coincide completely, so I won't list them; I'll only touch on the differences.

To start, the run method is used, which takes a port as input and processes incoming data from the socket in an endless loop.

    public void run(int port) {
            this.port = port;
            initialization();
            while (true) {
                getAndParseInput();
                selector();
            }
    }

The put method, which is a wrapper for the writeToFileFromSocket method, opens a write stream to a file and writes all input bytes from the socket. After the write operation completes, it outputs a message indicating the successful completion of the transfer.

    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());
        }
    }

The get method facilitates the retrieval of a file from the server. As mentioned in the client-side section of the program, to successfully transfer the file, its size, stored as a long integer, must be known. Therefore, I break it down into an array of 4 bytes, transmit them byte by byte through the socket, and then, after receiving and reconstructing them back into a number on the client-side, I send all bytes that make up the file, read from the input stream.


 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 corresponds to the one in the client, with the only difference being that it reads data from the socket rather than from the keyboard. The code in the repository is the same as the selector.
In this case, the initialization has been moved to a separate code block, as in this implementation, resources are released after transmission and are reallocated again — once again to prevent memory leaks.

    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());
        }
    }

In summary:

We have just written our variation on a simple data transfer protocol and understood how it should operate. In principle, I haven’t discovered anything groundbreaking, nor have I written anything particularly new. However, there were no similar articles on Habr, and within the framework of writing a series of articles on cmd utilities, it was essential to address this topic.

Links:

Repository with the source code
A brief overview of TFTP
The same, but in Russian

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster