¿Cómo funciona la carga de archivos HTTP?

528

Cuando envío un formulario simple como este con un archivo adjunto:

<form enctype="multipart/form-data" action="http://localhost:3000/upload?upload_progress_id=12344" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
Choose a file to upload: <input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>

¿Cómo se envía el archivo internamente? ¿El archivo se envía como parte del cuerpo HTTP como datos? En los encabezados de esta solicitud, no veo nada relacionado con el nombre del archivo.

Solo me gustaría saber el funcionamiento interno de HTTP al enviar un archivo.

0xSina
fuente
No he usado un sniffer en mucho tiempo, pero si desea ver lo que se envía en su solicitud (ya que es para el servidor, es una solicitud) huela. Esta pregunta es demasiado amplia. SO es más para preguntas de programación específicas.
paparazzo
... como olfatea, el violinista es mi arma preferida. Incluso puede crear sus propias solicitudes de prueba para ver cómo se publican.
Phil Cooper el
Para aquellos interesados, también vea " MAX_FILE_SIZEen PHP - cuál es el punto" en stackoverflow.com/q/1381364/632951
Pacerier
Encuentro MAX_FILE_SIZE raro. ya que puedo modificar mi html en Chrome a 100000000 antes de publicarlo para que publique un mejor valor. Ya sea 1. tenerlo en una cookie con un hash seguro a través de la sal, de modo que si se modifica la cookie, el servidor puede validar y lanzar una excepción (como las piezas web o playframework lo hacen) o algún tipo de validación de formulario que las cosas no han cambiado. @ 0xSina
Dean Hiller

Respuestas:

320

Echemos un vistazo a lo que sucede cuando selecciona un archivo y envía su formulario (he truncado los encabezados por brevedad):

POST /upload?upload_progress_id=12344 HTTP/1.1
Host: localhost:3000
Content-Length: 1325
Origin: http://localhost:3000
... other headers ...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryePkpFF7tjBAqx29L

------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="uploadedfile"; filename="hello.o"
Content-Type: application/x-object

... contents of file goes here ...
------WebKitFormBoundaryePkpFF7tjBAqx29L--

NOTA: cada cadena de límite debe tener como prefijo un extra -- , al igual que al final de la última cadena de límite. El ejemplo anterior ya incluye esto, pero puede ser fácil pasarlo por alto. Ver comentario de @Andreas a continuación.

En lugar de codificar la URL de los parámetros del formulario, los parámetros del formulario (incluidos los datos del archivo) se envían como secciones en un documento de varias partes en el cuerpo de la solicitud.

En el ejemplo anterior, puede ver la entrada MAX_FILE_SIZEcon el valor establecido en el formulario, así como una sección que contiene los datos del archivo. El nombre del archivo es parte del Content-Dispositionencabezado.

Los detalles completos están aquí .

toddsundsted
fuente
77
@ source.rar: No. Los servidores web están (¿casi?) siempre enhebrados para que puedan manejar conexiones concurrentes. Esencialmente, el proceso del demonio que está escuchando en el puerto 80 inmediatamente deja de lado la tarea de servir a otro hilo / proceso para que pueda volver a escuchar otra conexión; incluso si dos conexiones entrantes llegan exactamente en el mismo momento, simplemente se sentarán en el búfer de red hasta que el demonio esté listo para leerlas.
eggyal
10
La explicación del subproceso es un poco incorrecta, ya que hay servidores de alto rendimiento que están diseñados como un solo subproceso y utilizan una máquina de estado para turnarse rápidamente para descargar paquetes de datos de las conexiones. Más bien, en TCP / IP, el puerto 80 es un puerto de escucha, no el puerto en el que se transfieren los datos.
slebetman
99
Cuando un socket de escucha IP (puerto 80) recibe una conexión, se crea otro socket en otro puerto, generalmente con un número aleatorio superior a 1000. Este socket se conecta al socket remoto dejando el puerto 80 libre para escuchar nuevas conexiones.
slebetman
11
@slebetman En primer lugar, se trata de HTTP. El modo activo FTP no se aplica aquí. En segundo lugar, el socket de escucha no se bloquea en cada conexión. Puede tener tantas conexiones a un puerto, ya que los otros lados tienen puertos para unir sus propios extremos.
Slotos
33
Tenga en cuenta que la cadena de límites que se pasa como parte del campo de encabezado Content-Type es 2 caracteres más corta que las cadenas de límites para las partes individuales a continuación. Acabo de pasar una hora tratando de averiguar por qué mi cargador no funciona porque es bastante difícil notar que en realidad solo hay 4 guiones en la primera cadena de límite pero 6 guiones en las otras cadenas de límite. En otras palabras: cuando se usa la cadena de límite para separar los datos del formulario individual, tiene que ir precedida de dos guiones: - Se describe en RFC1867, por supuesto, pero creo que también debería señalarse aquí
Andreas
279

