Iphone download di un file..

Una Servlet per permettere il download con header HTTPRange (Necessario per IPhone!)
G.Morreale

Introduzione:

Implementando un sistema di download per file 3gp da scaricare su dispositivi mobili, si può notare come alcuni dispositivi, ad esempio l'Iphone, effettuino una sorta di richiesta dello stream avvalendosi dell'http header range.

Il Problema:

In sostanza a differenza del caso classico in cui il file viene fornito come allegato avvelendosi di header del tipo:

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

con l'header range, l'iphone, o comunque altri client richiedono il contenuto per porzioni, richiedendo solo intervalli di bytes.

Se si vuole approfondire circa le specifiche dell'header range inviato dal client basta collegarsi su w3c.org (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) sezione 14.35

Il client ricevendo l'header content-lenght ha idea di quanti bytes richiedere, e a seconda delle necessità ne richiede i diversi sottoinsiemi finchè non compone l'intero contenuto.

es.

richiesta dei bytes che vanno dalla posizione 500 alla posizione 999
range: bytes=500-900

La Soluzione:

Il server deve effettuare il parsing dell'header range e restituire solo la porzione di dati richiesti.
Leggendo le specifiche dell'header range però si può notare come ad ogni richiesta potrebbero essere reclamati diversi range.

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

Quindi il parsing deve essere in grado di prevedere tale specifica.

Un Pò di Codice:

Come primo step si potrebbe costruire una classe in grado di rappresentare un 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

     /**
     * 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;
    }

La parte finale della servlet consiste nella restituzione dei bytes richiesti:

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);        
        }

    }
Il codice,credo, sia abbastanza semplice da capire, unica nota da fare riguarda il codice STATUS Http restituito, infatti quando si restituiscono  porzioni di bytes bisogna rispondere con codice "206" ad indicare che il contenuto è stato restituito ancora solo in parte.

Altri header http correlati:

In relazione alla problematica del "download in parti" e al relativo header "range" ci sono altri header che possono tornare utili nella gestione di queste problematiche:

  • Accept-Ranges: E' un header inviato dal server in grado di comunicare al client la tipologia di range in grado di gestire
  • Content-Range:Viene inviato dal server ad indicare il range restituito in proporzione al numero totale di bytes da restituire (usato nel codice della servlet!)
  • If-Range:Serve per la gestione della cache del client nel caso in cui alcune porzioni sono state già messe nella cache.


Iphone nello specifico.

Nel caso particolare di questo dispositivo, la richiesta del download di un 3gp avviene in due fasi:
Nella prima il dispositivo fà una normale richiesta proponendo come userAgent quello classi del browser safari:

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

Successivamente, una volta che il browser capisce che si tratta di un video 3gp da scaricare in streaming, subentra il client del quicktime che effettua le richieste con l'header range.
In quest'ultimo caso lo userAgent della richiesta è:

Apple iPhone OS v2.1 CoreMedia v1.0.0.5F136


Conclusione:

Tale articolo non copre tutti i risvolti dell'header range, in quanto ad esempio, manca il supporto per i range finali (Es. range:bytes=-500)
E' comunque un punto di partenza per capire e implementare il download per parti!

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.
Tale servlet soddisfa le specifiche http.


No comments: