TCP流传输图片:仅关闭客户端时服务器才接收数据问题排查
问题:TCP发送图片时服务器仅在客户端关闭后才接收数据
我尝试通过TCP流从客户端向服务器发送图片数据包,客户端连接服务器后立即发送图片,但服务器只有在客户端关闭连接时才能接收到完整图片,接收时机和客户端关闭完全同步。使用ProtoBuf-Net做序列化和反序列化,相关代码如下:
客户端代码
// Connect to the client client.Connect(Client.SERVER_IP, 1729); // Capture screenshot Bitmap captureBitmap = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); System.Drawing.Rectangle captureRectangle = Screen.PrimaryScreen.Bounds; Graphics captureGraphics = Graphics.FromImage(captureBitmap); captureGraphics.CopyFromScreen(captureRectangle.Left, captureRectangle.Top, 0, 0, captureRectangle.Size); // Serialize the screenshot to the socket's stream ImageConverter img = new ImageConverter(); Packet<byte[]> packet = new Packet<byte[]> { value = (byte[])img.ConvertTo(captureBitmap, typeof(byte[])), type = PacketType.IMAGE }; Serializer.SerializeWithLengthPrefix(stream, packet.GetType().AssemblyQualifiedName, PrefixStyle.Base128); Serializer.Serialize(stream, packet); stream.Flush();
服务器代码
ImageConverter imageConverter = new ImageConverter(); // Wait for client to connect var client = new ExtendedTcpClient(listener.AcceptTcpClient()); currentClientControlling = client; // Deserialize type var typeName = Serializer.DeserializeWithLengthPrefix<string>(stream, PrefixStyle.Base128); var type = Type.GetType(typeName); // Register the type var model = RuntimeTypeModel.Default; model.Add(type, true); // Deserialize the data var bytes = model.Deserialize(stream, null, type); var image = (Bitmap)imageConverter.ConvertFrom(bytes);
Packet模型
public enum PacketType { IMAGE } [ProtoContract] public class Packet<T> { [ProtoMember(1)] public PacketType type { get; set; } [ProtoMember(2)] public T value { get; set; } }
解决方案
核心原因
TCP是字节流协议,没有天然的消息边界。服务器端的Deserialize方法会一直等待流的结束信号(客户端关闭连接时发送的FIN包),才会认为数据接收完成,因此出现"客户端关闭才同步接收"的现象。
修复方案1:用长度前缀标记数据包边界(推荐)
直接使用SerializeWithLengthPrefix序列化整个Packet对象,让ProtoBuf-Net自动添加长度前缀,服务器通过前缀即可知道数据包的完整大小,无需等待流关闭。
修改客户端代码
替换原有的两次Serialize调用:
// 直接序列化带长度前缀的Packet对象 Serializer.SerializeWithLengthPrefix(stream, packet, PrefixStyle.Base128); stream.Flush();
修改服务器代码
直接反序列化带长度前缀的Packet<byte[]>:
// 无需手动传递类型名,直接反序列化指定类型 var packet = Serializer.DeserializeWithLengthPrefix<Packet<byte[]>>(stream, PrefixStyle.Base128); var image = (Bitmap)imageConverter.ConvertFrom(packet.value);
修复方案2:手动传递数据长度(兼容原有类型传递逻辑)
如果需要保留"先传类型名再传数据包"的逻辑,可手动发送每个部分的长度,让服务器按固定顺序读取:
客户端修改
// 发送类型名(先传长度,再传内容) byte[] typeBytes = Encoding.UTF8.GetBytes(packet.GetType().AssemblyQualifiedName); stream.Write(BitConverter.GetBytes(typeBytes.Length), 0, 4); stream.Write(typeBytes, 0, typeBytes.Length); // 序列化Packet到内存流,获取字节数组 using (var ms = new MemoryStream()) { Serializer.Serialize(ms, packet); byte[] packetBytes = ms.ToArray(); // 发送数据包长度+内容 stream.Write(BitConverter.GetBytes(packetBytes.Length), 0, 4); stream.Write(packetBytes, 0, packetBytes.Length); } stream.Flush();
服务器修改
// 读取类型名长度 byte[] typeLenBuf = new byte[4]; stream.Read(typeLenBuf, 0, 4); int typeLen = BitConverter.ToInt32(typeLenBuf, 0); // 读取类型名 byte[] typeBytes = new byte[typeLen]; stream.Read(typeBytes, 0, typeLen); string typeName = Encoding.UTF8.GetString(typeBytes); var type = Type.GetType(typeName); // 读取数据包长度 byte[] packetLenBuf = new byte[4]; stream.Read(packetLenBuf, 0, 4); int packetLen = BitConverter.ToInt32(packetLenBuf, 0); // 读取数据包内容 byte[] packetBytes = new byte[packetLen]; stream.Read(packetBytes, 0, packetLen); // 反序列化数据包 using (var ms = new MemoryStream(packetBytes)) { var model = RuntimeTypeModel.Default; model.Add(type, true); var packet = model.Deserialize(ms, null, type); var imgBytes = ((Packet<byte[]>)packet).value; var image = (Bitmap)imageConverter.ConvertFrom(imgBytes); }
额外优化建议
- 图片序列化优化:用
Bitmap.Save直接保存为PNG/JPG格式到内存流,比ImageConverter更高效且数据量更小:
using (var ms = new MemoryStream()) { captureBitmap.Save(ms, ImageFormat.Png); byte[] imgBytes = ms.ToArray(); Packet<byte[]> packet = new Packet<byte[]> { value = imgBytes, type = PacketType.IMAGE }; }
- 流缓冲检查:如果使用
BufferedStream包装NetworkStream,确保调用Flush()强制发送缓冲数据;NetworkStream默认无缓冲,Flush()可省略,但保留也无影响。
内容的提问来源于stack exchange,提问作者Lior v
相关产品推荐
相关产品推荐