¿Cómo se envía el archivo internamente?

Se llama al formato multipart/form-data, como se pregunta en: ¿Qué significa enctype = 'multipart / form-data'?

Voy a:

  • agregue algunas referencias HTML5 más
  • explicar por qué tiene razón con un formulario enviar ejemplo

Referencias HTML5

Hay tres posibilidades para enctype:

¿Cómo generar los ejemplos?

Una vez que vea un ejemplo de cada método, se hace evidente cómo funcionan y cuándo debe usar cada uno.

Puede producir ejemplos usando:

Guarde el formulario en un .htmlarchivo mínimo :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>upload</title>
</head>
<body>
  <form action="http://localhost:8000" method="post" enctype="multipart/form-data">
  <p><input type="text" name="text1" value="text default">
  <p><input type="text" name="text2" value="a&#x03C9;b">
  <p><input type="file" name="file1">
  <p><input type="file" name="file2">
  <p><input type="file" name="file3">
  <p><button type="submit">Submit</button>
</form>
</body>
</html>

Establecemos el valor de texto predeterminado en a&#x03C9;b, lo que significa aωbporque ωes U+03C9, que son los bytes 61 CF 89 62en UTF-8.

Crear archivos para cargar:

echo 'Content of a.txt.' > a.txt

echo '<!DOCTYPE html><title>Content of a.html.</title>' > a.html

# Binary file containing 4 bytes: 'a', 1, 2 and 'b'.
printf 'a\xCF\x89b' > binary

Ejecute nuestro pequeño servidor echo:

while true; do printf '' | nc -l 8000 localhost; done

Abra el HTML en su navegador, seleccione los archivos y haga clic en enviar y verifique el terminal.

nc imprime la solicitud recibida.

Probado en: Ubuntu 14.04.3, ncBSD 1.105, Firefox 40.

multipart / form-data

Firefox envió:

POST / HTTP/1.1
[[ Less interesting headers ... ]]
Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150
Content-Length: 834

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="text1"

text default
-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="text2"

aωb
-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file1"; filename="a.txt"
Content-Type: text/plain

Content of a.txt.

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html

<!DOCTYPE html><title>Content of a.html.</title>

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file3"; filename="binary"
Content-Type: application/octet-stream

aωb
-----------------------------735323031399963166993862150--

Para el archivo binario y el campo de texto, los bytes 61 CF 89 62( aωben UTF-8) se envían literalmente. Puede verificar eso con nc -l localhost 8000 | hd, que dice que los bytes:

61 CF 89 62

fueron enviados ( 61== 'a' y 62== 'b').

