Python предлагает очень удобную обертку над сокетами (socket), однако, как известно, сокеты являются достаточно низкоуровневыми и не гарантируют доставку сообщения целиком. То есть, отправив большой объем данных, вы их получите, но, скорее всего, по частям. В этой заметке будет показан простой способ сделать обертку над сокетами для получения примитивного протокола уровня пакетов и передавать таким образом большие файлы.
Идея
Фрагментация сообщений при передаче через сокеты приводит к тому, что невозможно заранее знать размер получаемого сообщения, а прием/передача с применением заведомо большего буфера также не гарантирует передачу за один вызов socket.send.
Простейшим решением этой проблемы является добавление в начало посылки ее размера в байтах. Таким образом, первым вызовом recvall можно получать только размер (4 байта), который практически гарантированно будет передан в одном TCP пакете (фрагментация, как правило, возникает при передаче больше, чем 1 кБ), после чего считать из буфера оставшееся сообщение, длинна которого уже известна.
Передача пакета
Для передачи большого сообщения (например, картинки) к нему, сформированным привычным образом (как для socket) добавим в начало длину в байтах (в сетевом порядке следования байт) и отправим целиком через вызов socket.send.
Реализуется это простой функцией:
def send_msg(self, sock, msg): # Каждое сообщение будет иметь префикс в 4 байта блинной(network byte order) msg = struct.pack('>I', len(msg)) + msg sock.send(msg)
Получение пакета
Получение пакета менее тривиально: первоначально напишем вспомогательную функцию для получения заданного числа байт:
def recvall(self, sock, n): # Функция для получения n байт или возврата None если получен EOF data = b'' while len(data) < n: packet = sock.recv(n - len(data)) if not packet: return None data += packet return data
Так, пока не будет достигнут заданный размер, в byteArray добавляются принятые данные. По достижению заданного размера возвращается весь принятый пакет.
Теперь можно написать функцию для приема пакета целиком:
def recv_msg(self, sock): # Получение длины сообщения и распаковка в integer raw_msglen = self.recvall(sock, 4) if not raw_msglen: return None msglen = struct.unpack('>I', raw_msglen)[0] # Получение данных return self.recvall(sock, msglen)
Тут первоначально из сокета считывается размер сообщения (4 байта), распаковывается в целое число и, узнав таким образом размер сообщения, получается остаток данных с помощью вспомогательной функции
Простой класс
Упростим использование: чтобы не передавать сокет каждый раз явно, создадим класс.
Фактически он ничем не отличается от предложенных ранее функций, но объект сокета передается один раз при создании, а не каждый раз при вызове функций:
import socket import struct class SuperSocket(): def __init__(self, sock): self._sock = sock def send_msg(self, msg): # Каждое сообщение будет иметь префикс в 4 байта блинной(network byte order) msg = struct.pack('>I', len(msg)) + msg self._sock.send(msg) def recv_msg(self): # Получение длины сообщения и распаковка в integer raw_msglen = self.recvall(4) if not raw_msglen: return None msglen = struct.unpack('>I', raw_msglen)[0] # Получение данных return self.recvall(msglen) def recvall(self, n): # Функция для получения n байт или возврата None если получен EOF data = b'' while len(data) < n: packet = self._sock.recv(n - len(data)) if not packet: return None data += packet return data