Почему сервер и клиент не синхронизированы? (розетки питона) - PullRequest
2 голосов
/ 14 марта 2019

В настоящее время я пишу небольшое клиент-серверное приложение для передачи произвольного файла с сервера на клиент через сокеты.

Сервер будет обрабатывать только одного клиента за раз, но когда клиентобслуживается, он должен быть готов обработать новое клиентское соединение.

Клиент запросит файл, если файл существует, клиент получит файл, запишет его на диск и закроет соединение.

Код сервера:

PORT = 9000
BUFSIZE = 1000

def main(argv):
    print('The server is ready to receive')
    server_socket = socket(AF_INET, SOCK_STREAM)
    server_socket.bind(('', PORT))
    server_socket.listen(1)
    while True:
        connection_socket, addr = server_socket.accept()

        try:
            requested_filepath = connection_socket.recv(BUFSIZE).decode()
            print("Client requested the file: " + requested_filepath)
            capital_sentence = requested_filepath.upper()
            if(os.path.isfile(requested_filepath)):
                filesize = str(os.path.getsize(requested_filepath))
                connection_socket.send(filesize.encode())
                with open(requested_filepath, 'rb') as f:
                    while(True):
                        content = f.read(BUFSIZE)
                        if not content:
                            break
                        connection_socket.send(content)
                print('File has been send')
            else:
                error = "error"
                connection_socket.send(error.encode())
        finally: 
            connection_socket.close()

Код клиента:

PORT = 9000
BUFSIZE = 1000

def main(argv):
    servername = argv[0]
    filepath = argv[1]

    client_socket = socket(AF_INET, SOCK_STREAM)    
    client_socket.connect((servername, PORT))
    try:
        client_socket.send(filepath.encode())
        response = client_socket.recv(BUFSIZE).decode()
        if(response != "error"):
            filesize = int(response)
            print("Requested filesize: " + str(filesize))
            filename = filepath.split('/')[-1]
            with open(filename, 'wb') as f:
                while(True):
                    content = client_socket.recv(BUFSIZE)
                    if not content:
                        break
                    f.write(content)
            print('File recived')
        else:
            print("The requested file did not exist")
    finally:
        client_socket.close()

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

Ошибка клиента:

Traceback (most recent call last):
  File "client.py", line 37, in <module>
    main(sys.argv[1:])
  File "client.py", line 16, in main
    response = client_socket.recv(BUFSIZE).decode()
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 6: invalid start byte

Ошибка сервера:

The server is ready to receive
Client requested the file: /pepe.jpeg
File has been send
Client requested the file: /pepe.jpeg
File has been send
Client requested the file: /pepe.jpeg
Traceback (most recent call last):
  File "server.py", line 44, in <module>
    main(sys.argv[1:])
  File "server.py", line 30, in main
    connection_socket.send(content)
ConnectionResetError: [Errno 104] Connection reset by peer

Не закрываю ли я сокетное соединение в надлежащемпуть?

1 Ответ

2 голосов
/ 14 марта 2019

Вы попали в одну из самых распространенных ловушек программирования сокетов TCP. Вы предполагали, что ваш сокет будет отправлять сообщения, в то время как он отправляет и получает только данные и абсолютно не зависит от вашей структуры обмена сообщениями. Даже если вы отправляете данные, используя несколько вызовов send, ваши вызовы recv не получают такую ​​точную структуру, как это ни происходит в буфере. Если вы отправили один байт тысячу раз, ваш recv (1000) получит тысячу байт, и это то, что здесь происходит.

Ваша проблема вызвана тем, что ваш сервер работает немного быстрее вашего клиента. Мне пришлось настроить ваш код, чтобы иметь возможность надежно воспроизводить код, но это делает это:

client_socket.send(filepath.encode())
sleep(1)
response = client_socket.recv(BUFSIZE).decode()

Это эмулирует ваш сервер быстрее, чем клиент, что в конечном итоге произойдет в любом случае. Добавив sleep, мы можем сделать это каждый раз.

Когда вы вызываете recv на сокете TCP, может произойти одно из следующих пяти событий:

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

Что происходит с вашим кодом, так это то, что вашему серверу удалось отправить размер закодированного файла, а также некоторые ваши данные. На вашем клиенте вы теперь предполагаете, что ваш первый recv получает только размер файла, но это никак не гарантировано. Там уже могут быть некоторые данные файла (как вы будете читать BUFSIZE - там может быть почти полный буфер данных), и когда вы пытаетесь декодировать это как целое число, происходят странные вещи, так как данные не соответствуют вашим ожиданиям ,

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

Самый простой способ исправить это, если ваш сервер отправляет исходное сообщение фиксированной длины. Затем вы можете безопасно прочитать именно это количество символов из сокета и обработать его как сообщение о размере / ошибке, а остальные будут данными. Это ужасное исправление во многих отношениях, и вы должны стремиться к чему-то лучшему. «Правильный» способ - разработать протокол, в котором сервер устанавливает разделители, чтобы ваш клиент мог определить, какое сообщение означает, что. Ваш протокол может быть, например,

SIZE: <decimal>\n
DATA: <data>

или даже так просто, как предполагать, что все перед новой строкой - это размер файла, а все, что следует, - данные

Но это работает лучше даже с добавленным sleep (1), так как теперь начальное сообщение будет заполнено ровно 100 байтами. Это может все же пойти не так из-за (4), так что на самом деле вам нужно будет проверить, что вы получили 100 символов изначально и продолжить чтение, пока вы не сделаете, но я оставлю это для вас, чтобы реализовать.

        if(os.path.isfile(requested_filepath)):
            filesize = str(os.path.getsize(requested_filepath))
            connection_socket.send(("%s" % filesize).encode().ljust(100))
            with open(requested_filepath, 'rb') as f:
                while(True):
                    content = f.read(BUFSIZE)
                    if not content:
                        break
                    connection_socket.send(content)
            print('File has been send')
        else:
            error = "error"
            connection_socket.send(error.encode().ljust(100))

Клиент:

try:
    client_socket.send(filepath.encode())
    sleep(1)
    response_raw = client_socket.recv(100)
    response = response_raw.strip().decode()

PS ваш сервер должен поймать ошибку «сброс соединения по одноранговой сети». Это может произойти, если возникнет проблема с сетью или произойдет сбой клиентского приложения. Сервер может спокойно проигнорировать эту ошибку и просто прекратить отправку в этот конкретный клиентский сокет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...