Por lo tanto, está claro que:

  • Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150establece el tipo de contenido multipart/form-datay dice que los campos están separados por el dadoboundary cadena .

    Pero tenga en cuenta que el:

    boundary=---------------------------735323031399963166993862150
    

    tiene dos papas menos --que la barrera real

    -----------------------------735323031399963166993862150
    

    Esto se debe a que el estándar requiere que el límite comience con dos guiones --. Los otros guiones parecen ser exactamente cómo Firefox eligió implementar el límite arbitrario. RFC 7578 menciona claramente que --se requieren esos dos guiones iniciales:

    4.1. Parámetro "límite" de datos multiparte / formulario

    Al igual que con otros tipos multiparte, las partes se delimitan con un delimitador de límite, construido utilizando CRLF, "-" y el valor del parámetro "límite".

  • cada campo obtiene algunos subtítulos antes de sus datos: Content-Disposition: form-data;el campo name, elfilename , seguido de los datos.

    El servidor lee los datos hasta la siguiente cadena de límite. El navegador debe elegir un límite que no aparecerá en ninguno de los campos, por lo que el límite puede variar entre las solicitudes.

    Debido a que tenemos el límite único, no es necesaria la codificación de los datos: los datos binarios se envían tal cual.

    TODO: ¿cuál es el tamaño de límite óptimo ( log(N)apuesto) y el nombre / tiempo de ejecución del algoritmo que lo encuentra? Preguntado en: /cs/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences

  • Content-Type se determina automáticamente por el navegador.

    Se preguntó cómo se determina exactamente en: ¿Cómo se determina el tipo mime de un archivo cargado por el navegador?

application / x-www-form-urlencoded

Ahora cambie enctypea application/x-www-form-urlencoded, vuelva a cargar el navegador y vuelva a enviar.

Firefox envió:

POST / HTTP/1.1
[[ Less interesting headers ... ]]
Content-Type: application/x-www-form-urlencoded
Content-Length: 51

text1=text+default&text2=a%CF%89b&file1=a.txt&file2=a.html&file3=binary

Claramente, los datos del archivo no se enviaron, solo los nombres básicos. Por lo tanto, esto no se puede usar para archivos.

En cuanto al campo de texto, vemos que a los caracteres imprimibles habituales les gusta ay bse enviaron en un byte, mientras que a los no imprimibles les gusta 0xCFy 0x89ocupaban 3 bytes cada uno %CF%89:!

Comparación

Las cargas de archivos a menudo contienen muchos caracteres no imprimibles (por ejemplo, imágenes), mientras que los formularios de texto casi nunca lo hacen.

De los ejemplos hemos visto que:

  • multipart/form-data: agrega unos pocos bytes de sobrecarga de límite al mensaje, y debe pasar algún tiempo calculándolo, pero envía cada byte en un byte.

  • application/x-www-form-urlencoded: tiene un límite de un solo byte por campo ( &), pero agrega un factor de sobrecarga lineal de 3x por cada carácter no imprimible.

Por lo tanto, incluso si pudiéramos enviar archivos con application/x-www-form-urlencoded , no querríamos hacerlo, porque es muy ineficiente.

Pero para los caracteres imprimibles que se encuentran en los campos de texto, no importa y genera menos sobrecarga, por lo que solo lo usamos.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
1
¿Cómo agregarías un archivo adjunto binario? (es decir, una imagen pequeña): ¿puedo ver cómo cambian los valores de los atributos Content-Dispositiony Content-Typepero cómo manejar el 'contenido'?
blurfus
3
@ianbeks El navegador lo hace automáticamente antes de enviar la solicitud. No sé qué heurística usa, pero lo más probable es que la extensión de archivo se encuentre entre ellas. Esto puede responder la pregunta: stackoverflow.com/questions/1201945/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
3
@CiroSantilli 六四 事件 法轮功 纳米比亚 威 视 Creo que esta respuesta es mucho mejor que la elegida. Pero elimine el contenido irrelevante de su perfil. Está en contra del espíritu de SO.
smwikipedia
2
@smwikipedia gracias por la cita de rfc y por agradar esta respuesta! Sobre el nombre de usuario: para mí, el espíritu de SO es que todos deberían tener la mejor información en todo momento. ~~ Mantengamos esta discusión en twitter o meta. Paz.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
@ KumarHarsh no hay suficientes detalles para responder, creo. Abra una nueva pregunta súper detallada.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
62

Enviar archivo como contenido binario (cargar sin formulario o FormData)

