2 votes

Pourquoi la socket Python ne reçoit-elle pas une valeur vide finale pendant la boucle recv de la socket HTTP ?

J'ai écrit un module proxy miniature en Python 3 qui se place simplement entre mon navigateur et le web. Mon but est de simplement proxyer le trafic allant et venant. Un des comportements du programme est de sauvegarder les réponses du site web que je reçois dans un répertoire local.

Tout fonctionne comme prévu, à l'exception du simple fait que l'utilisation de socket.recv() dans une boucle semble ne jamais donner le blanc bytes impliqué dans l'objet exemples fournis dans la documentation . Pratiquement tous les exemples en ligne parlent de la chaîne vide qui passe par le socket lorsque le serveur le ferme.

Je suppose qu'il se passe quelque chose via l'en-tête keep-alive, où le serveur distant nunca ferme la socket à moins que son propre seuil de dépassement de temps ne soit atteint. Est-ce correct ? Si oui, comment puis-je détecter la fin de l'envoi d'une charge utile ? Observer que les données reçues sont plus petites que la taille des morceaux que j'ai déclarée ne fonctionne pas du tout, en raison de la façon dont le TCP fonctionne.

Pour démontrer, le code suivant ouvre un socket vers un fichier image sur le serveur web de Google. J'ai copié la chaîne de requête réelle des propres requêtes de mon navigateur. L'exécution du code (rappelez-vous, Python 3 !) montre que les données binaires de l'image sont reçues jusqu'à la fin, mais le code n'est jamais capable de frapper la requête break déclaration. Ce n'est qu'au moment où le serveur ferme le socket (après environ 3 minutes d'inactivité) que ce code atteint réellement l'instruction print à la fin du fichier.

Comment diable peut-on contourner ce problème ? Mon objectif est de ne pas modifier le comportement des requêtes de mon navigateur - je ne veux pas avoir à définir la valeur de l'attribut keep-alive à l'en-tête false ou quelque chose d'éclatant comme ça. La solution consiste-t-elle à utiliser de vilains timeouts (par le biais de socket.settimeout() ) ? Cela semble risible, mais je ne vois pas ce qu'on pourrait faire d'autre.

Merci d'avance.

import socket

remote_host = 'www.google.com'
remote_port = 80

remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote_socket.connect((remote_host, remote_port))
remote_socket.sendall(b'GET http://www.google.com/images/logos/ps_logo2a_cp.png HTTP/1.1\r\nHost: www.google.com\r\nCache-Control: max-age=0\r\nPragma: no-cache\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\n\r\n')

content = b''
while True:
    msg = remote_socket.recv(1024)
    if not msg:
        break
    print(msg)
    content += msg

print("DONE: %d" % len(content))

3voto

Mark Tolonen Points 32702

Si vous avez une connexion de type "keep-alive", il y aura une indication de la longueur du message dans les en-têtes de la réponse. Voir Message HTTP . Tampon recv jusqu'à ce que vous ayez l'en-tête complet (terminé par une ligne blanche), déterminez la longueur du corps du message et lisez exactement cette quantité d'informations.

Voici une classe simple pour mettre en mémoire tampon les lectures TCP jusqu'à ce qu'un terminateur de message ou un nombre spécifique d'octets ait été lu. Je l'ai ajoutée à votre exemple :

import socket
import re

class MessageError(Exception): pass

class MessageReader(object):
    def __init__(self,sock):
        self.sock = sock
        self.buffer = b''

    def get_until(self,what):
        while what not in self.buffer:
            if not self._fill():
                return b''
        offset = self.buffer.find(what) + len(what)
        data,self.buffer = self.buffer[:offset],self.buffer[offset:]
        return data

    def get_bytes(self,size):
        while len(self.buffer) < size:
            if not self._fill():
                return b''
        data,self.buffer = self.buffer[:size],self.buffer[size:]
        return data

    def _fill(self):
        data = self.sock.recv(1024)
        if not data:
            if self.buffer:
                raise MessageError('socket closed with incomplete message')
            return False
        self.buffer += data
        return True

remote_host = 'www.google.com'
remote_port = 80

remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote_socket.connect((remote_host, remote_port))
remote_socket.sendall(b'GET http://www.google.com/images/logos/ps_logo2a_cp.png HTTP/1.1\r\nHost: www.google.com\r\nCache-Control: max-age=0\r\nPragma: no-cache\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\n\r\n')
mr = MessageReader(remote_socket)
header = mr.get_until(b'\r\n\r\n')
print(header.decode('ascii'))
m = re.search(b'Content-Length: (\d+)',header)
if m:
    length = int(m.group(1))
    data = mr.get_bytes(length)
    print(data)
remote_socket.close()

Sortie

HTTP/1.1 200 OK
Content-Type: image/png
Last-Modified: Thu, 12 Aug 2010 00:42:08 GMT
Date: Tue, 21 Jun 2011 05:03:35 GMT
Expires: Tue, 21 Jun 2011 05:03:35 GMT
Cache-Control: private, max-age=31536000
X-Content-Type-Options: nosniff
Server: sffe
Content-Length: 6148
X-XSS-Protection: 1; mode=block

b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01l\x00\x00\x00~\x08\x03\x00\ (rest omitted)

2voto

Dietrich Epp Points 72865

Un moyen très simple de faire en sorte que le serveur ferme la connexion consiste à ajouter cet en-tête à votre requête HTTP :

Connection: close

Par défaut, les serveurs HTTP/1.1 sont autorisés à maintenir la connexion ouverte afin que vous puissiez créer une deuxième requête. Vous devriez tout de même créer un délai d'attente afin de ne pas être privé de sockets lorsque les serveurs ignorent l'en-tête.

1voto

George Points 11

Lorsqu'une connexion tcp est fermée, elle envoie un dernier message vide indiquant que la socket a été fermée. Lorsque vous recevez ce message, vous devez probablement fermer le socket de votre côté également.

1voto

platinummonkey Points 800

Honnêtement, la solution la plus simple et la plus fiable reste l'utilisation des délais d'attente des sockets, l'encapsulation dans un try/except et l'utilisation de l'exception socket.timeout. Vous pourriez probablement regarder le dernier bit de données reçu pour voir s'il aurait dû ou non mourir.

remote_socket.setblocking(True) # not really needed but to emphasize this 
                                #is a blocking socket until the timeout
remote_socket.settimeout(15) # 15 second timeout
while True:
  try
    msg = remote_socket.recv(1024)
    if not msg:
        break
    print(msg)
    content += msg
  except socket.timeout:
    #do some checking on last received data
  else:
    #socket died for another reason or ended the way it was supposed to.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X