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

C#如何使用UDPClient传输大文件实现高速实时图像流传输

非TCP低延迟屏幕图像传输实现方案

你当前TCP版本延迟高的核心原因是TCP的可靠按序交付机制和实时流媒体需求天然冲突:TCP要求所有数据包按序、无丢失到达,一旦传输过程中丢包,后续已经收到的帧数据会全部阻塞在缓冲区,等待丢失的包重传完成才能交付给应用层,延迟会越积越高,很容易涨到数百毫秒,完全不满足屏幕传输的低延迟要求。

这类场景直接用UDP实现即可,不需要复杂的可靠传输逻辑——实时屏幕流的核心优先级是拿到最新的帧,老帧丢失不需要重传,直接丢弃追新帧就好,少量丢帧肉眼几乎无法感知,换UDP后局域网内传输延迟可以稳定压到10~20ms,比TCP实现低一个数量级。

核心实现要点

  • 单UDP包大小控制在1400字节以内(公网环境建议设为1200字节),避免IP层分片导致的丢包,不要超过链路MTU值。
  • 每帧加轻量包头标记:4字节递增帧序号 + 4字节当前帧总长度 + 1字节末包标记,不需要加复杂校验逻辑。
  • 客户端收包时设置30ms超时,超过时间没收到当前帧的后续包直接丢弃残缺帧缓存,直接接收最新帧,绝对不要做重传请求,避免引入队头阻塞。
  • JPEG压缩质量调整到5070区间,肉眼几乎看不出画质损失,单帧大小可以压缩到100300KB,千兆局域网下完全不会出现带宽瓶颈;如果对CPU占用敏感,可以换LZ4等帧内快速压缩算法,编码速度比JPEG高3~5倍。
  • 可选优化:增加帧差检测逻辑,只截取传输屏幕发生变化的矩形区域,传输数据量可以降低70%以上,延迟还能进一步下降。

核心参考代码

服务端发送逻辑

private const int MaxUdpPacketSize = 1400;
private const int FrameHeaderSize = 9; // 4字节帧序号 + 4字节帧总长 + 1字节末包标记
private int _frameSeq = 0;
private readonly UdpClient _udpSender = new UdpClient();
// 替换为实际客户端IP和端口
private readonly IPEndPoint _clientEp = new IPEndPoint(IPAddress.Parse("192.168.1.100"), 12345);

// 独立线程运行
private void SendImage()
{
    _udpSender.Connect(_clientEp);
    while (Status == ServerStatus.Connected)
    {
        try
        {
            using Bitmap bitmap = Desktop.TakeScreenShoot();
            using MemoryStream ms = new MemoryStream();
            // 配置JPEG压缩质量为60,平衡画质和体积
            ImageCodecInfo jpgEncoder = GetEncoder(ImageFormat.Jpeg);
            EncoderParameters encoderParams = new EncoderParameters(1);
            encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 60L);
            bitmap.Save(ms, jpgEncoder, encoderParams);
            
            byte[] frameData = ms.ToArray();
            int frameTotalLen = frameData.Length;
            int currentSeq = Interlocked.Increment(ref _frameSeq);
            int offset = 0;
            
            // 切片分包发送
            while (offset < frameTotalLen)
            {
                int chunkSize = Math.Min(MaxUdpPacketSize - FrameHeaderSize, frameTotalLen - offset);
                bool isLastChunk = (offset + chunkSize) == frameTotalLen;
                byte[] packet = new byte[chunkSize + FrameHeaderSize];
                
                // 写入帧头
                BitConverter.GetBytes(currentSeq).CopyTo(packet, 0);
                BitConverter.GetBytes(frameTotalLen).CopyTo(packet, 4);
                packet[8] = isLastChunk ? (byte)1 : (byte)0;
                // 写入分片数据
                Array.Copy(frameData, offset, packet, FrameHeaderSize, chunkSize);
                
                _udpSender.Send(packet, packet.Length);
                offset += chunkSize;
            }
        }
        catch
        {
            // 发送异常直接忽略,不重试
        }
    }
}

private ImageCodecInfo GetEncoder(ImageFormat format)
{
    ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
    foreach (ImageCodecInfo codec in codecs)
    {
        if (codec.FormatID == format.Guid) return codec;
    }
    return null;
}

客户端接收逻辑

private readonly UdpClient _udpReceiver = new UdpClient(12345);
private int _currentExpectedSeq = 0;
private byte[] _currentFrameBuffer = null;
private int _currentFrameReceivedLen = 0;
private const int FrameDropTimeoutMs = 30;
private DateTime _lastPacketTime = DateTime.Now;

// 独立线程运行
private void ReceiveLoop()
{
    while (Status == ServerStatus.Connected)
    {
        try
        {
            IPEndPoint remoteEp = new IPEndPoint(IPAddress.Any, 0);
            byte[] packet = _udpReceiver.Receive(ref remoteEp);
            
            // 超时判定:超过阈值没收到包,直接丢弃当前残缺帧追新帧
            if ((DateTime.Now - _lastPacketTime).TotalMilliseconds > FrameDropTimeoutMs)
            {
                _currentFrameBuffer = null;
                _currentFrameReceivedLen = 0;
            }
            _lastPacketTime = DateTime.Now;
            
            // 解析包头
            int seq = BitConverter.ToInt32(packet, 0);
            int frameTotalLen = BitConverter.ToInt32(packet, 4);
            bool isLastChunk = packet[8] == 1;
            byte[] chunkData = packet.Skip(9).ToArray();
            
            // 收到新帧,重置缓存
            if (seq != _currentExpectedSeq || _currentFrameBuffer == null)
            {
                _currentExpectedSeq = seq;
                _currentFrameBuffer = new byte[frameTotalLen];
                _currentFrameReceivedLen = 0;
            }
            
            // 写入分片数据到帧缓存
            Array.Copy(chunkData, 0, _currentFrameBuffer, _currentFrameReceivedLen, chunkData.Length);
            _currentFrameReceivedLen += chunkData.Length;
            
            // 收齐完整帧,触发渲染
            if (isLastChunk && _currentFrameReceivedLen == frameTotalLen)
            {
                using MemoryStream ms = new MemoryStream(_currentFrameBuffer);
                Image frame = Image.FromStream(ms);
                ImageRecibed?.Invoke(frame);
                // 重置缓存准备收下一帧
                _currentFrameBuffer = null;
                _currentFrameReceivedLen = 0;
                _currentExpectedSeq++;
            }
        }
        catch
        {
            // 解析/收包异常直接重置缓存,不做重试
            _currentFrameBuffer = null;
            _currentFrameReceivedLen = 0;
        }
    }
}

补充说明

  • 不要给UDP额外增加可靠传输、按序重传的逻辑,一旦加入这类机制就会重新引入TCP的队头阻塞问题,完全失去换UDP的意义。
  • 如果是跨公网传输,可以额外加简单的FEC前向纠错(比如每发10个数据包附带1个冗余校验包),可以在不引入重传的前提下大幅降低丢包导致的花屏概率。

内容的提问来源于stack exchange,提问作者Luis Garcia

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 20:03:23