Trio send_all 没报错却少发数据,检查 memoryview 的 itemsize
Trio 0.34.0 的发送重试会混用字节数与 memoryview 元素偏移。对已确认连续内存传入一维字节视图,并校验接收内容与在途消息。
把一段连续内存转换成 memoryview(...).cast("H") 后交给 Trio send_all(),调用正常返回,接收端却少了一部分数据。这个条件下应检查发送缓冲区的元素大小,以及底层是否发生了部分写入。
Trio 0.34.0 的 SocketStream.send_all 和 POSIX FdStream.send_all,仍把系统调用返回的字节数用于 memoryview 的切片偏移。对于 itemsize > 1 的视图,字节计数与元素索引不一致,部分写入后的重试可能跳过数据。一次性写完的小样本不一定暴露这个问题。
先把字节数和元素数分开
例如,一个 H 视图的元素通常占两个字节,len(view) 与 view.nbytes 含义不同。系统调用报告发送了多少字节,而 view[sent:] 按元素偏移;若混用这两个单位,后续切片就可能越过原本还没发送的区域。
先记录视图的 format、itemsize、ndim、连续性与 nbytes,并让受控接收端核对完整长度和内容。不要只比较发送调用是否抛异常,也不要在日志中输出真实业务载荷。少发的具体比例受系统写入大小影响,不应把某次“刚好一半”当作固定规律。
传入一维字节视图
对于已经确认的 C 连续内存,可以在调用边界转换成一维 B 视图,让切片索引和字节计数一致。应用已有连续缓冲区 payload 时,可以把发送位置改成:
with memoryview(payload) as original:
if not original.c_contiguous:
raise ValueError("expected a C-contiguous buffer")
with original.cast("B") as byte_view:
await stream.send_all(byte_view)
stream 是应用已经建立、由当前任务管理的 Trio 流,代码位于 async 函数中。视图必须保持到 await 完成;发送期间不能让其他任务修改其底层可写内存。转换视图不会替你解决并发修改或缓冲区生命周期问题。
如果业务本来就有一份 bytes,直接发送它也能避免这种多字节元素索引的情况。不连续视图需要先确定正确的元素排列和协议字节序,再评估是否复制;不能不加判断地把所有数组转换成原始内存后发出去。cast("B") 只改变观察内存的方式,不是网络字节序转换或通用序列化。
多维视图和零维视图还涉及长度、切片规则。不要依赖恰好能在某个小输入上通过的形状;上面的调用只接受已确认的 C 连续内存,并显式展平成字节视图。
处理已发生的截断,不能盲目重发
调用返回成功不意味着对端已经收到完整业务消息。若协议包含长度、校验或操作编号,先用它核对完整消息和处理状态。对端可能已收到前半段,或已经开始执行有副作用的操作;直接在同一连接上重发整段,可能把残余字节与新消息拼在一起。
恢复方式应遵守原有协议的断帧、连接重建与幂等规则。保留可重放的原始输入及发送记录,不能靠清空接收缓存掩盖已丢失的数据。需要确认修复时,在隔离管道或受控服务中安排足够大的载荷及背压,让部分写入路径得到覆盖,比较完整字节内容,而不是只看日志里的长度。
正式 0.34.0 的这两处实现仍可核到相同单位混用。后续升级应检查具体正式版本与同一接收校验,不把提交了补丁等同于发行修复。回退字节视图调整前,也要保留接收端对旧格式和在途消息的处理约定。


