如何优化C# TCP远程桌面的屏幕收发速度?
优化基于TCP的C#远程桌面传输速度的方案
问题背景
我使用C#开发基于TCP的远程桌面应用,局域网环境下运行正常,但通过VPN连接远程服务器时,图像传输耗时0.5至5秒。现有实现流程如下:
发送流程
- 使用Bitmap的
CopyFromScreen方法捕获屏幕 - 用
MemoryStream保存位图 - 通过TCP Socket序列化发送位图
接收流程
- 用
ReadBytes方法接收消息 - 用
MemoryStream存储字节数组 - 通过
Image.FromStream创建图像
现有实现代码:
Desktop类
internal static class Desktop { public static Image TakeScreenShoot() { Bitmap bitmap = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); try { using (Graphics Graphics = Graphics.FromImage(bitmap)) { Graphics.CopyFromScreen(0, 0, 0, 0, Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy); } } catch { bitmap = null; } return bitmap; } public static void SerializeScreen(Stream stream, Image Image) { MemoryStream memory = new MemoryStream(); Image.Save(memory, System.Drawing.Imaging.ImageFormat.Jpeg); int numBytes = (int)memory.Length; BinaryWriter binaryWriter = new BinaryWriter(stream); binaryWriter.Write(numBytes); binaryWriter.Write(memory.GetBuffer(), 0, numBytes); stream.Flush(); } public static Image DeserializeScreen(Stream stream) { BinaryReader binaryReader = new BinaryReader(stream); int numBytes = binaryReader.ReadInt32(); byte[] buffer = binaryReader.ReadBytes(numBytes); MemoryStream memory = new MemoryStream(buffer); return Image.FromStream(memory); } }
Host类
private void SendImage() { while (Status == ServerStatus.Connected) { try { Image bitmap = Desktop.TakeScreenShoot(); Desktop.SerializeScreen(_NetStream,bitmap); } catch { } } }
Client类
protected void ReciveMessage() { while(Status == ServerStatus.Connected) { try { ImageRecibed?.Invoke(Desktop.DeserializeScreen(_NetStream)); } catch { } } }
优化方案
1. 修复TCP流式传输的读取完整性问题
TCP是流式协议,ReadBytes(numBytes)不一定能一次性读取到完整字节数,网络不稳定时会导致数据截断,既影响解析也拖慢传输。修正DeserializeScreen方法:
public static Image DeserializeScreen(Stream stream) { using BinaryReader binaryReader = new BinaryReader(stream); int numBytes = binaryReader.ReadInt32(); byte[] buffer = new byte[numBytes]; int totalRead = 0; // 循环读取直到获取完整数据 while (totalRead < numBytes) { int readCount = binaryReader.Read(buffer, totalRead, numBytes - totalRead); if (readCount == 0) { throw new IOException("远程连接已中断"); } totalRead += readCount; } using MemoryStream memory = new MemoryStream(buffer); return Image.FromStream(memory); }
2. 优化图像压缩,减少传输字节数
默认JPEG压缩质量较高,可降低质量大幅减少数据量,同时视觉损失可控。修改SerializeScreen方法:
public static void SerializeScreen(Stream stream, Image image) { using MemoryStream memory = new MemoryStream(); // 获取JPEG编码器 ImageCodecInfo jpegCodec = ImageCodecInfo.GetImageEncoders() .FirstOrDefault(e => e.FormatID == ImageFormat.Jpeg.Guid); if (jpegCodec == null) { image.Save(memory, ImageFormat.Jpeg); } else { // 设置压缩质量(范围0-100,数值越低压缩率越高) using EncoderParameters encoderParams = new EncoderParameters(1); encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 60L); image.Save(memory, jpegCodec, encoderParams); } int numBytes = (int)memory.Length; using BinaryWriter binaryWriter = new BinaryWriter(stream); binaryWriter.Write(numBytes); // 用ToArray()替代GetBuffer(),避免发送多余的未使用缓冲区字节 binaryWriter.Write(memory.ToArray()); stream.Flush(); }
3. 只传输屏幕变化区域
全量发送整屏是最大性能瓶颈,改为只发送上一帧与当前帧的差异区域:
- 保存上一次捕获的Bitmap
- 逐块对比两帧,提取所有变化的矩形区域
- 发送区域坐标+区域图像数据
- 客户端收到后将变化区域绘制到原有图像上
简化示例思路:
private static Bitmap _lastFrame; public static List<(Rectangle Rect, Bitmap Region)> GetScreenChanges() { Bitmap currentFrame = TakeScreenShoot(); List<(Rectangle, Bitmap)> changes = new List<(Rectangle, Bitmap)>(); if (_lastFrame == null) { changes.Add((Screen.PrimaryScreen.Bounds, currentFrame)); _lastFrame = new Bitmap(currentFrame); return changes; } // 分块检测差异(比逐像素更高效) int blockSize = 32; for (int y = 0; y < currentFrame.Height; y += blockSize) { for (int x = 0; x < currentFrame.Width; x += blockSize) { int blockWidth = Math.Min(blockSize, currentFrame.Width - x); int blockHeight = Math.Min(blockSize, currentFrame.Height - y); Rectangle blockRect = new Rectangle(x, y, blockWidth, blockHeight); bool hasChange = false; for (int by = 0; by < blockHeight && !hasChange; by++) { for (int bx = 0; bx < blockWidth && !hasChange; bx++) { if (currentFrame.GetPixel(x + bx, y + by) != _lastFrame.GetPixel(x + bx, y + by)) { hasChange = true; } } } if (hasChange) { using Bitmap region = currentFrame.Clone(blockRect, currentFrame.PixelFormat); changes.Add((blockRect, region)); } } } _lastFrame.Dispose(); _lastFrame = new Bitmap(currentFrame); return changes; }
4. 控制发送帧率,避免网络拥塞
当前代码无延迟循环发送,会导致CPU占用过高且网络过载,添加帧率限制(如每秒15帧):
private void SendImage() { int frameInterval = 1000 / 15; // 15帧/秒,约66ms间隔 while (Status == ServerStatus.Connected) { try { using Image bitmap = Desktop.TakeScreenShoot(); Desktop.SerializeScreen(_NetStream, bitmap); } catch (Exception ex) { // 替换空catch,记录错误便于排查 Console.WriteLine($"发送失败: {ex.Message}"); } Thread.Sleep(frameInterval); } }
5. 优化屏幕捕获效率
CopyFromScreen属于GDI+方式,可改用更高效的Windows API(如BitBlt)减少捕获耗时:
[DllImport("gdi32.dll")] private static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop); [DllImport("user32.dll")] private static extern IntPtr GetDC(IntPtr hwnd); [DllImport("user32.dll")] private static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); public static Bitmap TakeScreenShootFast() { Rectangle screenBounds = Screen.PrimaryScreen.Bounds; Bitmap bitmap = new Bitmap(screenBounds.Width, screenBounds.Height); using (Graphics g = Graphics.FromImage(bitmap)) { IntPtr hdcDest = g.GetHdc(); IntPtr hdcSrc = GetDC(IntPtr.Zero); BitBlt(hdcDest, 0, 0, screenBounds.Width, screenBounds.Height, hdcSrc, 0, 0, 0x00CC0020); // SRCCOPY g.ReleaseHdc(hdcDest); ReleaseDC(IntPtr.Zero, hdcSrc); } return bitmap; }
6. 清理代码中的不良实践
- 移除空的
catch块,添加错误日志,便于排查网络或捕获异常 - 及时释放
Bitmap、MemoryStream等实现IDisposable的资源,避免内存泄漏
内容的提问来源于stack exchange,提问作者Luis Garcia
相关产品推荐
相关产品推荐

