讨论一下 Python socket 该怎么正确的读所谓的"粘包", 主要是 Python 语言的细节

2021-02-24 10:11:48 +08:00
 todd7zhang

先声明,我知道没有粘包这种东西,关键是自己应用层处理好 socket 流的边界就行。
那么最常见的就是就是[header][body]这种,header 固定 4 字节大端 int32 代表 body 长度,那么我的写法如下

# server.py
body_length = sock.recv(4)    # 这里就不写 struct.unpack 了,意思意思
data = []
while body_length:
    bytes = sock.recv(body_length)
    body_length -= len(bytes)
    if not bytes:
        # 不写这个,如果客户端故意  header 传入的长度 > body,会无限循环?
        break
    data.append(bytes)

疑问 1:网上看到的 recv(4)好像都是直接写的,请问这个 recv(4)有必要向下面一样 while 吗? ps.我觉得可能还是要 socket.recv

为了看看 python 自己怎么写的,以 3.7.9 python 为例,我看了 wsgiref.WSGIRequestHandler, 简单逻辑如下

# 在 accept 拿到 client_socket 之后
self.connection = client_socket
self.rbufsize = -1
self.rfile = self.connection.makefile('rb', self.rbufsize)
self.raw_requestline = self.rfile.readline(65537)

发现 http header 的处理好像都是直接用的 readline,然后我看 readline 的解释是会保证正确的按行读取.

疑问 2:如果我也用 makefile('rb', -1)的方式,那么 self.rfile.read(n) 是不是可信的 n 字节数据呢?

是否要像 sock.recv 一样自己多次调用?还是说不用[header][body]这种固定字节处理 header, 转而使用 header\nbody 这种,也像 http 处理一样直接 readline

2714 次点击
所在节点    Python
15 条回复
GM
2021-02-24 10:16:53 +08:00
你这种已经是上层应用了,我感觉 sock.recv 已经在内部帮你处理好了所谓的“粘包”问题了,你要 sock.recv(N) 个字节,它已经保证能给你返回 N 个字节给你了,这种情况下,根本不存在你说是的“粘包”问题了
GM
2021-02-24 10:19:02 +08:00
操作系统的 recv 函数,每次调用返回的时候,是无法保证读取到数据长度的,有可能这次返回 1 个字节,有可能下次返回 1000 个字节(操作系统会把实际读取到的数据长度作为返回值返回给调用者),这个就是所谓的“粘包”问题的根本原因所在。
todd7zhang
2021-02-24 10:20:30 +08:00
@GM 文档 socket.recv(bufsize[, flags]) Receive data from the socket. The return value is a bytes object representing the data received. The maximum amount of data to be received at once is specified by bufsize. See the Unix manual page recv(2) for the meaning of the optional argument flags; it defaults to zero. 因为这里只是说返回数据最大不超过 bufsize, 所以我也不知道到底是不是明确返回 bufsize 大小呀
fengjianxinghun
2021-02-24 11:04:03 +08:00
```
struct sock_recv {
char *cbuf;
Py_ssize_t len;
int flags;
Py_ssize_t result;
};

static int
sock_recv_impl(PySocketSockObject *s, void *data)
{
struct sock_recv *ctx = data;

#ifdef MS_WINDOWS
if (ctx->len > INT_MAX)
ctx->len = INT_MAX;
ctx->result = recv(s->sock_fd, ctx->cbuf, (int)ctx->len, ctx->flags);
#else
ctx->result = recv(s->sock_fd, ctx->cbuf, ctx->len, ctx->flags);
#endif
return (ctx->result >= 0);
}

```
MSG_WAITALL (since Linux 2.2)
This flag requests that the operation block until the full request is satisfied. However, the call may
still return less data than requested if a signal is caught, an error or disconnect occurs, or the next
data to be received is of a different type than that returned. This flag has no effect for datagram
sockets.``

