Enabling Multi-threaded Upload via ActiveX/Java Uploader in PHP

By the nature of HTTP, protocol web servers are designed to handle multiple simultaneous requests. While creating a file upload solution we can employ this feature to increase upload speed. The idea is quite simple: divide data into several pieces and upload them simultaneously, each data piece within a separate HTTP request. This trick can yield up to a 50% increase in upload speed if the channel between a client computer and a web server is fast enough. ActiveX/Java Uploader supports this approach, and this topic explains what changes are required to switch your application to multi-threaded upload.

Multi-threaded upload works only if the packages and/or chunks modes are turned on (to do this, the FilesPerPackage property and/or the ChunkSize property must have a positive value). In this case, the uploading files are processed in multiple pieces (by packages or chunks) rather than all at once. While uploading each file in a separate request (FilesPerPackage = 1) is the simplest way to start with multi-threaded upload, we recommend you to read the Sending All Files at Once or in Several Parts in ActiveX/Java Uploader topic. Also the Upload Modes Best Practices topic can help you to find the most appropriate configuration.

Even though ActiveX/Java Uploader supports up to 10 simultaneous threads, in real world use we have found that the optimal number of connections to use is 3-4. Uploading files in a larger number of simultaneous requests can stress your server too much which will result in the elimination of the desired positive effect.

The Saving Uploaded Files in ActiveX/Java Uploader PHP topic explains how to process uploaded files and outlines three approaches for handling files on the server side. Here we will examine each approach and suggest changes required to work in the multi-threaded mode:

Processing Uploaded Data Automatically

ActiveX/Java Uploader provides the UploadHandler class to process the uploaded data. This class allows the saving of uploaded data automatically to the specified folder. To enable the multi-threaded upload along with the autosave mode, you do not need to change your server code. Just set the MaxConnectionCount property to a positive value (specifying a number of simultaneous connections). Make sure that you enable multiple packages and/or chunk upload as we discussed above.

The snippet below configures an Uploader object to send files in up to 4 simultaneous threads and to place each file in a separate package:

PHP
<?php
    require_once "ImageUploaderPHP/Uploader.class.php";
    
    $uploader->getUploadSettings()->setActionUrl("upload.php");
    $uploader->getUploadSettings()->setMaxConnectionCount(4);
    $uploader->getUploadSettings()->setFilesPerPackage(1);
?>

In the previous snippet ActionUrl specifies the upload script (upload.php). Here is the code for upload.php, which saves received data to the /Catalog folder:

PHP
<?php
    require_once "ImageUploaderPHP/UploadHandler.class.php";

    $uploadHandler = new UploadHandler();
    $uploadHandler->saveFiles("Catalog/");
?>

Processing Uploaded Data Using UploadHandler Class

The UploadHandler class is a server-side component which gives you flexible access to all data uploaded from client computers (for more information see the Saving Uploaded Files in ActiveX/Java Uploader PHP topic). This component exposes the FileUploadedCallback and AllFilesUploadedCallback properties allowing you to set callback functions for handling uploaded data.

The AllFilesUploadedCallback function is called when the whole upload session is completed, i.e. while the upload process is in progress the server collects all data sent in single or multiple threads and then returns a collection of uploaded files and metadata to the function. Thus, if you use the AllFilesUploadedCallback property, you can enable multi-threaded upload by setting the MaxConnectionCount property to a positive value. Also, do not forget to enable multiple packages and/or chunk upload as we discussed at the beginning of this topic.

The FileUploadedCallback function is called each time a file is successfully uploaded. If you set up ActiveX/Java Uploader to transfer files in the multi-threaded mode, it can turn out that multiple requests arrive to the server and call the function at the same moment. So, if you use the FileUploadedCallback property and want to enable multi-threaded upload in your application, it is not enough just to change the value of the MaxConnectionCount property. You must review the code of the FileUploadedCallback function and rewrite thread-unsafe segments.

Here you can see several examples of thread-unsafe code:

  • changing a resource that is not supposed to be changed concurrently (e.g. if you write to a file)
  • using thread-unsafe collections
  • using thread-unsafe libraries

For example, if the FileUploadedCallback function writes data to a file, there can be multiple concurrent calls causing simultaneous file writing. To resolve this issue add synchronization to the file accessing code. The following snippet demonstrates how to do this:

