Iphone File Download (ENG ver)..

Download Servlet implementing http range header
(Needed for IPhone)
G.Morreale

Introduction:

By implementing a system to download 3gp file to be downloaded on mobile devices, you can see how some devices, such as Iphone, making an http request using range header.

The Problem:

In essence, unlike the classic case where the file is provided as an attachment header like this:

  • Content-Disposition:attachment;filename=VIDEO
  • Content-Transfer-Encoding:binary

with the range header, the iphone, or other clients require the content portions, requiring only bytes ranges.

If you want to learn about the specific dell'header range sent by the client simply log on w3c.org (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) Section 14:35

The client receiving the content-length header knows how many bytes have to requests, then it requests the various range until different subsets made the entire contents.

ex.

request of bytes ranging from position 500 to 999
range: bytes=500-900

The Solution:

The server must parse range header and only return the portion of the requested data.
Reading the range header specifications you can see that any request may be claimed different range.

ex.
range:bytes=500-900,999-1500

So the parsing must be able to provide this specification.

The Source Code:

As a first step we could build a class that can represent a range:

class ByteRange
{
    long start;
    long end;

    public long getEnd()
    {
        return end;
    }

    public void setEnd(long end)
    {
        this.end = end;
    }

    public long getStart()
    {
        return start;
    }

    public void setStart(long start)
    {
        this.start = start;
    }

    public ByteRange(long start, long end)
    {
        this.start = start;
        this.end = end;
    }

}


e successivamente un metodo, da inserire nella servlet di download, in grado di effettuare il parsing dell'header and a method, to be included in the download servlet, capable of parsing dell'header

     /**
     * Effettua il parsing dell'header http range
     * @param rangeHeader - contenuto dell'header range
     * @param dataLen - dimensione dell'array di byte
     * @return arraylist di oggetti ByteRange rappresentanti i vari range
     */   
    private ArrayList<ByteRange> parseRange(String rangeHeader, int dataLen)
    {
        ArrayList<ByteRange> ranges = null;
        //verifica correttezza dell'header
        if (rangeHeader != null && rangeHeader.startsWith("bytes"))
        {            
            ranges = new ArrayList<ByteRange>(8);
            //split dei diversi range separti da ,
            String[] rangesComma = rangeHeader.split(",");
            //per ogni range si determina la posizione di partenza e quella finale
            for (String r : rangesComma)
            {
                r = r.substring(6);
                int dashPos = r.indexOf('-');
                long end = dataLen - 1;
                long start = Long.parseLong(r.substring(0, dashPos));
                if (dashPos < r.length() - 1)
                {
                    end = Long.parseLong(r.substring(dashPos + 1, r.length()));
                }

                ranges.add(new ByteRange(start, end));
            }
        }
        return ranges;
    }

The final part of the servlet returning requested bytes:

protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException
    {
//-----------DEBUG------------------------
//        Enumeration en = request.getHeaderNames();
//        while (en.hasMoreElements())
//        {
//            String elem = (String) en.nextElement();
//            System.out.println(elem + " " + request.getHeader(elem));
//        }
//-----------FINE DEBUG------------------------

        byte[] data = (byte[]) request.getAttribute("data");
        if (data == null)
        {
            response.sendError(404);
            return;
        }
        response.setContentLength(data.length);

        //data not found
        if (data == null)
        {
            response.setStatus(response.SC_NOT_FOUND);
            return;
        }

        ServletOutputStream sos = response.getOutputStream();

        //extract range header
        String rangeHeader = request.getHeader("range");
        //parse multiple range bytes
        ArrayList<ByteRange> ranges = parseRange(rangeHeader, data.length);
        if (ranges != null)
        {
            long start = -1;
            long end = -1;

            if (ranges.size() == 1)
            {
                ByteRange range = ranges.get(0);
                start = range.getStart();
                end = range.getEnd();
                response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + data.length);
                response.setStatus(response.SC_PARTIAL_CONTENT);
                for (long j = start; j <= end; j++)
                {
                    sos.write(data[(int) j]);
                }
            }
            else
            {
                response.setStatus(response.SC_NOT_IMPLEMENTED);            
            }
        }
        else
        {            
            sos.write(data);        
        }

    }
The code, I think, is quite simple to understand, only thing to note is http STATUS returned. When you return portions of bytes must respond with code "206" to indicate that the content has been returned yet only in part.

Other related http headers:

In relation to the issue of the "download parts" and the related header range header there are others that may be useful in dealing with these issues:

  • Accept-Ranges: header sent by a server can communicate to the client type range can handle
  • Content-Range:It is sent from the server to indicate the range returned in proportion to the total number of bytes to be returned (used in servlet code!)
  • If-Range:Need to manage the cache of the client when certain portions were already in the cache.


Iphone Specifically.

In the special case of this device, the application of a 3gp download occurs in two stages:
Nella prima il dispositivo fà una normale richiesta proponendo come userAgent quello classi del browser safari: In the first the device makes a normal request proposing that as UserAgent classes safari browser:

Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_2 like Mac OS X; it-it) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5G77 Safari/525.20

Later, once the browser understands that this is a downloadable video 3gp streaming, the client takes over the quicktime that making requests with the range header.
In quest'ultimo caso lo userAgent della richiesta è: In the latter case, the UserAgent of the request is:

Apple iPhone OS v2.1 CoreMedia v1.0.0.5F136


Conclusion:

This article does not cover all aspects range header, as for example, lack of support for end-range (eg range: bytes =- 500)
E' comunque un punto di partenza per capire e implementare il download per parti! It 'still a starting point to understand and implement the download to go!

Per approfondire è possibile dare un occhio alla default servlet di glassfish, ovvero quella servlet che si occupa di servire contenuti statici presenti nelle directory del server. For more you can give a look at the default servlet Glassfish, namely servlet that deals with serving static content in the directory server.
Tale servlet soddisfa le specifiche http. This meets the specific servlet http.







No comments: