You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
}

额外优化建议

  1. 图片序列化优化:用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 };
}
  1. 流缓冲检查:如果使用BufferedStream包装NetworkStream,确保调用Flush()强制发送缓冲数据;NetworkStream默认无缓冲,Flush()可省略,但保留也无影响。

内容的提问来源于stack exchange,提问作者Lior v

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 17:05:32