Comment fonctionne le téléchargement de fichiers HTTP?


528

Lorsque je soumets un formulaire simple comme celui-ci avec un fichier joint:

<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>

Comment envoie-t-il le fichier en interne? Le fichier est-il envoyé en tant que partie du corps HTTP en tant que données? Dans les en-têtes de cette demande, je ne vois rien de lié au nom du fichier.

Je voudrais juste savoir le fonctionnement interne du HTTP lors de l'envoi d'un fichier.


Je n'ai pas utilisé de renifleur depuis un certain temps, mais si vous voulez voir ce qui est envoyé dans votre demande (puisque c'est au serveur c'est une demande) reniflez-le. Cette question est trop large. SO est plus pour des questions de programmation spécifiques.
paparazzo

... comme les renifleurs, le violoneux est mon arme de choix. Vous pouvez même créer vos propres demandes de test pour voir comment elles sont publiées.
Phil Cooper

Pour ceux qui sont intéressés, voir aussi " MAX_FILE_SIZEen PHP - à quoi
ça sert

Je trouve MAX_FILE_SIZE bizarre. car je peux modifier mon html en chrome à 100000000 avant de le poster pour qu'il affiche une meilleure valeur. Soit 1. l'avoir dans un cookie avec un hachage sécurisé via salt donc le cookie s'il est modifié, le serveur peut valider et lever une exception (comme les webpieces ou playframework font tous les deux) ou une sorte de validation de formulaire que les choses n'ont pas changé. @ 0xSina
Dean Hiller

Réponses:


320

Jetons un coup d'œil à ce qui se passe lorsque vous sélectionnez un fichier et soumettez votre formulaire (j'ai tronqué les en-têtes par souci de concision):

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--

REMARQUE: chaque chaîne de limite doit être préfixée avec un extra --, tout comme à la fin de la dernière chaîne de limite. L'exemple ci-dessus comprend déjà cela, mais il peut être facile de le manquer. Voir le commentaire de @Andreas ci-dessous.

Au lieu d'URL codant les paramètres du formulaire, les paramètres du formulaire (y compris les données du fichier) sont envoyés sous forme de sections dans un document en plusieurs parties dans le corps de la demande.

Dans l'exemple ci-dessus, vous pouvez voir l'entrée MAX_FILE_SIZEavec la valeur définie dans le formulaire, ainsi qu'une section contenant les données du fichier. Le nom du fichier fait partie de l'en- Content-Dispositiontête.

Les détails complets sont ici .


7
@ source.rar: Non. Les serveurs Web sont (presque?) toujours filetés pour pouvoir gérer des connexions simultanées. Essentiellement, le processus démon qui écoute sur le port 80 délivre immédiatement la tâche de servir à un autre thread / processus afin qu'il puisse retourner à l'écoute pour une autre connexion; même si deux connexions entrantes arrivent exactement au même moment, elles resteront simplement dans le tampon réseau jusqu'à ce que le démon soit prêt à les lire.
eggyal

10
L'explication du threading est un peu incorrecte car il existe des serveurs hautes performances conçus comme un seul thread et utilisant une machine d'état pour télécharger rapidement à tour de rôle des paquets de données à partir des connexions. Au contraire, dans TCP / IP, le port 80 est un port d'écoute, pas le port sur lequel les données sont transférées.
slebetman

9
Lorsqu'une prise d'écoute IP (port 80) reçoit une connexion, une autre prise est créée sur un autre port, généralement avec un nombre aléatoire supérieur à 1000. Cette prise est ensuite connectée à la prise distante, laissant le port 80 libre pour écouter les nouvelles connexions.
slebetman

11
@slebetman Tout d'abord, il s'agit de HTTP. Le mode FTP actif ne s'applique pas ici. Deuxièmement, la prise d'écoute n'est pas bloquée à chaque connexion. Vous pouvez avoir autant de connexions à un port que les autres côtés ont de ports auxquels lier leur propre extrémité.
Slotos

33
Notez que la chaîne de limite transmise dans le cadre du champ d'en-tête Content-Type est 2 caractères plus courte que les chaînes de limite pour les pièces individuelles ci-dessous. Je viens de passer une heure à essayer de comprendre pourquoi mon programme de téléchargement ne fonctionne pas, car il est assez difficile de remarquer qu'il n'y a en fait que 4 tirets dans la première chaîne de limite mais 6 tirets dans les autres chaînes de limite. En d'autres termes: lorsque vous utilisez la chaîne de limite pour séparer les données de formulaire individuelles, elle doit être préfixée par deux tirets: - Elle est décrite dans la RFC1867 bien sûr, mais je pense qu'elle doit également être signalée ici
Andreas