PHP
<?php
function saveUploadedFile($uploadedFile) 
{
    $lock = fopen("lock.txt","a");

    //Lock resource.
    if(flock($lock, LOCK_EX)) 
    {
        $absGalleryPath = realpath("Gallery/");

        //Create or load XML file which will keep information about files.
        $descriptions = new DOMDocument('1.0');
        if (file_exists($absGalleryPath . DIRECTORY_SEPARATOR . "Descriptions.xml"))
            $descriptions->load($absGalleryPath . DIRECTORY_SEPARATOR . "Descriptions.xml");
        else
            $descriptions->appendChild($descriptions->createElement("files"));
    
        //Save file info.
        $xmlFile = $descriptions->createElement("file");
        $xmlFile->setAttribute("name", rawurlencode($uploadedFile->getSourceName()));
        $xmlFile->setAttribute("width", $uploadedFile->getSourceWidth());
        $xmlFile->setAttribute("height", $uploadedFile->getSourceHeight());
        $xmlFile->setAttribute("description", $uploadedFile->getDescription());
        $descriptions->documentElement->appendChild($xmlFile);
        $descriptions->save($absGalleryPath . DIRECTORY_SEPARATOR . "Descriptions.xml");

        flock($lock, LOCK_UN);
        fclose($lock);
    }
}
?>

Processing Uploaded Data Using POST Fields Data

This approach requires you to write code for parsing HTTP POST fields. All data sent by ActiveX/Java Uploader is stored in the $_POST predefined variable. To simplify accessing the POST request fields ActiveX/Java Uploader PHP classes provide PostFields (for more information see the Saving Uploaded Files in ActiveX/Java Uploader PHP and POST Field Reference topics).

Like in the previous approach, we should take steps to avoid simultaneous requests which can cause problems in case of thread-unsafe code. So, to enable multi-threaded upload for this approach, you should perform the following steps:

  • Specify the MaxConnectionCount property to define the number of simultaneous connections.
  • Make sure that you enable package upload as we discussed before. As this approach does not restore files from chunks automatically, we do not recommend using the chunk upload mode, as you would be required to write code for handling chunks.
  • Review the code for parsing HTTP POST fields and rewrite thread-unsafe segments. You can see examples of thread-unsafe code in the previous example.

The following snippet contains thread-safe code that saves information about uploaded files to an XML file:

PHP
<?php
    require_once "ImageUploaderPHP/PostFields.class.php";

    // Check if it is POST request;
    // we should not try to save files in HEAD requests.
    if ($_SERVER['REQUEST_METHOD'] != 'POST') 
        exit();
    
    $lock = fopen("lock.txt","a");

    //Lock resource.
    if(flock($lock, LOCK_EX)) 
    {
        $absGalleryPath = realpath("Gallery");

        //Create or load XML file which will keep information about files (image dimensions, description, etc).
        $descriptions = new DOMDocument('1.0');
        if (file_exists($absGalleryPath . DIRECTORY_SEPARATOR . "Descriptions.xml"))
            $descriptions->load($absGalleryPath . DIRECTORY_SEPARATOR . "Descriptions.xml");
        else
            $descriptions->appendChild($descriptions->createElement("files"));

        //Iterate through uploaded data and save the original file, thumbnail, and description.
        for ($i = 0; $i < $_POST[PostFields::packageFileCount]; $i++) 
        {
            //Save file info.
            $xmlFile = $descriptions->createElement("file");
            $xmlFile->setAttribute("name", rawurlencode($_POST[sprintf(PostFields::sourceName,$i)]));
            $xmlFile->setAttribute("width", $_POST[sprintf(PostFields::sourceWidth, $i)]);
            $xmlFile->setAttribute("height", $_POST[sprintf(PostFields::sourceHeight, $i)]);
            $xmlFile->setAttribute("description", $_POST[sprintf(PostFields::description, $i)]);
            $descriptions->documentElement->appendChild($xmlFile);
        }
        $descriptions->save($absGalleryPath . DIRECTORY_SEPARATOR . "Descriptions.xml");
    
        flock($lock, LOCK_UN);
        fclose($lock);
    }
?>

See Also

Reference

Manual