各位观众老爷,晚上好!我是今晚的主讲人,咱们今儿个不整虚的,直奔主题:Python socket
模块的recv()
和sendall()
方法,重点聊聊流量控制和缓冲区管理。这俩哥们儿看似简单,但用起来门道可不少,稍不留神,你的程序就可能掉坑里。
一、socket
编程的那些事儿:打个招呼先
在深入recv()
和sendall()
之前,咱们先简单回顾一下socket
编程的基础。简单来说,socket
就是应用程序之间进行网络通信的接口。你可以把它想象成一个电话插孔,两边的程序通过这个插孔连接起来,然后就可以互相“打电话”聊天了。
Python的socket
模块提供了创建和使用socket
的工具。使用流程大致如下:
- 创建Socket: 选择使用TCP(
SOCK_STREAM
)还是UDP(SOCK_DGRAM
)。TCP是可靠的、面向连接的,UDP是不可靠的、无连接的。 - 绑定地址: 将Socket绑定到一个IP地址和端口号上,这样其他程序才能找到你。
- 监听连接(TCP): 如果是服务器,需要监听来自客户端的连接请求。
- 接受连接(TCP): 接受客户端的连接请求,建立连接。
- 发送/接收数据: 使用
send()
/recv()
或者sendall()
/recv()
来发送和接收数据。 - 关闭连接: 通信完成后,关闭Socket连接。
二、recv()
:小口慢咽,接收数据
recv(bufsize)
方法用于从socket接收数据。 bufsize
参数指定了可以接收的最大数据量(字节数)。
- 工作原理:
recv()
尝试从socket缓冲区读取最多bufsize
个字节的数据。 - 返回值: 返回接收到的字节串,如果连接已关闭,则返回空字节串
b''
。 - 阻塞行为: 默认情况下,
recv()
是阻塞的。这意味着如果socket缓冲区中没有数据,recv()
会一直等待,直到有数据到达或者连接关闭。 - 数据完整性: 重点来了!
recv()
不保证 接收到bufsize
个字节的数据。它可能只接收到一部分数据,即使发送方发送了更多的数据。这是因为网络传输的复杂性和缓冲区大小的限制。
举个栗子:
import socket
# 创建一个TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址
s.bind(('127.0.0.1', 8888))
# 监听连接
s.listen(1)
# 接受连接
conn, addr = s.accept()
print(f"Connected by {addr}")
# 接收数据
data = conn.recv(1024) # 尝试接收1024字节的数据
print(f"Received: {data}")
# 关闭连接
conn.close()
s.close()
在这个例子中,conn.recv(1024)
尝试接收1024字节的数据,但实际上可能接收到的数据少于1024字节。所以,在使用recv()
时,你需要做好处理接收到的数据可能不完整的准备。
三、sendall()
:一口闷,发送全部数据
sendall(bytes)
方法用于从socket发送所有数据。
- 工作原理:
sendall()
会尝试发送bytes
中的所有数据。 - 返回值: 如果发送成功,
sendall()
返回None
。 - 阻塞行为: 和
recv()
一样,sendall()
也是阻塞的。 - 数据完整性: 重点来了!
sendall()
保证 发送所有数据,或者抛出一个异常。它会持续发送数据,直到所有数据都发送完毕,或者发生错误。
举个栗子:
import socket
# 创建一个TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接到服务器
s.connect(('127.0.0.1', 8888))
# 要发送的数据
message = b"Hello, world! This is a long message."
# 发送数据
s.sendall(message)
print("Message sent")
# 关闭连接
s.close()
在这个例子中,s.sendall(message)
会尝试发送 message
中的所有数据。如果发送过程中发生错误,比如连接断开,sendall()
会抛出一个 socket.error
异常。
四、recv()
和sendall()
:爱恨情仇,流量控制与缓冲区管理
现在,我们来聊聊recv()
和sendall()
与流量控制和缓冲区管理的关系。
-
流量控制:
-
recv()
:recv()
的bufsize
参数直接影响了接收方的接收速率。如果bufsize
设置得太小,接收方需要多次调用recv()
才能接收到完整的数据,这会增加开销。如果bufsize
设置得太大,可能会浪费内存。更重要的是,如果发送方发送数据的速率超过了接收方处理数据的速率,接收方的缓冲区可能会溢出,导致数据丢失。 -
sendall()
:sendall()
虽然保证发送所有数据,但它并不能解决流量控制的问题。如果接收方处理数据的速率跟不上发送方发送数据的速率,发送方仍然会被阻塞,直到接收方有足够的缓冲区空间来接收数据。
-
-
缓冲区管理:
-
recv()
:recv()
从 socket 缓冲区读取数据。socket 缓冲区是操作系统内核为每个 socket 分配的一块内存区域,用于临时存储接收到的数据。recv()
的作用就是将数据从 socket 缓冲区复制到应用程序的内存中。 -
sendall()
:sendall()
将数据写入 socket 缓冲区。操作系统内核会将数据从 socket 缓冲区发送到网络上。
-
五、实战演练:解决数据不完整的问题
前面说了,recv()
不保证接收到完整的数据。那么,如何解决这个问题呢?通常有两种方法:
- 固定长度的消息: 发送方发送固定长度的消息,接收方每次调用
recv()
接收固定长度的数据。 - 消息边界: 在消息中添加消息边界,接收方通过消息边界来判断消息是否完整。
咱们先来看固定长度的消息:
import socket
def receive_fixed_length_message(sock, msglen):
message = b''
while len(message) < msglen:
chunk = sock.recv(msglen - len(message))
if chunk == b'':
return None # 连接已关闭
message += chunk
return message
# 创建一个TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8888))
s.listen(1)
conn, addr = s.accept()
print(f"Connected by {addr}")
# 假设消息长度为10字节
message = receive_fixed_length_message(conn, 10)
print(f"Received: {message}")
conn.close()
s.close()
在这个例子中,receive_fixed_length_message()
函数会循环调用 recv()
,直到接收到指定长度的消息为止。
再来看消息边界的例子:
import socket
def receive_message_with_delimiter(sock, delimiter=b'n'):
message = b''
while True:
chunk = sock.recv(4096) # 每次接收 4096 字节
if not chunk:
return None # 连接已关闭
message += chunk
if delimiter in message:
return message.split(delimiter)[0] # 返回第一个完整的消息
# 创建一个TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8888))
s.listen(1)
conn, addr = s.accept()
print(f"Connected by {addr}")
# 接收消息,以换行符作为分隔符
message = receive_message_with_delimiter(conn)
print(f"Received: {message}")
conn.close()
s.close()
在这个例子中,receive_message_with_delimiter()
函数会循环调用 recv()
,直到接收到包含分隔符的消息为止。
六、缓冲区大小的调整:精打细算,物尽其用
socket 缓冲区的大小会影响网络通信的性能。可以使用 getsockopt()
和 setsockopt()
函数来获取和设置 socket 缓冲区的大小。
- SO_RCVBUF: 接收缓冲区大小。
- SO_SNDBUF: 发送缓冲区大小。
举个栗子:
import socket
# 创建一个TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取接收缓冲区大小
recv_buf_size = s.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
print(f"Default receive buffer size: {recv_buf_size}")
# 设置接收缓冲区大小
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8192) # 设置为 8KB
recv_buf_size = s.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
print(f"New receive buffer size: {recv_buf_size}")
# 获取发送缓冲区大小
send_buf_size = s.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
print(f"Default send buffer size: {send_buf_size}")
# 设置发送缓冲区大小
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192) # 设置为 8KB
send_buf_size = s.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
print(f"New send buffer size: {send_buf_size}")
s.close()
注意: 操作系统对 socket 缓冲区的大小有限制。通常情况下,你可以增大缓冲区的大小,但不能超过操作系统的限制。
七、总结:recv()
和sendall()
的正确使用姿势
方法 | 功能 | 是否保证数据完整性 | 阻塞行为 | 流量控制影响 |
---|