280

Comment envoie-t-il le fichier en interne?

Le format est appelé multipart/form-data, comme demandé à: Que signifie enctype = 'multipart / form-data'?

Je vais:

  • ajouter des références HTML5 supplémentaires
  • expliquer pourquoi il a raison avec un formulaire soumettre un exemple

Références HTML5

Il existe trois possibilités pour enctype:

Comment générer les exemples

Une fois que vous voyez un exemple de chaque méthode, il devient évident comment elles fonctionnent et quand vous devez les utiliser.

Vous pouvez produire des exemples en utilisant:

Enregistrez le formulaire dans un .htmlfichier minimal :

<!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>

Nous avons mis la valeur de texte par défaut a&#x03C9;b, ce qui signifie aωbque ωest U+03C9, qui sont les octets 61 CF 89 62en UTF-8.

Créez des fichiers à télécharger:

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

Exécutez notre petit serveur d'écho:

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

Ouvrez le code HTML sur votre navigateur, sélectionnez les fichiers et cliquez sur soumettre et vérifiez le terminal.

nc imprime la demande reçue.

Testé sur: Ubuntu 14.04.3, ncBSD 1.105, Firefox 40.

multipart / form-data

Firefox a envoyé:

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--

Pour le fichier binaire et le champ de texte, les octets 61 CF 89 62( aωben UTF-8) sont envoyés littéralement. Vous pouvez vérifier cela avec nc -l localhost 8000 | hd, qui dit que les octets:

61 CF 89 62

ont été envoyés ( 61== 'a' et 62== 'b').

Il est donc clair que:

  • Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150définit le type de contenu sur multipart/form-dataet indique que les champs sont séparés par la boundarychaîne donnée .

    Mais notez que:

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

    a deux papas de moins --que la barrière réelle

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

    En effet, la norme requiert que la limite commence par deux tirets --. Les autres tirets semblent être juste la façon dont Firefox a choisi d'implémenter la frontière arbitraire. La RFC 7578 mentionne clairement que ces deux tirets principaux --sont requis:

    4.1. "Boundary" Parameter of multipart / form-data

    Comme pour les autres types de parties multiples, les pièces sont délimitées par un délimiteur de limite, construit à l'aide de CRLF, "-" et de la valeur du paramètre "limite".

  • chaque champ obtient des sous-en-têtes avant ses données:, Content-Disposition: form-data;le champ name, le filename, suivi des données.

    Le serveur lit les données jusqu'à la chaîne de limite suivante. Le navigateur doit choisir une limite qui n'apparaîtra dans aucun des champs, c'est pourquoi la limite peut varier entre les demandes.

    Parce que nous avons la frontière unique, aucun encodage des données n'est nécessaire: les données binaires sont envoyées telles quelles.

    TODO: quelle est la taille optimale de la limite ( log(N)je parie) et le nom / le temps d'exécution de l'algorithme qui la trouve? Demandé à: /cs/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences

  • Content-Type est automatiquement déterminé par le navigateur.

    Comment il est déterminé exactement a été demandé à: Comment le type MIME d'un fichier téléchargé est-il déterminé par le navigateur?

application / x-www-form-urlencoded

Maintenant, changez le enctypeen application/x-www-form-urlencoded, rechargez le navigateur et soumettez à nouveau.

Firefox a envoyé:

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

De toute évidence, les données du fichier n'ont pas été envoyées, uniquement les noms de base. Donc, cela ne peut pas être utilisé pour les fichiers.

En ce qui concerne le champ de texte, nous voyons que les caractères imprimables habituels aiment aet bont été envoyés dans un octet, tandis que ceux non imprimables aiment 0xCFet 0x89ont pris 3 octets chacun %CF%89:!

Comparaison

Les téléchargements de fichiers contiennent souvent de nombreux caractères non imprimables (par exemple des images), alors que les formulaires texte ne le font presque jamais.

D'après les exemples, nous avons vu que:

  • multipart/form-data: ajoute quelques octets de surcharge de limite au message, et doit passer un certain temps à le calculer, mais envoie chaque octet dans un octet.

  • application/x-www-form-urlencoded: a une limite d'un octet par champ ( &), mais ajoute un facteur de surcharge linéaire de 3x pour chaque caractère non imprimable.

Par conséquent, même si nous pouvions envoyer des fichiers avec application/x-www-form-urlencoded, nous ne le voudrions pas, car c'est tellement inefficace.

