Python socket receive - входящие пакеты всегда имеют разный размер - PullRequest
33 голосов
/ 10 ноября 2009

Я использую модуль SocketServer для TCP-сервера. У меня возникли некоторые проблемы с функцией recv(), потому что входящие пакеты всегда имеют другой размер, поэтому, если я укажу recv(1024) (я пробовал с большим значением и меньшим), он застревает через 2 или 3 запросы, потому что длина пакета будет меньше (я думаю), и тогда сервер застревает до истечения времени ожидания.

class Test(SocketServer.BaseRequestHandler):

def handle(self):

   print "From:", self.client_address

   while True:    

     data = self.request.recv(1024)
     if not data: break

     if data[4] == "\x20":              
       self.request.sendall("hello")
     if data[4] == "\x21":
       self.request.sendall("bye")
     else:
       print "unknow packet"
   self.request.close()
   print "Disconnected", self.client_address

launch = SocketServer.ThreadingTCPServer(('', int(sys.argv[1])),Test)

launch.allow_reuse_address= True;

launch.serve_forever()

Если клиент отправляет несколько запросов через один и тот же исходный порт, но сервер застревает, любая помощь будет принята с благодарностью, спасибо!

Ответы [ 6 ]

124 голосов
/ 27 ноября 2009

В ответе Ларри Хастингса есть несколько общих советов о сокетах, но есть несколько ошибок, связанных с тем, как метод recv(bufsize) работает в модуле сокетов Python.

Итак, чтобы уточнить, так как это может сбить с толку других, которые обращаются к этому за помощью:

  1. Параметр bufsize для метода recv(bufsize) не является обязательным. Вы получите сообщение об ошибке, если позвоните recv() (без параметра).
  2. Размер буфера recv(bufsize) - максимальный . Recv с радостью вернет меньше байтов, если их будет меньше.

Подробнее см. в документации .

Теперь, если вы получаете данные от клиента и хотите знать, когда вы получили все данные, вам, вероятно, придется добавить их в свой протокол - как предполагает Ларри. См. этот рецепт для стратегий определения конца сообщения.

Как указывает этот рецепт, для некоторых протоколов клиент просто отключится, когда закончит отправку данных. В этих случаях ваш while True цикл должен работать нормально. Если клиент не отключается, вам нужно найти способ указать длину вашего контента, определить границы сообщений или установить тайм-аут.

Я бы с радостью постарался помочь вам, если бы вы могли опубликовать свой точный код клиента и описание протокола тестирования.

39 голосов
/ 11 ноября 2009

Сеть всегда непредсказуема. TCP избавляет вас от этого случайного поведения. TCP делает одну замечательную вещь: он гарантирует, что байты будут поступать в том же порядке. Но! Это не гарантирует, что они прибудут рублеными таким же образом. Вы просто не можете предположить, что каждый send () с одного конца соединения приведет к точно одному recv () на дальнем конце с точно таким же количеством байтов.

Когда вы говорите socket.recv(x), вы говорите «не возвращайтесь, пока не прочитаете x байтов из сокета». Это называется «блокировка ввода / вывода»: вы будете блокировать (ждать), пока ваш запрос не будет выполнен. Если бы каждое сообщение в вашем протоколе было ровно 1024 байта, вызов socket.recv(1024) работал бы отлично Но, похоже, это не так. Если ваши сообщения имеют фиксированное число байтов, просто передайте это число в socket.recv() и все готово.

Но что, если ваши сообщения могут быть разной длины? Первое, что вам нужно сделать: прекратить звонить socket.recv() с явным номером. Изменение этого:

data = self.request.recv(1024)

к этому:

data = self.request.recv()

означает, что recv() всегда будет возвращаться при получении новых данных.

Но теперь у вас возникла новая проблема: откуда вы знаете, когда отправитель отправил вам полное сообщение? Ответ таков: нет. Вы должны будете сделать длину сообщения явной частью вашего протокола. Вот лучший способ: префикс каждого сообщения с длиной, либо в виде целого числа фиксированного размера (преобразуется в сетевой порядок байтов с использованием socket.ntohs() или socket.ntohl(), пожалуйста!), Либо в виде строки, за которой следует некоторый разделитель (например, «123:»). ). Этот второй подход часто менее эффективен, но он проще в Python.

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

length = None
buffer = ""
while True:
  data += self.request.recv()
  if not data:
    break
  buffer += data
  while True:
    if length is None:
      if ':' not in buffer:
        break
      # remove the length bytes from the front of buffer
      # leave any remaining bytes in the buffer!
      length_str, ignored, buffer = buffer.partition(':')
      length = int(length_str)

    if len(buffer) < length:
      break
    # split off the full message from the remaining bytes
    # leave any remaining bytes in the buffer!
    message = buffer[:length]
    buffer = buffer[length:]
    length = None
    # PROCESS MESSAGE HERE
