TFTP server implementation guidelines
The file TFTPServer.java is a main program for a TFTP server developed here at DFM. You should consider it as a guide-line of what the final program might look like and above all; do not try to use it in its entirety from the beginning. A complex program like this should be developed step by step; the main program TFTPServer.java should only be regarded as a “master plan”, showing the various steps to be considered during the implementation.
In what follows we give a suggested, step by step, working plan to get you started. The main objective from the beginning is to get a program that runs. A suitable starting point is to handle a single read request without any concurrency. Once we have that, we can add various features step by step, always having control over the situation. Do not start with the next stage before you have thoroughly tested the previous one.
1. Download the two files (TFTPServer.java and the RFC) and read them. Try to get an overall picture of the work to come.
2. This involves the following steps:
- Get listening on the predefined port.
- Parse a read request: Once receiveFrom() has received a message we must parse it in order to get the information (type of request, requested file, transfer mode). The first 2 bytes of the message contains the opcode indicating type of request. The following approach reads two bytes at a given address and converts it to an unsigned short:
import java.nio.ByteBuffer; byte[] buf; ByteBuffer wrap= ByteBuffer.wrap(buf); short opcode= wrap.getShort();
We can now parse the request message for opcode and requested file as:
short opcode= wrap.getShort(); fileName= new String(buf, 2, readBytes-2);
Where readBytes is the number of bytes read into the byte array buf. We leave you with the problem of parsing the message for the transfer mode.
- Once the parsing is done we can test our program by sending a read request from the client and print out the opcode (should be 1), requested file and the transfer mode (should be octet).
- Open the requested file: Before you can open the file you must add the path (parameter READDIR, the directory where your server keeps files that are available for reading) to the received filename.
- Build a response: Add opcode for data (OP_DATA) and block number (1), each an unsigned short of 2 bytes in network byte order. We suggest a similar approach as for parsing the buffer
byte[] buf; short shortVal= OP_DATA; ByteBuffer wrap= ByteBuffer.wrap(buf); short opcode= wrap.putShort(shortVal);
that “reverses” the work done by the previous. Read a maximum of 512 bytes from the open file, add it to the response buffer and send it to the client. If everything works, the client will respond with an acknowledgment of your first package. Receive it and parse it. It is now time for a crucial test. Make a read request from the client (request to read a file that is shorter than 512 bytes) and check that everything works properly.
- More than one block: Add a loop that makes it possible to handle files larger than 512 bytes
3. Timeout and retransmission: In case of a read request this means that we should use a timer when sending a package. If no acknowledgment has arrived before the time expire, we retransmit the previous packet and start the timer once again. If an acknowledgment of the wrong packet arrives (check the block number), we also retransmit. Make sure that there exists an upper limit on the maximum number of retransmissions. In our program we did all this in a single function WriteAndReadAck() that did not finish before a packet N was sent and acknowledged or the maximum number of retransmissions was reached. This method is not visible in the TFTPServer.java, however.
4. More than one request: Add a loop that returns to receiveFrom() after each request has been handled.
5. Concurrency: Study the program TFTPServer.java to see how we used java.lang.Thread to handle the concurrency. Until now we have used a single socket, concurrency forces us to work with more than one socket. One that listens for new requests and one in each child that handles the requests.
6. Write requests. Once read requests work properly, implement the part that handles write requests.
7. Error handling: RFC1350 specifies a particular type of packets used to transport error messages as well as several error codes and err messages. For example, an error message should be sent if the client wants to read a file that doesn’t exist (errcode = 1, errmsg = File not found), or if the client wants to write to a file that already exists (errcode = 6, errmsg = File already exist). More generally, an error message should be sent every time the server wants to exit a connection. Remember also to check all packets that arrive to see if it is an error message. If that is the case, the client is dead and the server should exit the connection.
Additional clarification on error codes
“Access violation” or “No such user” errors are related to UNIX file permission / ownership model. It is OK if your implementation returns one of these codes on generic IOException (after checking the cases for codes 1 and 6).
“Illegal TFTP operation” error is related to client trying to send something other than valid RRQ/WRQ request code.
As for the “Invalid Transfer ID”: your implementation doesn’t have to explicitly send that data structure, but you still have access to (remote port, local port) values for each packet. After connection initialization, server and client are communicating via arbitrary ports X and Y (in “ephemeral” port range). Therefore, you can imagine a situation when “ACK” message arrives from a client, but from a port Z that was not previously used in that particular communication session. In such a situation, server should send an error response with error code 5.