TCP Socket传输数MB文件时出现死锁的排查与修复方法
问题根因
你遇到的挂起问题不是死锁,核心原因有两个,其中第二个完全匹配你描述的现象:
- 多线程竞态读网络流(最高概率):如果服务端存在独立的消息监听逻辑、其他线程同时从同一个
NetworkStream实例读取数据,开启NoDelay分块传输产生大量小包时,部分文件内容字节会被其他读取逻辑抢占,导致文件接收逻辑永远凑不够约定的length总字节数,最后阻塞在Read调用上。而缓冲区调大到能一次性装下整个文件时,所有文件数据会连续到达接收缓冲区,文件接收逻辑会一次性读走全部数据,其他线程没有抢占机会,所以不会挂起;小于缓冲区的小文件同理,不会出现抢占问题。 - 无发送完成标记+无超时兜底:客户端发完所有数据后没有调用
Shutdown(SocketShutdown.Send)通知服务端“后续不会再有发送数据”,且服务端读取操作没有设置超时,一旦出现字节数不匹配的情况,Read会无限阻塞。
代码隐患
你当前的实现还有几个容易触发问题的隐患:
- 客户端用同步
Write方法写网络流,没有配合异步上下文使用WriteAsync,在Xamarin的UI同步上下文下很容易出现发送线程被阻塞,导致最后一块数据未实际发出的问题。 - 纯字节传输场景下不需要用
BinaryWriter/BinaryReader做包装,这两个类的内部缓冲、类型序列化前缀逻辑很容易引入隐蔽的字节计数错误,比如之前用它们写入长度字段、字符串时,额外的前缀字节很容易打乱你约定的长度计数。 - 两端都没有对本地流的短读做兼容处理:Xamarin上读取系统相册、沙盒外的文件流时,
ReadAsync经常会返回小于请求长度的字节数(未到流末尾),虽然你当前的外层循环能处理网络流的短读,但如果本地流的Length属性返回值和实际可读字节数不一致,就会出现服务端等待不存在的字节的问题。
修复方案
- 首先排查服务端的流读取逻辑,确保文件接收期间,对应
NetworkStream的读取操作完全由receiveBigFile独占,文件内容接收完成前禁止其他线程调用该流的Read方法。 - 客户端发完所有文件数据后,立刻调用底层Socket的Shutdown方法通知服务端发送完成:
这样服务端如果收完所有数据后Read返回0,但// 放在SendBigFile方法的using块外部,所有数据写完后调用 this.ns.Socket.Shutdown(SocketShutdown.Send);bytesLeft仍大于0,可以直接抛出传输不完整的异常,不会无限挂起。 - 给服务端的网络流设置读取超时做兜底,避免无限阻塞:
// 初始化ns之后设置,超时时间按业务需求调整,单位毫秒 this->ns->ReadTimeout = 30000; - 把客户端的同步写入改成异步写入,适配Xamarin的异步上下文:
// 替换原来的bw.Write和bw.Flush await bw.BaseStream.WriteAsync(data, 0, bytesRead); await bw.BaseStream.FlushAsync(); - 可以去掉
BinaryWriter/BinaryReader的包装,直接操作NetworkStream做字节读写,减少中间层带来的不确定问题。 - 建议增加传输校验:客户端发完文件内容后追加4字节的CRC32校验值,服务端收完后比对校验值,及时发现传输损坏、字节丢失的问题。
内容的提问来源于stack exchange,提问作者NightCity10932
相关产品推荐
相关产品推荐