15 голосов
/ 02 декабря 2009

В качестве альтернативы вы можете использовать recv(x_bytes, socket.MSG_WAITALL), который, кажется, работает только в Unix и будет возвращать точно x_bytes.

2 голосов
/ 10 ноября 2009

Такова природа TCP: протокол заполняет пакеты (нижним уровнем являются IP-пакеты) и отправляет их. Вы можете иметь некоторую степень контроля над MTU (Maximum Transfer Unit).

Другими словами: вы должны разработать протокол, который работает поверх TCP, где определено ваше «разграничение полезной нагрузки». Под «разграничением полезной нагрузки» я понимаю способ, которым вы извлекаете единицу сообщения, поддерживаемую вашим протоколом. Это может быть так же просто, как «все строки, заканчивающиеся на NULL».

1 голос
/ 15 декабря 2017

Обратите внимание, что точная причина , почему ваш код заморожен, , а не , потому что вы установили слишком большой размер буфера request.recv (). Здесь объясняется Что означает размер буфера в socket.recv (buffer_size)

Этот код будет работать до тех пор, пока не будет получено пустое TCP-сообщение (если вы напечатаете это пустое сообщение, оно покажет b''):

while True:    
  data = self.request.recv(1024)
  if not data: break

И обратите внимание, что невозможно отправить пустое TCP-сообщение. socket.send(b'') просто не будет работать.

Почему? Поскольку пустое сообщение отправляется только при вводе socket.close(), ваш сценарий будет зацикливаться до тех пор, пока вы не закроете соединение. Как указал Ганс Л , здесь есть несколько хороших методов для завершения сообщения .

0 голосов
/ 26 апреля 2017

Я знаю, что это старо, но я надеюсь, что это кому-то поможет.

Используя обычные python-сокеты, я обнаружил, что вы можете отправлять и получать информацию в пакетах, используя sendto и recvfrom

# tcp_echo_server.py
import socket

ADDRESS = ''
PORT = 54321

connections = []
host = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host.setblocking(0)
host.bind((ADDRESS, PORT))
host.listen(10)  # 10 is how many clients it accepts

def close_socket(connection):
    try:
        connection.shutdown(socket.SHUT_RDWR)
    except:
        pass
    try:
        connection.close()
    except:
        pass

def read():
    for i in reversed(range(len(connections))):
        try:
            data, sender = connections[i][0].recvfrom(1500)
            return data
        except (BlockingIOError, socket.timeout, OSError):
            pass
        except (ConnectionResetError, ConnectionAbortedError):
            close_socket(connections[i][0])
            connections.pop(i)
    return b''  # return empty if no data found

def write(data):
    for i in reversed(range(len(connections))):
        try:
            connections[i][0].sendto(data, connections[i][1])
        except (BlockingIOError, socket.timeout, OSError):
            pass
        except (ConnectionResetError, ConnectionAbortedError):
            close_socket(connections[i][0])
            connections.pop(i)

# Run the main loop
while True:
    try:
        con, addr = host.accept()
        connections.append((con, addr))
    except BlockingIOError:
        pass

    data = read()
    if data != b'':
        print(data)
        write(b'ECHO: ' + data)
        if data == b"exit":
            break

# Close the sockets
for i in reversed(range(len(connections))):
    close_socket(connections[i][0])
    connections.pop(i)
close_socket(host)

Клиент похож

# tcp_client.py
import socket

ADDRESS = "localhost"
PORT = 54321

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ADDRESS, PORT))
s.setblocking(0)

def close_socket(connection):
    try:
        connection.shutdown(socket.SHUT_RDWR)
    except:
        pass
    try:
        connection.close()
    except:
        pass

def read():
    """Read data and return the read bytes."""
    try:
        data, sender = s.recvfrom(1500)
        return data
    except (BlockingIOError, socket.timeout, AttributeError, OSError):
        return b''
    except (ConnectionResetError, ConnectionAbortedError, AttributeError):
        close_socket(s)
        return b''

def write(data):
    try:
        s.sendto(data, (ADDRESS, PORT))
    except (ConnectionResetError, ConnectionAbortedError):
        close_socket(s)

while True:
    msg = input("Enter a message: ")
    write(msg.encode('utf-8'))

    data = read()
    if data != b"":
        print("Message Received:", data)

    if msg == "exit":
        break

close_socket(s)
...