protocolo - php cabeceras http
¿Cómo funciona la carga de archivos HTTP? (5)
¿Cómo se envía el archivo internamente?
El formato se llama multipart/form-data
, como se pregunta en: ¿Qué significa enctype = ''multipart / form-data''?
Voy a:
- añadir algunas referencias HTML5 más
- explique por qué tiene razón con un formulario enviar ejemplo
Referencias HTML5
Hay tres posibilidades para enctype
:
-
x-www-urlencoded
-
multipart/form-data
(puntos de especificación a RFC2388 ) -
text-plain
Esto "no se puede interpretar de manera confiable por computadora", por lo que nunca debe usarse en producción, y no lo veremos más a fondo.
Cómo generar los ejemplos.
Una vez que vea un ejemplo de cada método, se vuelve obvio cómo funcionan y cuándo debe usar cada uno.
Puedes producir ejemplos usando:
-
nc -l
o un servidor ECHO: servidor de prueba HTTP que acepta solicitudes GET / POST - un agente de usuario como un navegador o cURL
Guarde el formulario en un archivo .html
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ω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 a aωb
, lo que significa aωb
porque ω
es U+03C9
, que son los bytes 61 CF 89 62
en UTF-8.
Crea archivos para subir:
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 de eco:
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, nc
BSD 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ωb
en UTF-8) se envían literalmente. Podrías verificar que con nc -l localhost 8000 | hd
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=---------------------------9051914041544843365972754266
Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266
establece el tipo de contenido enmultipart/form-data
y dice que los campos están separados por el dado cadena deboundary
.cada campo obtiene algunos subencabezados antes de sus datos:
Content-Disposition: form-data;
, elname
del campo, elfilename
delfilename
, 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 solicitudes.
Debido a que tenemos un límite único, no es necesaria la codificación de los datos: los datos binarios se envían tal como están.
TODO: ¿cuál es el tamaño de límite óptimo (
log(N)
que apuesto) y el nombre / tiempo de ejecución del algoritmo que lo encuentra? Consultado en: https://cs.stackexchange.com/questions/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequencesContent-Type
es determinado automáticamente por el navegador.¿Cómo se determina exactamente cómo se preguntó? ¿Cómo se determina el tipo de mime de un archivo cargado por el navegador?
aplicación / x-www-form-urlencoded
Ahora cambie el enctype
a 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 de base. Así que esto no puede ser usado para archivos.
En cuanto al campo de texto, vemos que los caracteres imprimibles habituales como a
y b
se enviaron en un byte, mientras que los no imprimibles como 0xCF
y 0x89
tomaron 3 bytes cada uno: %CF%89
!
Comparación
La carga de archivos a menudo contiene muchos caracteres no imprimibles (por ejemplo, imágenes), mientras que las formas 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 en calcularlo, 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 application/x-www-form-urlencoded
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 simplemente lo usamos.
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? ¿Se envía el archivo 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.
Un mensaje HTTP puede tener un cuerpo de datos enviados después de las líneas del 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 quizás 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.
Enviar archivo como contenido binario (subir sin formulario o FormData)
En las respuestas / ejemplos dados, el archivo es (lo más probable) subido con un formulario HTML o utilizando la API FormData . El archivo es solo una parte de los datos enviados en la solicitud, de ahí el encabezado de Content-Type
multipart/form-data
.
Si desea enviar el archivo como el único contenido, puede agregarlo directamente como el cuerpo de la solicitud y configurar el encabezado Content-Type
al tipo MIME del archivo que está enviando. El nombre del archivo se puede agregar en el encabezado Content-Disposition
. Puedes subir como esta:
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 (quiere) usar formularios y solo le interesa cargar un solo archivo, esta es la forma más fácil de incluir su archivo en la solicitud.
Echemos un vistazo a lo que sucede cuando selecciona un archivo y envía su formulario (he truncado los encabezados para mayor 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--
En lugar de que la URL codifique 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_SIZE
con el valor establecido en el formulario, así como una sección que contiene los datos del archivo. El nombre del archivo es parte del encabezado Content-Disposition
.
Los detalles completos están here .
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 {
final ServerSocket socket = new ServerSocket(8081);
final Socket accept = socket.accept();
final InputStream inputStream = accept.getInputStream();
final 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é con fines de prueba, denominado 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.dat
y envíe el formulario para ver qué 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 los personajes 9ie porque le dijimos a Java que los imprimiera tratándolos como caracteres UTF-8. Puede elegir 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 los metadatos y el contenido del archivo que cargamos.