Mais pour les caractères imprimables trouvés dans les champs de texte, cela n'a pas d'importance et génère moins de surcharge, nous ne l'utilisons donc que.


1
Comment ajouteriez-vous une pièce jointe binaire? (c'est-à-dire une petite image) - Je peux voir changer les valeurs des attributs Content-Dispositionet Content-Typemais comment gérer le «contenu»?
blurfus

3
@ianbeks Le navigateur le fait automatiquement avant d'envoyer la demande. Je ne sais pas quelle heuristique il utilise, mais très probablement l'extension de fichier en fait partie. Cela peut répondre à la question: stackoverflow.com/questions/1201945/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

3
@CiroSantilli 六四 事件 法轮功 纳米比亚 威 视 Je pense que cette réponse est bien meilleure que celle choisie. Mais veuillez supprimer le contenu non pertinent de votre profil. C'est contraire à l'esprit de SO.
smwikipedia

2
@smwikipedia merci pour la citation rfc et pour avoir aimé cette réponse! À propos du nom d'utilisateur: pour moi, l'esprit de SO est que tout le monde devrait avoir la meilleure information à tout moment. ~~ Gardons cette discussion sur Twitter ou méta. Paix.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

1
@KumarHarsh pas assez de détails pour répondre je pense. Veuillez ouvrir une nouvelle question super détaillée.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

62

Envoyer un fichier en tant que contenu binaire (téléchargement sans formulaire ni FormData)

Dans les réponses / exemples donnés, le fichier est (très probablement) téléchargé avec un formulaire HTML ou en utilisant l' API FormData . Le fichier n'est qu'une partie des données envoyées dans la demande, d'où l'en- multipart/form-data Content-Typetête.

Si vous souhaitez envoyer le fichier en tant que seul contenu, vous pouvez l'ajouter directement en tant que corps de la demande et définir l'en- Content-Typetête sur le type MIME du fichier que vous envoyez. Le nom du fichier peut être ajouté dans l'en- Content-Dispositiontête. Vous pouvez télécharger comme ceci:

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 vous n'utilisez pas (ne voulez pas) de formulaires et que vous ne souhaitez télécharger qu'un seul fichier, c'est la manière la plus simple d'inclure votre fichier dans la demande.


Comment configurez-vous un service côté serveur pour cela avec Asp.Net 4.0? Va-t-il également gérer plusieurs paramètres d'entrée, tels que userId, path, captionText, etc.?
Asle G

1
@AsleG Nope, c'est uniquement pour envoyer un seul fichier comme contenu de votre demande. Je ne suis pas un expert Asp.Net, mais vous devez simplement extraire le contenu (un blob) de la demande et l'enregistrer dans un fichier en utilisant l'en Content-Type-tête.
Flétrissement le

@AsleG Peut - être que ce lien peut vous aider
Wilt

@wilt Si je n'utilise pas de formulaire, mais que je veux utiliser l'API formdata, puis-je le faire de cette façon?
kiwi en colère

1
@AnkitKhettry On dirait qu'il est téléchargé avec un formulaire ou en utilisant l'API de formulaire. Ces «chaînes étranges» auxquelles vous vous référez sont les limites de formulaire normalement utilisées pour séparer les données de formulaire en parties sur le serveur.
Flétrissement du

9

J'ai cet exemple de code Java:

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

et j'ai ce fichier 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>

et enfin le fichier que j'utiliserai à des fins de test, nommé a.dat, a le contenu suivant:

0x39 0x69 0x65

si vous interprétez les octets ci-dessus comme des caractères ASCII ou UTF-8, ils représenteront en fait:

9ie

Exécutons donc notre code Java, ouvrons test.html dans notre navigateur préféré, téléchargez a.datet soumettez le formulaire et voyez ce que notre serveur reçoit:

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--

Eh bien, je ne suis pas surpris de voir les caractères 9ie car nous avons dit à Java de les imprimer en les traitant comme des caractères UTF-8. Vous pouvez également choisir de les lire sous forme d'octets bruts.

Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF 

est en fait le dernier en-tête HTTP ici. Après cela vient le corps HTTP, où les méta et le contenu du fichier que nous avons téléchargé peuvent être vus.


6

Un message HTTP peut avoir un corps de données envoyé après les lignes d'en-tête. Dans une réponse, c'est là que la ressource demandée est retournée au client (l'utilisation la plus courante du corps du message), ou peut-être un texte explicatif en cas d'erreur. Dans une demande, c'est là que les données saisies par l'utilisateur ou les fichiers téléchargés sont envoyés au serveur.

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.