Почему сокет Python не получает окончательное пустое значение во время цикла recv HTTP-сокета? - PullRequest
2 голосов
/ 21 июня 2011

Я написал миниатюрный прокси-модуль в Python 3, чтобы просто сидеть между моим браузером и Интернетом. Моя цель - просто прокси трафика, идущего туда и обратно. Одно из действий программы - сохранять ответы сайта, которые я получаю, в локальном каталоге.

Все работает так, как я ожидаю, за исключением простого факта, что использование socket.recv() в цикле, похоже, никогда не приведет к пустому объекту bytes, подразумеваемому в примерах , приведенных в документах . Практически каждый пример в сети говорит о пустой строке, проходящей через сокет, когда сервер закрывает ее.

Я предполагаю, что что-то происходит через заголовок keep-alive, где удаленный сервер никогда не закрывает сокет, пока не будет достигнут собственный порог тайм-аута. Это правильно? Если да, то как мне узнать, когда закончена отправка полезной нагрузки? Наблюдение за тем, что полученные данные меньше моего объявленного размера порции, вообще не работает из-за того, как функционирует TCP.

Для демонстрации следующий код открывает сокет в файле изображения на веб-сервере Google. Я скопировал фактическую строку запроса из собственных запросов моего браузера. Выполнение кода (помните, Python 3!) Показывает, что двоичные данные изображения получены до завершения, но тогда код никогда не сможет выполнить оператор break. Только когда сервер закрывает сокет (после 3 минут простоя) этот код фактически достигает команды print в конце файла.

Как же можно обойти это? Моя цель - не изменять поведение запросов моего браузера - я не хочу устанавливать для заголовка keep-alive значение false или что-то безвкусное. Является ли ответ использовать некрасивые тайм-ауты (через socket.settimeout())? Кажется смешным, но я не знаю, что еще можно сделать.

Заранее спасибо.

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

Ответы [ 4 ]

3 голосов
/ 21 июня 2011

Если у вас есть соединение keep-alive, в заголовках ответа будет некоторое указание длины сообщения. См. HTTP-сообщение . Буферизуйте recv до тех пор, пока не получите полный заголовок (оканчивающийся пустой строкой), определите длину тела сообщения и прочитайте ровно столько информации.

Вот простой класс для буферизации чтения TCP до тех пор, пока не будет прочитан терминатор сообщения или определенное количество байтов. Я добавил это к вашему примеру:

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

выход

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)
2 голосов
/ 21 июня 2011

Один очень простой способ заставить сервер закрыть соединение - добавить этот заголовок в ваш HTTP-запрос:

Connection: close

По умолчанию серверам HTTP / 1.1 разрешено сохранять соединение открытым, чтобы вы могли создать второй запрос. Вы все равно должны создать тайм-аут, чтобы не нуждаться в сокетах, когда серверы игнорируют заголовок.

0 голосов
/ 21 июня 2011

Честно говоря, самое простое и надежное решение по-прежнему будет использовать тайм-ауты сокета и инкапсулировать его в попытке / исключении и использовать исключение socket.timeout.Вы могли бы, вероятно, взглянуть на последний бит полученных данных, чтобы увидеть, должен ли он умереть или не погибнуть.

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.
0 голосов
/ 21 июня 2011

Когда соединение TCP закрыто, оно отправит окончательное пустое сообщение, указывающее, что сокет был закрыт.Когда вы получите сообщение, вам, скорее всего, следует также закрыть сокет на своем конце.

...