TFTP Client & Server
with Option Extension over UDP
Trivial File Transfer Protocol
for POSIX compatible platform: Solaris
Documentation Report Presented to
x
By :
x
x
December 2000
x
x
X
x
Table of Contents
Introduction
*Trivial file transfer protocol. 3
Sockets. 3
User Datagram Protocol. 3
Client/Server model. 4
TFTP Design 5
Program command line example. 8
TFTP Sliding window design. 9
TFTP Blocksize option 9
TFTP Timeout Interval 9
TFTP Option Negociation 10
Overview of the TFTP Protocol 11
Connection Phase
*1Data Transfer
*1Disconnection Phase
*2Structure and overall architecture of the class model
*Assumptions for the TFTP Project
*1TFTP program performance and limitation
*4CONCLUSION 26
Trivial File Transfer Protocol
Trivial File Transfer Protocol (TFTP) is a simple protocol for transferring files between systems. TFTP is implemented on top of User Datagram Protocol (UDP) and provides no security features and it lacks most of the features of a regular FTP. TFTP may be used to move files between machines on different networks implementing UDP. TFTP uses go-back-N protocol. In other words, data packets are transferred in one direction only and each data packet is acknowledged separately. TFTP can read and write files from/to a remote server. The only supported transfer mode for this project is octet. This means that our TFTP program should send or receive data in binary format without any interpretation. The data is sent or received as is, there is no translation of CR/LF ASCII characters or any other ASCII characters.
A socket is one end point of a two-way communication link between two programs connected to the Internet. Sockets are seen as communication interfaces on top of TCP or UDP. TCP is connection-oriented socket and UDP is connectionless datagram socket. A socket is bound to a port number that represents a communication end-point, so that the TCP or UDP layer can identify the application for data to be sent.
User Datagram Protocol (UDP) is a standard, low-overhead, connectionless, host-to-host protocol that is used over packet-switched computer communications networks, and that allows an application program on one computer to send a datagram to an application program on another computer. Connectionless protocol refers to network protocols in which a host can send a message without establishing a connection with the recipient. That is, the host simply puts the message onto the network with the destination address and hopes that it arrives. UDP is a simple protocol that exchanges datagrams without acknowledgments or guaranteed delivery, requiring that error processing and retransmission be handled by the other protocols. UDP provides very few error recovery services, offering instead a direct way to send and receive datagrams over an IP network. It is used primarily for broadcasting messages over a network or to distribute information that changes every few seconds.
Client/Server model
TFTP is using the client/server model, which consist of two distinct programs, usually running on different machines at different locations. The two different socket classes: UDP_SocketServer and UDP_SocketClient, are used to represent the connection between a client program and a server program. A server program contains the main method and performs the work of listening to the port, establishing connections, reading from and writing to the socket to answer requests from client programs. Given the server address and socket number, the client program can find the server and requests a connection from the server. Once a connection is been established, both the client and the server are ready to transfer information. Once the transfer is completed between both parties, the client and the server will disconnect from each other. However, multiple client requests can come into the server socket. Client connection requests can be accepted or rejected. The server can service many clients simultaneously through the use of threads, where one thread is used for each client connection.
TFTP Design
Our main objectives were to have a multi-thread environment with queues, mutex and read-write locks to protect shared data and queues integrity. We also decided to use real-time clock timers for accurate wake up time to verify if a timeout as occur or not.
In order, to have a fully working multi-thread environment, that is fast, efficient and reliable, where data integrity is guaranteed, we needed to use some protection mechanism that will insure that no global shared object will ever get corrupted. We used global shared object, so that we can split the task to be accomplished by many threads that are working together to get the job done. Those mechanisms are known as mutex and read-write locks. Mutex ensure that the only owner of the lock will be able to modify the data at a given time. Read-write locks ensure that only a single writer can modify the data or that many readers can read the data at a given time. In order, to achieved some degree of portability, we used the commonly known Posix Threads library, that we wrapped up to make it easy to used in a class named Basic_PThread. We did the same for Mutex and RWLock classes. Those classes permit us to forget about the Posix Thread library complexity and to make it as easy to use as it is in Java. Having those tools at hands, we divided the program in many sub tasks, where each sub task is a class, which inherit from Basic_PThread. We can therefore say that we used mainly the bottom-up approach, mainly by first designing the required tools needed by the program and then use those fully debugged and working classes to build up more complex classes.
For instance, we wrote down a brief list of needed classes for the program and decided to start designing and coding those classes, since everything else used those classes by after. For example, we cannot design multi-thread TFTP classes without having threads, mutex, RW locks and UDP sockets working.
It was pretty easy by after to create as many sub task as needed as the following list illustrates:
Program command line examples:
Run server on port 2000
./tftp445 -server 2000
Run a client to write a file call test.txt on the server DEA,
port 2000 with a blocksize of 600 bytes and a windowsize of 3.
./tftp445 --blocksize=600 --windowsize=3 -client dea.ece.concordia.ca:2000
-put test.txt
TFTP Sliding Window Design
TFTP Block Size Option
TFTP Timeout Interval
TFTP Option Negotiation
Overview of the TFTP Protocol
OACK Acknowledge the Read Request and the options.
DATA 1 Acknowledge the Read Request but not the options.
ERROR The read request has been denied.
OACK Acknowledge the Write Request and the options
ACK 0 Acknowledge the Write Request but not the options
ERROR The write request has been denied
or by an acknowledgement packet for a data packet with a bigger block number.
Structure and overall architecture of the class model
The class
The class RWLock
The class FileFactory will take care of writing the raw packets into the file, by parsing the data and the length and store it into the file at the appropriate location by calling the appropriate JFile functions. We currently use directWrite() instead of our first design which was to enqueue packet to be written into data_queue and then let the FileFactory thread take care of writing the packets to the file once it get some CPU cycles. The problem was that the FileFactory didn’t get enough CPU cycles and it was therefore simpler to just write the data packet to the file as soon as we receive them.
The class DList_pPacket is simply a double linked list with queue features where each element stored is a pointer to a TFTP_Packet object.
The class DataFactory will take care of reading the file and produce data packets from the content stored inside the file. After that, it will take the data packet and enqueue it at the end of the double linked list named data_queue. This class use JFile and TFTP_Option to read from the file.
The class TFTP_Options will take care of every option and mostly all global variables that we are using inside this program. Most of those variables are initialized by default and reinitialized later on as needed through the command line, the shell, a client request or a server acknowledgement. Few RWLock objects protect all the data stored inside this class. Each of them is assigned an ID number, so that any cascaded request to a RWLock must be in ascending order to avoid any deadlocks. For instance, you can lock 1 and 3, but you cannot ask for 2 before unlocking 3, else a deadlock will probably occur.
This way all the class users can simultaneously access the data as needed in parallel, but only one of them can write to it, but many of them can read it, at a given time. As seen in the demo, we often print the current program status by calling showOptions() to display the following global data and their length for debugging purposes:
the last data packet )
we don’t exceed the current window size maximum )
Each of those data follows the RFC documentation, the range are respected and for the access mode any value other that octet is considered as non implemented and result in the termination of the program with an error code 2.
To make the implementation of the class TFTP_Packet easier, we added two special functions in TFTP_Option which are to build the option extension template using given defined parameters and to build the read or write request template without the option extension. This way, the TFTP_Packet simply have to take care of initializing packets with the default-established templates that we decided to use. The first operation is obviously to create those templates.
The class PThread and Basic_PThread are wrappers for the Posix Thread set of functions part of the POSIX Thread library. PThread is more evolved than Basic_PThread, since it contains the setting for scheduling and other pthread advanced features. This class is wonderfully design and very easy to used. It follows the Java idea, which is order to get a working PThread, you simply have to inherit from that class, implement the function virtual void run() and start() it to make the pthread running. The function start() will take care of initializing the pthread and create it with default scheduling parameters, it will pass a static function wrapper, since a callback cannot be done on virtual functions and a pointer to the class, since a static function cannot access members of a class. The wrapper takes a void* argument which will be cast back to PThread*, so that we can call the run function from it, as simple as that. If you ever programmed threads in Java, you will get the same easy to use API to make your life simple. Inherit from PThread, implement run() and start() it !
The class Timer uses signals and timer_create() to implement a real-time clock timer, it uses SIGUSR1 that will be catch by a timer handler. Since that timer_create() cannot pass a void* parameter and that only signal SIGUSR1 and SIGUSR2 can be used, we are forced to have a maximum of two timer for the entire program, unless we fork a timer for each packet, but that could slow down everything. This class use the same trick as in PThread, which is to call a static function that will call a virtual, unimplemented one that shall be implemented by the children class. The Timer class constructor takes as a parameter the number of nanoseconds and seconds which is the clock tick length of the timer square wave. The clock low duration and the clock high duration are set as equal, to get a real square wave at the specified frequency.
The class JFile can handle any kind of files, text or binary. The main role is to read data from it or to write to data into it. The class knows itself (the FILE* pointer), it’s size and it’s current position. The filename included in the path, access mode and the record size are given as parameters to the constructor. The JFile is able to parse the given path to retrieve the short filename, so that the TFTP_Option user can ask JFile what is the short filename, so RRQ and WRQ parameters can be established. It can also ask the file size for the option extension, if needed. Therefore, the class is able to append data or to read data as a stream, since it updates the pointer as it read or write into the file by incrementing the pointer by the specified block size stored internally.
JErrorNo is an enumeration of supported errors that are used in C programming. It follows the errno definition as declared in <sys/errno.h>. Since we used a lot of C functions inside this TFTP program, it was much more convenient, to use the returned errno value and simply match that value with a set of predefined error handlers. The error code returned by the function is converted to a JErrorNo type and passed to the JErrorController. Since different platforms have different error number, we standardized it, so that we can have a uniform array of function pointer. It converts incompatible error number to the one we defined as standard and then pass it to the call() function, which will do a look up in the array using the JErrorNo as an index. Each element of the array are a pointer to the head of a single linked list of JErrorHandler* elements.
The class JErrorController manages and formats the error received from the TFTP program. The JErrorController will find in the array the error received from the TFTP program and deliver the error to the error handler. The main task of the JErrorController is to react accordingly to that specific error by using the JErrorHandler for that error number. The call() function inside the JErrorController class will simply call the JErrorHandler* head, which will call a static function which will deal with the error. The default error handler is simply a text display, specifying the origin of the error and a short description of the possible reason to explain why this error happened. The line number and source code file name are displayed and also the theoretical JErrorLevel, which is a value out of five, which tell us how important this error is and if the program can be resumed, aborted or if the error can be solved or not.
The class JErrorHandler handles errors and try to resolve the error according to the description of the error and the error number. If the error can be recovered then JErrorHandler will resolve the current error. Otherwise, JErrorHandler will display by default the following error message on the standard error output ( stderr ):
***********************************************
An error of type #X of level Y/5 has occurred
at line L of file XYZ.cpp,
Here's a brief description of the error:
[………]
***********************************************
By default the JErrorHandler will point on the default error handler. New error handlers can be added to perform specific error handling management, i.e. different from the defaultErrorHandler() function, simply by adding a new JErrorHandler that points to the new error handler static function and then add this handler on top of the stack for that specific JErrorNo number. JErrorHandler is simply a container, so that we can do a link list of it. The JErrorHandler is used, for example, to inform us that the JFile couldn't be opened properly or that a Mutex, RWLock or PThread operation was not successful.
The class TFTP_Timeout scan the double link-list waitingAck_queue from the head and search for an expired timestamp stored inside a TFTP_Packet, if this is the case then a timeout occurs and the packet will be resent if less than three retries were attempts. We detect a timeout by storing the current time added to the timeout value to get a timestamp value. When a clock tick occurs, we update the current time and look for any timestamp lower then or equal to the current time. If such a thing happen, we resent the packet, update the timestamp and enqueue it back into the waitingAck_queue, just in case another timeout occurs.
The class TFTP_DataAck will acknowledge each packet in the queue during the TFTP transfer and will be called every time a valid ACK packet is received with a block number greater than zero.
The file TFTP_header is design to define all states, enumerations and C type structures used in the TFTP program, such as:
// List of all OpCode accepted by TFTP.
enum OpCode_t
{
RRQ = 1,
WRQ = 2,
DATA = 3,
ACK = 4,
ERROR = 5,
OACK = 6
};
const char* TFTP_errmsg[ 8 ] = {
/* 0 */ "Not defined.",
/* 1 */ "File not found.",
/* 2 */ "Access violation.",
/* 3 */ "Disk full or allocation exceeded.",
/* 4 */ "Illegal TFTP operation.",
/* 5 */ "Unknown transfer ID.",
/* 6 */ "File already exist.",
/* 7 */ "No such user.",
};
enum TFTP_errcode_t
{
TFTP_NotDefined = 0,
TFTP_FileNotFound = 1,
TFTP_AccessViolation = 2,
TFTP_DiskFull = 3,
TFTP_IllegalOp = 4,
TFTP_FileAlreadyExist = 5,
TFTP_NoSuchUser = 6 // only use for mail (Not supported)
};
/**************************************************************************
**************************************************************************
*** bit 0 Init
*** bit 1 Connected
*** bit 2 Ack for RRQ/WRQ
*** bit 3 Reading
*** bit 4 Writing
*** bit 5 End of Read/Write
*** bit 6 Error
*** bit 7 Finished, program exit signal
**************************************************************************
*** Disconnect means that we are not connected and that
*** all the variables in TFTP_Option are still initialized.
**************************************************************************
**************************************************************************/
enum TFTP_Status_t
{
TFTP_Uninitialized = 0x00, // %0000,0000
TFTP_Disconnected = 0x01, // %0000,0001
TFTP_Connected = 0x03, // %0000,0011
TFTP_ReadingNotAck = 0x0B, // %0000,1011
TFTP_WritingNotAck = 0x13, // %0001,0011
TFTP_Reading = 0x0F, // %0000,1111
TFTP_Writing = 0x17, // %0001,0111
TFTP_EndofReadNotAck = 0x2B, // %0010,1011
TFTP_EndofWriteNotAck = 0x33, // %0011,0011
TFTP_EndofRead = 0x2F, // %0010,1111
TFTP_EndofWrite = 0x37, // %0011,0111
TFTP_Error = 0x43, // %0100,0011
TFTP_Finished = 0x81 // %1000,0001
};
/**************************************************************************
**************************************************************************
*** bit 0 Server
*** bit 1 Client
*** bit 2 Reading Command Line request
*** bit 3 Writing Command Line request
**************************************************************************
**************************************************************************/
enum TFTP_Type_t
{
TFTP_Type_Undefined = 0x00, // %0000
TFTP_Server = 0x01, // %0001
TFTP_ServerRead = 0x05, // %0101
TFTP_ServerWrite = 0x09, // %1001
TFTP_Client = 0x02, // %0010
TFTP_ClientRead = 0x06, // %0110
TFTP_ClientWrite = 0x0A, // %1010
};
The class TFTP_Sender sends data packet created by the Data_Factory until the queue is empty or that the windowsizeLeft reached zero.
The functions in TFTP_Error send a TFTP error packet for the assigned errno values. The following error functions are defined:
The last error should not occur because we are not using Mail option.
Assumptions for the TFTP Project
and the program is aborted.
such as Solaris.
with the inclusion of the -lmalloc library in the Makefile.
packet, if connected, and display a meaningful message before exiting as long as the
error is covered by the Unix errno values, if not the program execution
is undetermined.
e.g. Bus error, segmentation fault are not covered by JErrorController.
TFTP program performance and limitation
Experiments:
Conclusion
The project was a great learning experience for us. We encountered many problems during this project. Segmentation Faults were a naturally incurring error. This off course made the use of gdb essential for solving those errors. The decision to use threads and mutex proved to be wise decision since scheduling was left to the operating system. The use of Mutex caused some problems in the beginning but only because we had not included the proper libraries.
The design of project went through many changes throughout the course of the semester since we used a bottom up approach to the project. But that proved to be a good idea since most of our time was spent on making the low-level code work without flaws.
Working in a team caused some friction at some times but was dealt with like any team project. But the TFTP project proved to be an overall success at the end.