You need to enable JavaScript to run this app. C# Socket传输服务器截图至客户端时第二张图不刷新、接收后卡顿问题
优惠活动
大模型
产品
解决方案
定价
更多

C# Socket传输服务器截图至客户端时第二张图不刷新、接收后卡顿问题

现存问题的核心根因
  • TCP流式无边界导致的粘包/拆包解析失败
    TCP是面向字节流的协议,本身不携带消息边界标识,当前代码没有任何长度头校验,第一次接收时刚好完整读到第一张图的字节就能正常解析,第二张图传输时很容易因为流中残留上一次的无效字节、或两次发送的数据包粘连,导致读到的字节流不是完整的单张图数据,Bitmap.FromStream() 解析失败就会中断后续逻辑,不会触发控件刷新。
    另外你固定申请了1MB的接收缓冲区,每次接收时不管实际拿到多少字节,直接把整个缓冲区丢给内存流解析,残留的旧数据会直接混在新的图像流头部,根本无法正常解码。

  • UI线程被阻塞导致界面无响应
    你把Socket阻塞接收逻辑直接写在按钮点击事件(UI线程)里,不管是单次接收还是注释里的无限循环,只要UI线程卡在client.Receive() 等待数据的状态,就无法处理窗口重绘、前台切换、控件更新等UI消息,自然会出现窗口卡死后切不回来、PictureBox不刷新的情况。
    而且当前接收逻辑是绑定在按钮点击上的,第二张图到达时你没有再次触发按钮事件,Receive() 根本不会执行,控件自然不会更新。

  • 资源泄漏导致的渲染异常
    每次给PictureBox赋新图片时,没有主动释放上一次的Image GDI资源,长期运行会占满系统绘图资源,触发控件渲染失败。
    服务端截图后先存本地文件再读字节的逻辑属于冗余IO,不仅拖慢速度,还会因为文件读写占用导致发送时机异常。

可落地的修复方案

首先需要定义极简的传输协议,彻底解决粘包问题:

每次传输分两步:先发送4字节的图片实际长度(转网络字节序),再发送完整的图片字节内容,客户端按这个规则分段读取,就不会出现拆包读不全、粘包读混的问题。

服务端代码修正点

  • 去掉冗余的临时文件读写,直接把内存中的Bitmap转成JPEG格式的字节数组
  • 按照协议先传长度、再传图片内容,循环调用Send() 确保所有字节全部发送完成,避免一次Send只发了部分数据
  • 核心修正代码:
private void button2_Click(object sender, EventArgs e)
{
    if (client == null || !client.Connected) return;
    // 截图资源用using自动释放
    using var bmp = TakingScreenshotEx1();
    using var ms = new MemoryStream();
    // 直接内存转JPEG,绕开临时文件IO
    bmp.Save(ms, ImageFormat.Jpeg);
    byte[] imgBytes = ms.ToArray();
    
    // 转4字节网络序的长度头,兼容后续Android端跨平台解析
    byte[] lenBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(imgBytes.Length));
    client.Send(lenBytes);
    
    // 循环确保图片全部发送完成
    int totalSent = 0;
    while (totalSent < imgBytes.Length)
    {
        totalSent += client.Send(imgBytes, totalSent, imgBytes.Length - totalSent, SocketFlags.None);
    }
    Console.WriteLine("Image SENT!");
}

客户端代码修正点

  • 把接收逻辑挪到独立后台线程,绝对不能阻塞UI线程
  • 严格按照长度协议分段读取,读够指定长度的字节再去解析图片,不要用整块大缓冲区直接解析
  • 解析完图片后,通过UI线程的Invoke方法切回主线程更新PictureBox,禁止跨线程直接操作控件
  • 更新图片时主动释放旧的Image资源,避免GDI泄漏
  • 窗口关闭时主动停止接收循环、释放Socket连接
  • 核心修正代码:
private CancellationTokenSource receiveCts;
private void Form1_Load(object sender, EventArgs e)
{
    pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
    if (!client.Connected)
    {
        client.Connect(iep);
    }
    // 启动后台接收任务,不阻塞UI
    receiveCts = new CancellationTokenSource();
    _ = Task.Run(() => ReceiveLoop(receiveCts.Token));
}

private void ReceiveLoop(CancellationToken ct)
{
    byte[] lenBuffer = new byte[4];
    try
    {
        while (!ct.IsCancellationRequested)
        {
            // 第一步:读够4字节的长度头
            int readCount = 0;
            while (readCount < 4)
            {
                int receiveLen = client.Receive(lenBuffer, readCount, 4 - readCount, SocketFlags.None);
                if (receiveLen == 0) return; // 连接对端断开,退出循环
                readCount += receiveLen;
            }
            // 网络序转主机序,得到单张图片的实际长度
            int imageLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(lenBuffer, 0));
            
            // 第二步:按长度读够完整的图片字节
            byte[] imageBuffer = new byte[imageLength];
            readCount = 0;
            while (readCount < imageLength)
            {
                int receiveLen = client.Receive(imageBuffer, readCount, imageLength - readCount, SocketFlags.None);
                if (receiveLen == 0) return;
                readCount += receiveLen;
            }
            
            // 第三步:切回UI线程更新图片
            Invoke(() => {
                // 释放旧图片资源
                if (pictureBox1.Image != null)
                {
                    var oldImage = pictureBox1.Image;
                    pictureBox1.Image = null;
                    oldImage.Dispose();
                }
                // 用收到的实际图片字节构建Image
                using var ms = new MemoryStream(imageBuffer);
                pictureBox1.Image = Image.FromStream(ms);
            });
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"接收异常: {ex.Message}");
    }
}

// 窗口关闭时清理资源
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    receiveCts?.Cancel();
    client.Close();
}

后续Android适配注意事项

  • 上述后台接收、跨线程更新UI的逻辑本身就符合Android的开发规范(Android本身就禁止在主线程做网络操作,也要求必须在主线程更新控件),传输层的逻辑可以1:1复用
  • Android端的屏幕截图需要单独申请悬浮窗、媒体投影权限,和Windows端的截图实现逻辑不同,不需要改动现有传输协议
  • Android端更新控件时用RunOnUiThread() 替代Windows端的Invoke() 即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.24 23:15:03