讨论一下 Python socket 该怎么正确的读所谓的"粘包", 主要是 Python 语言的细节 - V2EX
V2EX = way t explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
todd7zhang

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

  •  
  •   todd7zhang Feb 24, 2021 3173 views
    This topic created in 1887 days ago, the information mentioned may be changed or developed.

    先声明,我知道没有粘包这种东西,关键是自己应用层处理好 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

    Supplement 1    Feb 24, 2021

    感谢各位,最后代码如下。

    # server.py import socket import struct s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('localhost', 20080)) s.listen(5) def handle(sock): header_bytes = sock.recv(4, socket.MSG_WAITALL) # 这里强制阻塞读取全部4字节 if len(header_bytes) != 4: print("header bytes not valid") sock.close() return print('header bytes %s' % header_bytes) length = struct.unpack('>i', header_bytes)[0] print('unpack body length %d' % length) res = [] while length: read_length = min((length, 4096)) data = sock.recv(read_length) print(' --> now read', len(data), data) if not data: print("body size < header claimed") sock.close() return res.append(data) length -= len(data) print("Got data %s" % b''.join(res)) sock.sendall(b'Got it %d' % len(b''.join(res))) sock.close() print("close") while 1: c, addr = s.accept() print("Accept from (%s,%s)" % addr) handle(c) # client.py #coding: utf-8 import socket import time import struct import random s = socket.socket() s.connect(('localhost', 20080)) kk = [1, 4, 6, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 19201, 4097, 4095, 3302, 1234] n = sum(kk) data = b'i' * n byte = struct.pack('>i', n) print('will total sent', n) for i in range(4): s.sendall(byte[i:i+1]) # 4字节分批模拟交通阻塞 time.sleep(random.random()) send = 0 for v in kk: s.sendall(data[send:send+v]) send += v print('send', send, '+', v) time.sleep(andom.random()) s.shutdown(1) print("Got", s.recv(1024)) 
    15 replies    2022-12-05 23:24:40 +08:00
    GM
        1
    GM  
       Feb 24, 2021
    你这种已经是上层应用了,我感觉 sock.recv 已经在内部帮你处理好了所谓的“粘包”问题了,你要 sock.recv(N) 个字节,它已经保证能给你返回 N 个字节给你了,这种情况下,根本不存在你说是的“粘包”问题了
    GM
        2
    GM  
       Feb 24, 2021
    操作系统的 recv 函数,每次调用返回的时候,是无法保证读取到数据长度的,有可能这次返回 1 个字节,有可能下次返回 1000 个字节(操作系统会把实际读取到的数据长度作为返回值返回给调用者),这个就是所谓的“粘包”问题的根本原因所在。
    todd7zhang
        3
    todd7zhang  
    OP
       Feb 24, 2021
    @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
        4
    fengjianxinghun  
       Feb 24, 2021
    ```
    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
        5
    todd7zhang  
    OP
       Feb 24, 2021
    @fengjianxinghun 谢谢。所以这个意思是,除非异常或者客户 client close 了,recv(n) 通常都是返回 n bytes 大小的数据吗
    fengjianxinghun
        6
    fengjianxinghun  
       Feb 24, 2021
    @todd7zhang 不是,要加 socket.MSG_WAITALL
    todd7zhang
        7
    todd7zhang  
    OP
       Feb 24, 2021
    然后,我还测试了一下
    rfile = makefile('rb', -1)
    rfile.read(min((length, 4096)))
    读取 body 的时候,每次都会阻塞的返回 4096 长度的 bytes, 除非最后的数据<4096 。
    socket.recv 不是
    LeeReamond
        8
    LeeReamond  
       Feb 25, 2021 via Android
    想请问一下 lz,以前想用 socket 实现一个简单的 rpc,但是遇到问题。从效率角度讲,最好两者之间建立一次连接后可以一直使用,不需要重连。那么假设客户端向服务端发出两次请求,内容都比较长,需要拆分成多个封包发送,如果因为网络延迟,可能导致服务端收到的两个请求的封包的内容掺杂起来,这种情况应该如何解决呢
    todd7zhang
        9
    todd7zhang  
    OP
       Feb 25, 2021
    @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
        10
    todd7zhang  
    OP
       Feb 25, 2021
    @LeeReamond 没看到你的长链接要求,这里我写了一个 https://paste.ubuntu.com/p/qcVf2rYZYM/
    LeeReamond
        11
    LeeReamond  
       Feb 25, 2021
    @todd7zhang 感谢回复,我看了一下你的代码,感觉跟我说的不太一样,可能我没有描述清楚。我预想中的情况是,即使使用 header 标明长度,假设 client 在同一个连接中发出两次请求,分别为 herder = 102400 & 102400*'i',即长度为 102400 的 i 字符,且带了一个描述长度的标头。之后又发送了一个 herder = 102400 & 102400*'j'的请求,即将上个请求中所有的 i 转换成 j,由于这两个请求都较长,会被拆成多次发送。

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

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

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

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

    我印象中我以前的实验是 recv 始终能读出东西,丢掉的封包会被跳过,很神秘
    ClericPy
        15
    ClericPy  
       Dec 5, 2022
    偶然搜到这个问题, 楼主提到的

    疑问 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 可以读到完整
    About     Help     Advertise     Blog     API     FAQ     Solana     5452 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 59ms UTC 01:25 PVG 09:25 LAX 18:25 JFK 21:25
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86