6
votes

Comment un serveur Web peut-il savoir quand une requête HTTP est entièrement reçue?

J'écris actuellement un serveur Web très simple pour en savoir plus sur la programmation de socket de bas niveau. Plus précisément, j'utilise C ++ comme langage principal et j'essaie d'encapsuler les appels système de bas niveau C dans des classes C ++ avec une API de plus haut niveau.

J'ai écrit une classe Socket qui gère un descripteur de fichier de socket et gère l'ouverture et la fermeture à l'aide de RAII. Cette classe expose également les opérations de socket standard pour une socket orientée connexion (TCP) comme lier, écouter, accepter, se connecter, etc.

Après avoir lu les pages de manuel pour send et recv appels système J'ai réalisé que je devais appeler ces fonctions dans une certaine forme de boucle afin de garantir que tous les octets sont correctement envoyés / reçus. p >

Mon API d'envoi et de réception ressemble à ceci

template<typename T>
T Receive(const int fd)
{
   using SizeType = typename T::size_type;
   using ValueType = typename T::value_type;

   T result;

   const SizeType bufSize{1024};
   ValueType buf[bufSize];
   while (true)
   {
      const ssize_t retVal{recv(fd, buf, bufSize, 0)};
      if (retVal < 0)
      {
          throw ch::NetworkError{"Failed to receive."};
      }

      if (retVal == 0)
      {
          break; /* Connection is closed. */
      }

      const SizeType offset{static_cast<SizeType>(retVal)};
      result.insert(std::end(result), buf, buf + offset);
   }

   return result;
}

Pour la fonctionnalité d'envoi, j'ai décidé d'utiliser un appel de blocage send dans un boucle comme celle-ci (c'est une fonction d'assistance interne qui fonctionne à la fois pour std :: string et std :: vector).

template<typename T>
void Send(const int fd, const T& bytes)
{
   using ValueType = typename T::value_type;
   using SizeType = typename T::size_type;

   const ValueType *const data{bytes.data()};
   SizeType bytesToSend{bytes.size()};
   SizeType bytesSent{0};
   while (bytesToSend > 0)
   {
      const ValueType *const buf{data + bytesSent};
      const ssize_t retVal{send(fd, buf, bytesToSend, 0)};
      if (retVal < 0)
      {
          throw ch::NetworkError{"Failed to send."};
      }
      const SizeType sent{static_cast<SizeType>(retVal)};
      bytesSent += sent;
      bytesToSend -= sent;
   }
}

Cela semble fonctionner correctement et garantit que tout les octets sont envoyés une fois que la fonction membre est renvoyée sans lever d'exception.

Cependant, j'ai commencé à rencontrer des problèmes lorsque j'ai commencé à implémenter la fonctionnalité de réception. Pour ma première tentative, j'ai utilisé un appel de blocage recv dans une boucle et j'ai quitté la boucle si recv retournait 0 indiquant que la connexion TCP sous-jacente était fermée.

void SendBytes(const std::vector<std::uint8_t>& bytes) const;
void SendStr(const std::string& str) const;
std::vector<std::uint8_t> ReceiveBytes() const;
std::string ReceiveStr() const;

Cela fonctionne bien tant que la connexion est fermée par l'expéditeur après que tous les octets ont été envoyés. Cependant, ce n'est pas le cas lors de l'utilisation par ex. Chrome pour demander une page Web. La connexion est maintenue ouverte et ma fonction de réception membre est bloquée lors de l'appel système recv après avoir reçu tous les octets de la requête. J'ai réussi à contourner ce problème en définissant un délai d'expiration sur l'appel recv en utilisant setsockopt . Fondamentalement, je renvoie tous les octets reçus jusqu'à présent une fois le délai expiré. Cela semble être une solution très inélégante et je ne pense pas que ce soit la façon dont les serveurs Web gèrent ce problème en réalité.

Alors, passons à ma question.

Comment un serveur Web sait-il quand une requête HTTP a été entièrement reçue?

Une requête GET dans HTTP 1.1 ne semble pas inclure d'en-tête Content-Length. Voir par exemple ce lien . p >


5 commentaires

c ++ et c sont des langages distincts. Il n'y a pratiquement aucune ligne dans ce code qui pourrait être considérée c.


Une requête HTTP GET n'a pas de données, tout ce qu'elle a, ce sont les champs d'en-tête (facultatifs) et une terminaison bien spécifiée.


La connexion reste ouverte Notez que c'est une pratique normale dans le protocole HTTP: une connexion existante peut être réutilisée plusieurs fois. Vous ne devriez pas le fermer sans aucune raison.


@Matt exactement, c'est pourquoi je voulais déterminer les bons critères pour savoir quand arrêter d'appeler recv à plusieurs reprises.


Au fait, vos idiomes C ++ semblent bons. Bien joué!


4 Réponses :


2
votes

Un en-tête de requête se termine par une ligne vide (deux CRLF sans rien entre eux).

Ainsi, lorsque le serveur a reçu un en-tête de requête, puis reçoit une ligne vide, et si la requête était un GET (qui n'a pas de charge utile), il sait que la requête est complète et peut passer à la formation d'une réponse. Dans d'autres cas, il peut passer à la lecture de Content-Length de la charge utile et agir en conséquence.

Il s'agit d'une propriété fiable et bien définie de la syntaxe .

Aucune Content-Length n'est requise ou utile pour un GET : le contenu est toujours de longueur nulle. Une Header-Length hypothétique ressemble plus à ce que vous demandez, mais vous devez d'abord analyser l'en-tête pour le trouver, il n'existe donc pas et nous utilisons cette propriété du syntaxe à la place. En conséquence, cependant, vous pouvez envisager d’ajouter un délai d’expiration artificiel et une taille de mémoire tampon maximale, en plus de votre analyse normale, pour vous protéger contre les requêtes parfois lentes ou longues de manière malveillante.


7 commentaires

Techniquement, la longueur du contenu peut être ajoutée à n'importe quel verbe de requête, y compris get. Techniquement, les requêtes GET peuvent inclure du contenu.


@Michael C'est certainement possible mais c'est sans utilité dans ce contexte.


@Remy Merci pour votre modification. Chaque requête HTTP fonctionne de cette façon AFAIK. Y a-t-il des divergences?


@LightnessRacesinOrbit votre libellé d'origine impliquait que CHAQUE requête HTTP se termine à la ligne vide suivant les en-têtes, et ce n'est tout simplement pas vrai. LA PLUPART Les requêtes HTTP ont un corps de message après les en-têtes (même si le corps est de 0 octet). Les requêtes GET et HEAD se terminent après les en-têtes, car il n'y a pas de corps. D'autres demandes se terminent après le corps. Vous devez analyser les en-têtes de chaque requête pour savoir si un corps est présent et comment le lire.


@RemyLebeau D'accord, j'imagine que j'aurais dû dire que la requête en-tête se termine par une ligne vide. Dans ma philosophie, la demande est une chose, et une charge utile suit éventuellement, mais YMMV.


@LightnessRacesinOrbit Les en-têtes de requête et la charge utile de la requête sont des éléments séparés d'un même message. La demande est tout le message. Idem avec les réponses.


@RemyLebeau D'accord, d'où le "j'aurais dû dire"! Quoi qu'il en soit, j'ai ajusté le libellé maintenant.



5
votes

HTTP / 1.1 est un protocole basé sur du texte, avec des données POST binaires ajoutées d'une manière quelque peu hacky. Lors de l'écriture d'une "boucle de réception" pour HTTP, vous ne pouvez pas séparer complètement la partie de réception de données de la partie d'analyse HTTP. En effet, dans HTTP, certains caractères ont une signification particulière. En particulier, le jeton CRLF ( 0x0D 0x0A ) est utilisé pour séparer les en-têtes, mais aussi pour terminer la requête en utilisant deux jetons CRLF un après le autre.

Donc, pour arrêter de recevoir, vous devez continuer à recevoir des données jusqu'à ce que l'un des événements suivants se produise:

  • Délai d'expiration - suivi en envoyant une réponse d'expiration
  • Deux CRLF dans la requête - suivez en analysant la requête, puis répondez si nécessaire (analysée correctement? la requête a du sens? envoyer des données?)
  • Trop de données - certains exploits HTTP visent à épuiser les ressources du serveur comme la mémoire ou les processus (voir par exemple loris lent)

Et peut-être d'autres cas extrêmes. Notez également que cela ne s'applique qu'aux demandes sans corps. Pour les requêtes POST, vous attendez d'abord deux jetons CRLF , puis lisez Content-Length octets en plus. Et cela est encore plus compliqué lorsque le client utilise le codage en plusieurs parties.


3 commentaires

Merci pour votre réponse détaillée! Je connaissais déjà les deux ensembles de CRLF utilisés pour signaler la fin de la demande, j'aurais peut-être dû le préciser dans la question. La principale chose à retenir de votre réponse est que je dois continuer à recevoir des données jusqu'à ce que je trouve ce délimiteur dans le flux d'octets ou que je quitte plus tôt en fonction d'autres critères. Il s'avère que mon idée de temps mort n'était pas si loin après tout.


Les deux CRLF ne signalent pas la fin de la requête , ils signalent uniquement la fin des en-têtes de requête . Il PEUT OU PEUT NE PAS y avoir un corps de message après les en-têtes. Vous devez analyser les en-têtes pour déterminer non seulement SI un corps est présent, mais également dans QUEL FORMAT il est envoyé afin que vous le lisiez correctement. La requête se termine à la fin du corps du message s'il y en a un, ou dans le sens inverse à la fin des en-têtes. COMMENT vous déterminez la fin du corps dépend de son format de transfert.


@RemyLebeau Oui, je suis d'accord. Je mets "notez que cela ne s'applique qu'aux demandes sans corps". En général, vous déterminez quel type (méthode) de requête vous traitez en analysant les en-têtes, après avoir reçu deux CRLF .



2
votes

La solution se trouve dans votre lien

Une requête GET dans HTTP 1.1 ne semble pas inclure d'en-tête Content-Length. Voir par exemple ce lien .

Là, il est dit:

Il doit utiliser des fins de ligne CRLF, et il doit se terminer par \ r \ n \ r \ n


0 commentaires

1
votes

La réponse est formellement définie dans les spécifications du protocole HTTP 1 :

Donc, pour résumer, le serveur lit d'abord la ligne de départ initiale du message pour déterminer le type de requête. Si la version HTTP est 0.9, la requête est effectuée, car la seule requête prise en charge est GET sans aucun en-tête. Sinon, le serveur lit alors les en-tête de message du message jusqu'à ce qu'un CRLF de fin soit atteint. Ensuite, seulement si le type de demande a un corps de message défini, le serveur lit le corps selon le format de transfert décrit par les en-têtes de demande (les demandes et les réponses ne sont pas limitées à l'utilisation d'un en-tête Content-Length HTTP 1.1).

Dans le cas d'une requête GET , il n'y a pas de corps de message défini, donc le message se termine après la start-line dans HTTP 0.9, et après la fin < code> CRLF des message-header dans HTTP 1.0 et 1.1.

1: Je ne vais pas entrer dans HTTP 2.0 , qui est un tout autre jeu de balle.


4 commentaires

Je pense que RFC 7230 sec. 3.3 est entièrement suffisant pour répondre à cette question. Je ne sais pas pourquoi vous avez vu la nécessité de citer la RFC 2616 obsolète (comme vous le savez), thouch. Un vote favorable car cela devrait être la réponse acceptée.


@DaSourcerer de nombreux serveurs Web n'ont pas encore été mis à jour pour implémenter les RFC 7230 ... 7235, ils implémentent toujours RFC 2616. Bien que les RFC 7230-7235 ne soient principalement qu'une restructuration de la RFC 2616 pour la décomposer, apportez également un certain nombre de modifications au protocole (comme déprécier le pliage d'en-tête et développer la façon dont la longueur d'un message est déterminée). C'est pourquoi je mentionne les deux ensembles de RFC pour HTTP 1.1.


Chaque en-tête est séparé par CRLF, comment savoir quelle CRLF est une terminaison?


@EntityinArray a lu les spécifications auxquelles j'ai lié. Oui, chaque en-tête se termine par un CRLF , mais une fois les en-têtes terminés, il y a un autre CRLF isolé. En d'autres termes, les en-têtes se terminent par une paire CRLF CRLF .