Python套接字接收 - 收到的数据包大小总是不同

60
我正在使用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()

如果客户端在同一源端口上发送多个请求,但服务器卡住了,任何帮助都将不胜感激,谢谢!

7个回答

174
Larry Hastings的回答有一些关于sockets的很好的通用建议,但是涉及到Python socket模块中recv(bufsize)方法的工作方式时存在一些错误。
因此,为了澄清这一点,因为其他人也可能在寻求帮助,需要注意以下几点:
  1. recv()方法的bufsize参数不是可选的。如果没有参数调用recv()会导致出错。
  2. recv(bufsize)方法中的bufferlen是最大值。如果可用数据较少,recv将愉快地返回较少的字节数。
详细内容请参阅文档
现在,如果您正在从客户端接收数据并希望知道何时接收完所有数据,您可能需要将其添加到您的协议中--正如Larry所建议的那样。请参阅此处的示例以获取确定消息结尾的策略。
正如该示例所指出的那样,在某些协议中,客户端完成发送数据后会直接断开连接。在这种情况下,您的while True循环应该可以正常工作。如果客户端没有断开连接,您需要想办法发出信号来指示内容长度、分隔消息或实现超时。
如果您能发布您的确切客户端代码和测试协议的描述,我很乐意尝试进一步帮助。

4
我发现最好的方法是先计算信息/文件/数据中的字节数,然后将信息/文件/数据的长度作为头部以分隔符 : 的形式发送。接收时,检测到 : 后就一直用 recv 接收直到获取消息的长度,然后根据头部明确地使用 recv 获取需要的内容。如果是文件,则循环接收文件的块,同时确保 recv 的大小可被2整除,直到最后一个字节(如果 总字节数 % 2 != 0)。我使用这种方法传输大文件(几个GB),这对于显示进度条也很有帮助。 - RattleyCooper
我测试了 recv(bufsize),它发送了较少的数据。但我的问题是 Python 如何理解这是结束?由于 TCP 是一个流,服务器如何检测流中的数据结束? - peiman F.

36

注意:正如评论中所指出的那样,在Python中调用没有参数的recv()是不允许的,因此应该忽略这个答案。

原始回答:


网络始终是不可预测的。 TCP 可以帮你消除很多这种随机行为。 TCP 做的一件很棒的事情:它保证字节将按相同的顺序到达。 但是! 它不能保证它们会以相同的方式切割到达。 你根本 不能 假设从连接的一端发送的每个 send() 都会导致从另一端接收到恰好一个具有完全相同字节数的 recv()。

当你说 socket.recv(x) 时,你是在说“在读取 x 字节之前不要返回”。 这被称为“阻塞 I/O”:您将阻塞(等待),直到请求已填充。 如果您的协议中的每个消息都是完全 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

62
Hans L在下面的评论中说得对,Python中的request.recv()不是一个有效的调用,因为bufsize是一个必填参数。理想情况下,应该删除或编辑这个答案。详见http://docs.python.org/library/socket.html。 - prashantsunkari
3
如果你的协议中每个消息都恰好是1024字节,那么调用socket.recv(1024)会非常有效...这也是不正确的。 - President James K. Polk
10
如果你尝试调用socket.recv()时没有传递参数,就会返回TypeError: recv() takes at least 1 argument (0 given)。请注意,这意味着你必须至少传递一个参数给recv()函数。 - Jguy
11
类型错误:recv()至少需要一个参数(未提供0个)。 - evandrix
18
令人惊讶的是,点赞此评论的51个人不知道这并不可行,更糟糕的是楼主将该回答标记为正确... - Danny Watson
显示剩余2条评论

22

您也可以使用recv(x_bytes, socket.MSG_WAITALL)的替代方法,该方法似乎只适用于Unix,并且将精确返回x_bytes


4
请注意,您的代码被冻结的确切原因不是因为您设置了太高的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()时才发送空消息,因此只要不关闭连接,程序就会一直循环。

Hans L 在这里指出了一些好的方法来结束消息。

编辑:

问题

你真正的问题是没有任何合适的方式来结束你的网络消息。所以你的程序会一直等到客户端结束连接或超时发生。

关键字解决方案

一个解决方案是在接收到数据时查找特殊的关键字,当你找到特殊关键字时,你就不必等待连接关闭,而是中断循环并继续运行程序。更高级的方法是在消息中加入一个特殊标签,例如:<message>hello world</message>

头部解决方案

另一种方法是先发送一个具有固定长度的头部消息。在这个消息中,你发送信息,说明你的消息的其余部分(主体)有多长,这样你的程序就知道应该把什么放入self.request.recv,以及何时中断循环。

这些问题正是为什么我们使用 HTTP 等协议。它们是经过精心设计、解决了所有这些底层问题的协议。


3

这就是TCP的本质:该协议填充数据包(底层为IP数据包)并发送。您可以在一定程度上控制MTU(最大传输单元)。

换句话说,您必须设计一个协议,该协议建立在TCP之上,并定义了您的“有效载荷分界线”。通过“有效载荷分界线”,我指的是提取协议支持的消息单元的方式。这可以简单地表示为“每个以NULL结尾的字符串”。


2

你可以尝试始终将前4个字节的数据作为数据大小发送,然后一次性读取完整的数据。在客户端和服务器端都使用以下函数来发送和接收数据。

def send_data(conn, data):
    serialized_data = pickle.dumps(data)
    conn.sendall(struct.pack('>I', len(serialized_data)))
    conn.sendall(serialized_data)


def receive_data(conn):
    data_size = struct.unpack('>I', conn.recv(4))[0]
    received_payload = b""
    reamining_payload_size = data_size
    while reamining_payload_size != 0:
        received_payload += conn.recv(reamining_payload_size)
        reamining_payload_size = data_size - len(received_payload)
    data = pickle.loads(received_payload)

    return data

您可以在https://github.com/vijendra1125/Python-Socket-Programming.git找到示例程序。


这个链接提供了Python套接字编程的示例程序。

1

我知道这篇文章有点老,但我希望能对某些人有所帮助。

使用普通的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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接