En las respuestas / ejemplos dados, el archivo se carga (muy probablemente) con un formulario HTML o con la API FormData . El archivo es solo una parte de los datos enviados en la solicitud, de ahí el multipart/form-data Content-Typeencabezado.

Si desea enviar el archivo como el único contenido, puede agregarlo directamente como el cuerpo de la solicitud y establecer el Content-Typeencabezado con el tipo MIME del archivo que está enviando. El nombre del archivo se puede agregar en el Content-Dispositionencabezado. Puedes subir así:

var xmlHttpRequest = new XMLHttpRequest();

var file = ...file handle...
var fileName = ...file name...
var target = ...target...
var mimeType = ...mime type...

xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.send(file);

Si no (desea) usar formularios y solo está interesado en cargar un solo archivo, esta es la forma más fácil de incluir su archivo en la solicitud.

Marchitar
fuente
¿Cómo se configura un servicio del lado del servidor para esto con Asp.Net 4.0? ¿Manejará también múltiples parámetros de entrada, como userId, path, captionText, etc.?
Asle G
1
@AsleG No, es solo para enviar un solo archivo como contenido de su solicitud. No soy un experto en Asp.Net, pero simplemente debe extraer el contenido (un blob) de la solicitud y guardarlo en un archivo usando el Content-Typedel encabezado.
marchita el
@AsleG Quizás este enlace pueda ayudar
Wilt
@wilt Si no uso el formulario, pero quiero usar la API formdata, ¿puedo hacerlo de esa manera?
Kiwi enojado
1
@AnkitKhettry Parece que se carga con un formulario o mediante el uso de la API del formulario. Estas 'cadenas extrañas' a las que se refiere son los límites del formulario que normalmente se utilizan para separar los datos del formulario en partes del servidor.
Wilt
9

Tengo este código Java de muestra:

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class TestClass {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(8081);
        Socket accept = socket.accept();
        InputStream inputStream = accept.getInputStream();

        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
        char readChar;
        while ((readChar = (char) inputStreamReader.read()) != -1) {
            System.out.print(readChar);
        }

        inputStream.close();
        accept.close();
        System.exit(1);
    }
}

y tengo este archivo test.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>File Upload!</title>
</head>
<body>
<form method="post" action="http://localhost:8081" enctype="multipart/form-data">
    <input type="file" name="file" id="file">
    <input type="submit">
</form>
</body>
</html>

y finalmente el archivo que usaré para fines de prueba, llamado a.dat, tiene el siguiente contenido:

0x39 0x69 0x65

si interpreta los bytes anteriores como caracteres ASCII o UTF-8, en realidad representarán:

9ie

Así que ejecutemos nuestro Código Java, abra test.html en nuestro navegador favorito, cargue a.daty envíe el formulario y vea lo que recibe nuestro servidor:

POST / HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Content-Length: 196
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: null
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary06f6g54NVbSieT6y
DNT: 1
Accept-Encoding: gzip, deflate
Accept-Language: en,en-US;q=0.8,tr;q=0.6
Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF

------WebKitFormBoundary06f6g54NVbSieT6y
Content-Disposition: form-data; name="file"; filename="a.dat"
Content-Type: application/octet-stream

9ie
------WebKitFormBoundary06f6g54NVbSieT6y--

Bueno, no me sorprende ver a los personajes 9ie porque le dijimos a Java que los imprimiera tratándolos como personajes UTF-8. También puede optar por leerlos como bytes sin procesar.

Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF 

es en realidad el último encabezado HTTP aquí. Después de eso viene el Cuerpo HTTP, donde se pueden ver meta y contenidos del archivo que subimos.

Koray Tugay
fuente
6

Un mensaje HTTP puede tener un cuerpo de datos enviado después de las líneas de encabezado. En una respuesta, aquí es donde el recurso solicitado se devuelve al cliente (el uso más común del cuerpo del mensaje), o tal vez un texto explicativo si hay un error. En una solicitud, aquí es donde los datos ingresados ​​por el usuario o los archivos cargados se envían al servidor.

http://www.tutorialspoint.com/http/http_messages.htm

flagg19
fuente