Writing software with the functionality of Windows client-server utilities, part 02

Continuing the started series of articles on custom implementations of Windows console utilities, one cannot but touch on TFTP (Trivial File Transfer Protocol) - a simple file transfer protocol.

Like last time, let's briefly go over the theory, see the code that implements the functionality similar to the required one, and analyze it. More details - under the cut

I will not copy-paste reference information, links to which can traditionally be found at the end of the article, I will only 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 but commands for receiving and transferring a file . However, in order to make our implementation a little more elegant and adapted to the current principles of writing code, the syntax has been slightly changed - this does not change the principles of operation, but the interface, IMHO, becomes a little more logical and combines the positive aspects of FTP and TFTP.

In particular, at startup, the client asks for the ip address of the server and the port on which custom TFTP is open (due to incompatibility with the standard protocol, I considered it appropriate to leave the user to choose the port), after which a connection occurs, as a result of which the client can send one of the commands - get or put to get or send a file to the server. All files are sent in binary mode - in order to simplify the logic.

To implement the protocol, I traditionally used 4 classes:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Due to the fact that the 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 analyze the main classes.

TFTPClient

The task of this class is to connect to a 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 a file, transfer it or get.

The code for launching the client to connect to the server and wait for a command from the input stream looks like this. A number of global variables that are used here are described outside of the article, in the full text of the program. Due to their triviality, I do not quote, so as 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 - with the help of the scanner, we present the contents of the file as an array of bytes, which we write to the socket one by one, after which we close it and reopen it (not the most obvious solution, but it guarantees the release of resources), after which we display a message on 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("nDonen");
            }

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

This code snippet describes getting data from the server. Everything is again trivial, only the first block of code is of interest. In order to understand exactly how many bytes to read from the socket, you need to know how much the transferred file weighs. The file size on the server is represented as a long integer, so 4 bytes are accepted here, which are later converted into a single number. This is not a very Java approach, it is rather similar for SI, but it solves its task.

Further, everything is trivial - we receive a 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("nDONEn");
       }
       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 incorrect input. Due to triviality - I do not cite. Somewhat more interesting is the function of getting and splitting the input string. We pass a scanner into it, from which we expect to receive a string 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 - transferring the command entered from 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());
        }
    }

A selector is a function that determines the program's actions depending on the input string. Everything is not very beautiful here and not the best trick is used with forced exit outside the code block, but the main reason for this is the lack of certain things in Java, like delegates in C #, function pointers from C ++, or at least the terrible and terrible goto, which allow you to implement 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 commands come to it not from the keyboard, but from the socket. Some of the methods generally coincide, so I will not give them, I will only touch on the differences.

To start, the run method is used here, which receives a port as an input and processes the 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 for the writeToFileFromSocket method that opens a stream to write to a file and writes all bytes of input from the socket, prints a success message after the write is complete.

    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 retrieves the server file. As mentioned in the section on the client side of the program, in order to successfully transfer the file, you need to know its size stored in a long integer, so I split it into an array of 4 bytes, transfer them byte by byte to the socket, and then, having received and collected them on the client to a number back, I pass 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 that it reads data from the socket, not from the keyboard. The code is in the repository, just like selector.
In this case, the initialization is moved to a separate code block, because within the framework of this implementation, after the end of the transfer, the resources are released and re-engaged again - again in order to provide 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 a simple data transfer protocol and figured out how it should work. In principle, I didn’t discover America here and didn’t write much new things, 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 it.

Links:

Source code repository
Briefly about TFTP
Same thing, but in Russian

Source: habr.com

Add a comment