```

```
sock.recv(4, socket.MSG_WAITALL )
```
todd7zhang
2021-02-24 11:16:17 +08:00
@fengjianxinghun 谢谢。所以这个意思是,除非异常或者客户 client close 了,recv(n) 通常都是返回 n bytes 大小的数据吗
fengjianxinghun
2021-02-24 11:21:00 +08:00
@todd7zhang 不是,要加 socket.MSG_WAITALL
todd7zhang
2021-02-24 14:46:35 +08:00
然后,我还测试了一下
rfile = makefile('rb', -1)
rfile.read(min((length, 4096)))
读取 body 的时候,每次都会阻塞的返回 4096 长度的 bytes, 除非最后的数据<4096 。
socket.recv 不是
LeeReamond
2021-02-25 01:09:39 +08:00
想请问一下 lz,以前想用 socket 实现一个简单的 rpc,但是遇到问题。从效率角度讲,最好两者之间建立一次连接后可以一直使用,不需要重连。那么假设客户端向服务端发出两次请求,内容都比较长,需要拆分成多个封包发送,如果因为网络延迟,可能导致服务端收到的两个请求的封包的内容掺杂起来,这种情况应该如何解决呢
todd7zhang
2021-02-25 08:59:54 +08:00
@LeeReamond 你就可以用我上面的代码啊,每次请求都是[header][body],这样才能处理好两次请求体的边界问题嘛

server.py 在 print("Got data %s" % b''.join(res)) 和 sock.sendall(b'Got it %d' % len(b''.join(res))) 中间插入你的 server 处理业务逻辑就行。

client.py 中:s.connect(('localhost', 20080)) 和 s.shutdown(1) 中间是发送一次请求,多次就在中间插入就行了
todd7zhang
2021-02-25 09:19:46 +08:00
@LeeReamond 没看到你的长链接要求,这里我写了一个 https://paste.ubuntu.com/p/qcVf2rYZYM/
LeeReamond
2021-02-25 11:35:28 +08:00
@todd7zhang 感谢回复,我看了一下你的代码,感觉跟我说的不太一样,可能我没有描述清楚。我预想中的情况是,即使使用 header 标明长度,假设 client 在同一个连接中发出两次请求,分别为 herder = 102400 & 102400*'i',即长度为 102400 的 i 字符,且带了一个描述长度的标头。之后又发送了一个 herder = 102400 & 102400*'j'的请求,即将上个请求中所有的 i 转换成 j,由于这两个请求都较长,会被拆成多次发送。

那么服务端第一次接收到 102400header 后,会读取接下来 102400 个字节作为一个请求。但接下来 102400 中未必是连续的请求 1,可能掺杂入请求 2 的内容,这种错误可能由网络波动导致,我不知道如何在本地模拟,本地由于没有网络延迟,一般 clinet 连续发出两个请求,server 也就连续收到两个请求,不太容易产生错位的情况
julyclyde
2021-02-25 12:07:04 +08:00
考虑到 wsgiref 不支持 http pipeline,这么读是不会读入“下一个请求”的
todd7zhang
2021-02-25 13:11:26 +08:00
@LeeReamond 这就不可能了吧,tcp 协议是控制了的,接收方收到的数据是肯定不会乱序的。

我猜测你说的这种情况最可能是

本意为了发送 102400 & 102400*i 和 102400 & 102400*j,
但是你客户端发送的代码没有写好导致服务端收到 102400 & 102300*i 和 102400 & 102400*j 。
这种情况可能就是你每次发送用的 socket.send 而不是 socket.sendall,sendall 是会多次调用 send 确保数据完全发完的
LeeReamond
2021-02-26 00:44:04 +08:00
@todd7zhang 我其实不是很理解 tcp 的有序性,因为之前朦胧印象中实验结果与理论上不符,不过因为是很久远以前的事了也不是记得很清楚。

所以理论上如果仍然发出上述两个长请求,如果请求 1 中的某一个封包因为网络波动丢掉了,TCP 是会自动重传,再补传的封包收到前,无论 server 读取多少次,都不能从 recv 读取任何东西是吗(即使网卡已经收到了后续的封包)。

我印象中我以前的实验是 recv 始终能读出东西,丢掉的封包会被跳过,很神秘
ClericPy
2022-12-05 23:24:40 +08:00
偶然搜到这个问题, 楼主提到的

疑问 1:网上看到的 recv(4)好像都是直接写的,请问这个 recv(4)有必要向下面一样 while 吗?



不是故意挖坟, 最近在搞 asyncio 处理 logger 的 SocketHandler, 官网给的文档和楼主 append 里面差不多, if len(header_bytes) < 4 就跳出

但是又偶然读到官网这段 https://docs.python.org/zh-cn/3/howto/sockets.html#using-a-socket

"以其长度(例如,作为 5 个数字字符)作为消息前缀时会变得更复杂,因为(信不信由你)你可能无法在一个 recv 中获得所有 5 个字符。在一般使用时,你会侥幸避免该状况;但是在高网络负载中,除非你使用两个 recv 循环,否则你的代码将很快中断 —— 第一个用于确定长度,第二个用于获取消息的数据部分。这很讨厌。当你发现 send 并不总是设法在支持搞定一切时,你也会有这种感觉。 尽管已经阅读过这篇文章,但最终还是会有所了解!"

如果不想弄丢日志, 这套方案还可行吗, 因为协程里的 read() 确实是读到不超过那个长度的结果. 虽然精确 read 可以读到完整

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/755712

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX