В программировании сокетов Python, почему recv()-ing данные непосредственно из сокетов дает вам только первое сообщение?

Я играю с программированием сокетов в Python 2 и пробую select() для сценария сервера. Когда у меня есть следующий код для сервера:

print('Server started in port {}.'.format(self.port))
server_socket = socket.socket()
server_socket.bind((self.address, self.port))
server_socket.listen(5)

client_sockets = [server_socket]
while True:
    for s in client_sockets:
        if s is server_socket:
            client_socket, address = server_socket.accept()
            client_sockets.append(client_socket)

            print('Connection received.')
        else:
            data = s.recv(200)
            if data:
                print('Received: {}'.format(data.decode().strip()))
            else:
                client_sockets.remove(s)
                s.close()

Сервер получает только первое сообщение от клиента. Однако второе и последующие сообщения будут получены только при перезапуске клиента. Это сбивает меня с толку (которое я приписываю своим недостаточным знаниям в области сетей). Данные, кажется, буферизуются. Почему это происходит?

Я пробовал это:

client_sockets = [server_socket]
while True:
    readable, writable, errored = select.select(client_sockets, [], [])
    for s in readable:
        if s is server_socket:
...

И, наконец, теперь сервер может получать второе и последующие сообщения от клиента.


Вот код для клиента:

class BasicClient(object):

    def __init__(self, name, address, port):
        self.name = name
        self.address = address
        self.port = int(port)
        self.socket = socket.socket()

    def connect(self):
        self.socket.connect((self.address, self.port))
        self.socket.send(self.name)

    def send_message(self, message):
        self.socket.send(message.encode())


args = sys.argv
if len(args) != 4:
    print "Please supply a name, server address, and port."
    sys.exit()

client = BasicClient(args[1], args[2], args[3])
client.connect()
while True:
    message = raw_input('Message: ')

    # We pad the message by 200 since we only expect messages to be
    # 200 characters long.
    num_space_pads = min(200 - len(message), 200)
    message = message.ljust(num_space_pads, ' ')

    client.send_message(message)

person Sean Francis N. Ballais    schedule 27.02.2019    source источник
comment
Сокеты TCP являются потоковыми, без фиксированных пакетов или границ сообщений. Это всего лишь поток байтов. Если вам нужно отправлять фиксированные сообщения, вам нужно реализовать свой собственный протокол поверх TCP, чтобы справиться с этим, который может включать длину сообщений или определенные последовательности конца сообщения. Из-за природы потока байтов TCP это означает, что один вызов send может быть разбит, и получателю потребуется несколько вызовов recv, чтобы получить все. Или что в одном recv может быть получено более одного send. И последнее может включать частичные сообщения.   -  person Some programmer dude    schedule 27.02.2019
comment
Я не знаю, насколько вышеизложенное применимо к вашей проблеме, но без дополнительного контекста и правильного минимального воспроизводимого примера ( как сервера, так и клиента) вам будет очень сложно помочь.   -  person Some programmer dude    schedule 27.02.2019


Ответы (1)


Здесь много проблем.

Ключевая проблема заключается в том, что socket.accept() является блокирующим вызовом. Вы принимаете свое клиентское соединение, затем читаете из него, но затем ваш цикл for обрабатывает обратно в сокет сервера, и ваш код застревает в принятии. Только когда вы повторно подключаетесь к сокету, код сервера перемещается вперед, поэтому кажется, что вы получаете только одно сообщение.

Лучший способ сделать это — использовать потоки. В вашем основном потоке ждите соединений (всегда ожидающих принятия). Когда появится соединение, примите его, затем создайте поток и обработайте там весь клиентский трафик. Затем поток блокируется, ожидая только клиентского сокета, и в конечном итоге завершается при закрытии соединения. Гораздо проще и меньше подвержены ошибкам.

Существует также проблема понятия «сообщение», поскольку сокеты TCP ничего не знают о структуре вашего сообщения. Они просто передают поток данных, что бы это ни было. Когда вы отправляете «сообщение» и читаете из TCP-сокета, на стороне сервера происходит одно из следующих действий:

  1. Нет данных и считывание вызовов блокируется
  2. Есть ровно одно сообщение, и вы получаете это
  3. Сервер работал медленнее вашего клиента, сообщений несколько и вы получаете их все за один раз
  4. Сервер поторопился и решил прочитать, когда была доступна только частичная часть сообщения.
  5. Комбинация 3 и 4: вы получаете несколько полных сообщений и одно частичное

Вам всегда нужно учитывать случаи 3-5 (или использовать zeromq или что-то еще, что понимает концепцию сообщения, если вы не хотите реализовывать это самостоятельно), а не только 1 и 2.

При чтении данных из TCP-сокета всегда необходимо проверять их. Имейте буфер «процесса» и добавляйте к нему то, что вы читаете. Затем начните синтаксический анализ с самого начала и обработайте столько полных сообщений, сколько сможете найти, удалите их из буфера и оставьте там остальные. Тогда предполагается, что частичное сообщение в конечном итоге будет завершено.

person Hannu    schedule 27.02.2019
comment
Я провел расследование, и действительно, оно застряло в accept(). Это помогло мне наконец понять, почему я должен использовать переменную readable при использовании select.select(). Спасибо! - person Sean Francis N. Ballais; 27.02.2019
comment
Почти всегда лучше обрабатывать клиентов в потоках. Это не только проще, но и сделает вашу программу всегда отзывчивой. Плохой или злой клиент не сможет заблокировать всю программу, сделав что-то, чего вы не ожидаете. Например, если вы ожидаете, что клиент отправит 200 символов, клиент, отправляющий 199 и никогда не отправляющий последний, заблокирует всю вашу программу навсегда. Использование одного потока на одно клиентское соединение может привести к множеству проблем. - person Hannu; 27.02.2019
comment
Ах! Это довольно хорошее понимание. Спасибо! Это напоминает мне, как работает Chrome. К сожалению, поскольку то, что я делаю с программированием сокетов, является университетской работой, а спецификации запрещают нам использовать потоки, я застрял на однопоточном сервере. - person Sean Francis N. Ballais; 27.02.2019
comment
Я как бы предположил, что это может быть школьный проект, так как казалось почти бессмысленным делать это таким образом, а многие школьные проекты, как правило, бессмысленны... Удачи в этом. Для получения более качественных оценок вы можете добавить какие-либо проверки отказоустойчивости в свой клиентский ридер, чтобы убедиться, что он всегда продолжается в какой-то момент, даже если клиент делает что-то не так. - person Hannu; 27.02.2019
comment
Проект состоит только в том, чтобы создать ограниченную версию IRC-сервера/клиента с сообщениями, ограниченными 200 символами, поэтому, возможно, мои отказоустойчивые проверки, вероятно, будут направлены на то, чтобы убедиться, что клиент отправляет правильные IRC-команды, и проверить, подключен ли клиент еще (если только у вас есть, что посоветовать по этому поводу). В любом случае, спасибо! - person Sean Francis N. Ballais; 27.02.2019