54

我将 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()

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

4

7 回答 7

164

Larry Hastings 的回答对套接字有一些很好的一般性建议,但存在一些错误,因为它与该recv(bufsize)方法在 Python 套接字模块中的工作方式有关。

因此,澄清一下,因为这可能会让其他寻求帮助的人感到困惑:

  1. recv(bufsize)方法的 bufsize 参数不是可选的。如果你调用recv()(没有参数)你会得到一个错误。
  2. bufferlen inrecv(bufsize)最大尺寸。如果可用的字节更少,recv 将很高兴返回更少的字节。

有关详细信息,请参阅文档

现在,如果您从客户端接收数据并想知道何时收到所有数据,您可能必须将其添加到您的协议中——正如 Larry 建议的那样。有关确定消息结束的策略,请参阅此配方。

正如该配方所指出的,对于某些协议,客户端在完成发送数据后将简单地断开连接。在这些情况下,您的while True循环应该可以正常工作。如果客户端没有断开连接,您将需要找出某种方式来指示您的内容长度、分隔消息或实现超时。

如果您可以发布您的确切客户端代码和您的测试协议的描述,我将很乐意提供进一步的帮助。

于 2009-11-27T05:43:38.603 回答
40

网络总是不可预测的。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
于 2009-11-11T16:02:59.103 回答
20

您也可以使用recv(x_bytes, socket.MSG_WAITALL),它似乎只在 Unix 上有效,并且将准确返回x_bytes.

于 2009-12-02T04:40:21.577 回答
3

这就是 TCP 的本质:协议填充数据包(下层是 IP 数据包)并发送它们。您可以对 MTU(最大传输单元)进行一定程度的控制。

换句话说:您必须设计一个基于 TCP 的协议,其中定义了您的“有效负载描述”。“有效负载描述”是指您提取协议支持的消息单元的方式。这可以像“每个 NULL 终止的字符串”一样简单。

于 2009-11-10T15:38:33.923 回答
3

请注意,您的代码被冻结的确切原因不是因为您设置了太高的 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 的好方法

编辑:

问题

所以你真正的问题是你没有任何适当的方法来结束你的网络消息。因此,您的程序将一直等到客户端结束连接或发生超时。

关键字解决方案

一种解决方案是在接收到的数据中查找特殊关键字,当您找到特殊关键字时,您无需等待连接关闭,而是中断循环并继续您的程序。一种更高级的方法是将您的消息包含在一个特殊的标签中,例如<message>hello world</message>.

标头解决方案

另一种方法是首先发送一个始终相同(固定)长度的标头消息。在此消息中,您发送消息的剩余时间(正文)信息,因此您的程序将知道它究竟应该放入什么self.request.recv以及何时中断循环。

这些问题是我们使用例如HTTP 的原因。它已经是精心设计的协议,可以为我们解决所有这些低级问题。

于 2017-12-15T11:36:37.823 回答
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找到示例程序

于 2020-08-22T05:49:22.930 回答
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)
于 2017-04-26T18:30:50.933 回答