十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#远程桌面实时监控源码解析:屏幕采集与Socket传输实战

C#远程桌面实时监控源码解析:屏幕采集与Socket传输实战 简介一份C#远程桌面实时监控完整源码包适合需要实现远程桌面控制、屏幕实时查看与监控的.NET开发者或学习Socket通信、桌面捕获、客户端/服务端架构的C#中级学习者。资源由客户端、服务端与封装类库三个项目组成核心实例类封装在SR.Base.BaseFunction.dll中并附有对应源码此外还提供其他常用方法封装注释详尽便于直接集成或二次开发。包体共104个文件以cs源码、dll类库、exe可执行程序为核心另含pdb调试文件、xml注释文档、工程配置文件等压缩包仅941KB轻量易部署。目前已有1078人学习下载实用性得到一定验证。通过阅读客户端与服务端源码可深入理解远程桌面连接建立、画面数据传输、指令响应等关键流程同时借助封装类库快速复用基础功能有效缩短自研远程监控系统的开发周期。1. 屏幕监控为什么要自己用C#重写实时通道真正让一线运维头疼的场景是监控端要嵌入自己的上位机界面同时还要按时间切片、按窗口过滤、把画面存成证据。这时候现成的远程桌面连接协议并不适用因为它们的重点是人机操作不是画面回传。这份C#远程桌面实时监控源码给出的路由是客户端采集屏幕并以流的方式推给服务端服务端完成显示和落盘。项目拆成 DeskTopApp、ServerSoftWareApp、SR.Base.BaseFunction 三个独立工程协议封装也留了源码不是只能调用的黑盒。适合做远程运维看屏、内网电脑画面轮询以及二次开发上位机监控模块的团队。2. 从三个工程拆解实时监控的数据流与类职责2.1 RemoteDeskTopClient.cs客户端是“被控推送端”而不是播放器拿到源码第一件事是分清角色。DeskTopApp 装在被监控的电脑上它负责截屏、按 JPEG 编码、通过 C# Socket 推给服务端ServerSoftWareApp 装在监控者电脑上负责监听端口、接收帧、在窗体上还原。很多人习惯把 RDP 那套“谁连谁”的模型套进来容易把方向搞反。连接部分的常见写法在 RemoteDeskTopClient.cs 里核心是一个带握手包的 Socket 连接。// RemoteDeskTopClient.cs 连接与握手 public bool Connect(string serverIp, int port, string accessKey) { tcpClient new TcpClient(); tcpClient.Connect(serverIp, port); // 主动连监控服务端 NetworkStream stream tcpClient.GetStream(); byte[] hello BuildHandshake(accessKey); // 16字节定长握手包 stream.Write(hello, 0, hello.Length); stream.Flush(); return stream.CanWrite; }serverIp 是监控服务端地址port 是服务端的监听端口accessKey 是两端约定好的口令。握手包固定 16 字节前 4 字节可以放命令字后 12 字节放客户端编号或时间戳这样服务端在 TCP 层就能区分“这是合法采集端”而不是乱入的连接。注意 Connect 是同步阻塞方法生产环境要套一层超时控制否则被控电脑断网时界面会卡在“正在连接”。截屏不能让 UI 线程去做。被监控电脑上如果有人正在操作截屏线程卡顿会直接影响用户的鼠标键盘响应所以 RemoteDeskTopClient 中的抓屏循环必须跑在独立线程或 Task 上主线程只负责界面状态提示。2.2 RemoteDesktopMonitor.cs 与 MonitorWinForm.cs服务端的接收与呈现拆分服务端不是把收包和显示混在一个地方。常见结构是ServerManager.csproj 里的监听器在 TCPListener 上 Accept每来一个客户端就 new 一个 RemoteDesktopMonitor 实例由它维护接收缓冲区、解出完整 JPEG 帧然后通过事件把帧交给 MonitorWinForm.cs 显示。这样的好处是连接数和 UI 解耦。被监控端从 1 台扩大到 20 台时RemoteDesktopMonitor 实例也随之增加而窗体只负责把最新帧往 PictureBox 上贴接收线程不会因为界面重绘而丢数据。工程文件的职责可以整理成下面这张表文件 / 工程角色关键职责DestTop.csproj客户端采集工程登录握手、定时截屏、JPEG 压缩、Socket 发送ServerSoftWareApp / ServerManager.csproj服务端监控工程端口监听、会话管理、帧分发、日志记录SR.Base.BaseFunction.csproj公共封装类库协议包、配置读取、日志、通用工具方法RemoteDeskTopClient.cs客户端连接器连接服务端、封装采集循环RemoteDesktopMonitor.cs服务端会话管理每连接一个接收实例解帧并触发事件MonitorWinForm.cs服务端主窗体显示实时画面、维护客户端列表WindowsFun.cs窗口工具枚举窗口标题、判断窗口状态、定位前台窗口这里要注意ServerManager 这个工程名容易让人以为服务端是被动的“被连接方”其实它才是真正等待采集端连入的监控端。源码里 MonitorWinForm 和 RemoteDesktopMonitor 的分工核心思路是“会话逻辑不进窗体”窗体只消费已经解析完的帧。后续要加画面录像、运动检测或人脸框选也都是在事件处理分支里加不需要动接收线程。2.3 SR.Base.BaseFunction.dll 与 WindowsFun.cs公共封装里藏着复用价值摘要里明确提到客户端和服务端的实例类都封装在 SR.Base.BaseFunction.dll 中并且附了源码。也就是说协议结构体、Socket 收发封装、JPEG 编码辅助方法都可以直接被其他项目引用。常见做法是这个类库里放 BitmapHelper、SocketHelper、ConfigManager 三个基础模块。BitmapHelper 负责把 Bitmap 按质量参数编码成 JPEG byte[]避免每个界面代码都重复写 Image 到流的转换。SocketHelper 提供带超时的 Read、异步接收事件把 TCP 粘包/半包问题收敛在一个文件里。ConfigManager 从 ini 或 exe 同目录的配置读参数端口、口令、帧间隔不用重新编译。WindowsFun.cs 是容易被忽略的文件。它的典型功能是根据进程名或窗口标题拿到 IntPtr再通过 Win32 API 获取窗口矩形用来实现“只监控某个窗口而不是整屏”。比如被控端在跑全屏软件时管理员只关心软件窗口范围采集前先用 WindowsFun 剪裁目标区域画面更清晰带宽消耗也小。ValidateUnUsedCode.cs 严格来说不属于运行时链路它是一个静态检查工具思路是扫描 csproj 中 Compiled 项与实际类引用找出没有被调用的公共方法。接手这套源码做二次开发前跑一遍能清掉一批“看着有用但没人调”的冗余代码减少后面搜索定位的干扰。3. 编译三件套与最小帧协议设计3.1 先编 SR.Base.BaseFunction再编两端可执行工程源码根目录里的 ResolveAssemblyReference.cache、DestTop.csproj.GenerateResource.Cache 这类文件是 Visual Studio 编译过程中生成的中间文件不需要提交到 Git也不要在源码分发时当作代码使用。真正需要编译的是三个 csproj且顺序有要求公共类库必须先编客户端和服务端都引用它。命令行编译可以直接走 MSBuild在 Visual Studio Developer Command Prompt 里执行msbuild SR.Base.BaseFunction/SR.Base.BaseFunction.csproj /p:ConfigurationRelease /p:PlatformAnyCPU msbuild DestTop/DestTop.csproj /p:ConfigurationRelease /p:PlatformAnyCPU msbuild ServerSoftWareApp/ServerManager.csproj /p:ConfigurationRelease /p:PlatformAnyCPU第一行先把 SR.Base.BaseFunction 编成 dll后两行分别生成客户端和服务端的可执行文件。编译顺序错了会出现“找不到 SR.Base.BaseFunction”的引用错误因为两个上层工程没有直接包含公共类库源码而是以项目引用或程序集引用的方式挂依赖。CodePlex 或 GitHub 上很多 C# Socket 项目都用这种结构公共部分单独成工程两端各自维护自己的入口类。Release 模式下输出目录通常是 bin\ReleaseDeskTopApp 生成在 DestTop 工程的输出目录ServerSoftWareApp 生成在 ServerSoftWareApp 工程的输出目录。把公共类库的 dll 和两个可执行文件放到同一个部署目录时注意别只拷 exe运行时会去找 SR.Base.BaseFunction.dll漏掉就直接报 FileNotFoundException。3.2 核心参数IP、端口、帧间隔、JPEG 质量远程桌面实时监控最敏感的参数是帧间隔和 JPEG 质量它们直接影响带宽和流畅度。源码里通常会把这几个值放到配置节或启动参数里整理后的参数表如下参数建议范围说明ServerIp内网 IP / 127.0.0.1被控端填监控服务端 IP多网卡时填实际可达地址ServerPort5900~5909避开常用端口防火墙需要同时放行 TCP 入站FrameIntervalMs30~100监控静态桌面时 100ms看视频/操作演示时 30msJpegQuality60~85数值越低越省带宽但文字边缘会发虚AccessKey任意字符串两端一致才会握手成功建议不低于 8 位我在实际套到上位机项目时习惯把 FrameIntervalMs 做成动态的鼠标或键盘事件发生后的 2 秒内用 30ms 采集静止之后自动降到 200ms。这套源码如果只做周期性截屏也可以按这个思路在 RemoteDeskTopClient 的采集循环里加判断不需要改协议。3.3 一个 12 字节帧头的最小协议远程桌面画面本质是一串 JPEG 字节流。如果直接往 TCP 流里写 JPEG接收端没法判断一帧从哪开始到哪结束。整理后的源码里通常自带一个帧封装方法核心思路是“帧头固定 12 字节 JPEG 数据体”。// 帧封装Magic(4) FrameNo(4) Length(4) JPEG Body public static byte[] BuildFrame(byte[] imageBytes, int frameNo) { byte[] header new byte[12]; byte[] magic { 0x58, 0x44, 0x4D, 0x53 }; // XDMS 标记 Buffer.BlockCopy(magic, 0, header, 0, 4); // Magic 固定值 Buffer.BlockCopy(BitConverter.GetBytes(frameNo), 0, header, 4, 4); Buffer.BlockCopy(BitConverter.GetBytes(imageBytes.Length), 0, header, 8, 4); byte[] frame new byte[header.Length imageBytes.Length]; Buffer.BlockCopy(header, 0, frame, 0, header.Length); Buffer.BlockCopy(imageBytes, 0, frame, header.Length, imageBytes.Length); return frame; }Magic 的前四个字节等于 0x58 0x44 0x4D 0x53作用是在接收端校验数据对齐。FrameNo 可以用于丢帧统计每次截屏自增一。Length 是 JPEG 数据体的字节数接收端先读满 12 字节头再按 Length 读完整 body。这里一定要用 BitConverter 而不是手动做移位因为 BitConverter 直接映射当前系统的字节序在 C# 环境下两端通常都是小端不会有兼容问题。配套的接收端不能直接调一次 Read 就认为拿到了完整帧TCP 是流协议一帧数据可能分两次到达两帧数据也可能粘在一起。读头时需要通过循环把 12 字节凑满再按 bodyLen 循环取 body。private byte[] ReadFullFrame(NetworkStream ns) { byte[] header new byte[12]; int read 0; while (read 12) // 循环读满帧头 { int n ns.Read(header, read, 12 - read); if (n 0) return null; read n; } int bodyLen BitConverter.ToInt32(header, 8); byte[] body new byte[bodyLen]; read 0; while (read bodyLen) // 循环读满数据体 { int n ns.Read(body, read, bodyLen - read); if (n 0) return null; read n; } return body; }如果不做循环读取内网低延迟下多数帧能一次读出来但一旦被控端在 4G 或跨三层网络环境画面就会出现偶发撕裂和花屏。这个 ReadFullFrame 放在 SR.Base.BaseFunction 里复用时还应该在每次进入循环前加一个可取消的 CancellationToken否则客户端断线后读线程会一直卡在 ns.Read 上。4. 联调排错从回环测试到 UI 不卡顿4.1 先跑通 127.0.0.1 回环再部署到两台机器我调试这类监控源码的第一步永远是本机回环。先启动 ServerSoftWareApp监听 127.0.0.1:5900再启动 DeskTopApp服务端 IP 填 127.0.0.1端口填 5900。如果本机两端都正常说明协议、帧封装、显示链路没问题再换真实 IP 联调减少变量。需要注意 Session 0 隔离问题。如果被控电脑把 DeskTopApp 注册成 Windows 服务运行那么服务进程位于 Session 0没有权限访问用户桌面的屏幕内容画面永远是黑屏。常见做法是让 DeskTopApp 以普通用户态进程开机启动而不是用服务方式托管。源码里如果单独拆了“服务端、客户端”两个可执行文件客户端这边请确认运行用户必须在当前登录会话中。4.2 接收线程只入队UI 定时器只出队解决画面刷新卡顿很多人在二次开发时把 JPEG 解码和 PictureBox 赋值直接写在 RemoteDesktopMonitor 的事件里内网延迟一高界面就频繁无响应。这是因为网络接收线程在频繁地跨线程操作 UI 控件。处理这个问题的标准套路是“采集线程入队UI 定时器出队”。private readonly ConcurrentQueuebyte[] _frameQueue new ConcurrentQueuebyte[](); private readonly System.Windows.Forms.Timer _uiTimer new System.Windows.Forms.Timer(); private void InitDisplay() { _uiTimer.Interval 33; // 约 30fps 的界面刷新上限 _uiTimer.Tick (s, e) { if (_frameQueue.TryDequeue(out byte[] jpeg)) { using (var ms new MemoryStream(jpeg)) { pictureBox.Image?.Dispose(); pictureBox.Image Image.FromStream(ms); } } }; _uiTimer.Start(); } // 接收线程中只做入队不碰任何控件 private void OnFrameReceived(byte[] jpeg) { _frameQueue.Enqueue(jpeg); }收到新帧后只入队即使接收线程瞬间来了 5 帧UI 定时器每秒最多也只取 30 帧。这样做的代价是画面会丢弃中间帧但人眼对实时画面的要求是“最新”不是“每一帧都画出来”所以这种做法在处理循环数据采集和 UI 刷新卡顿问题里最实用。需要注意 PictureBox.Image 在替换时要先 Dispose 旧引用否则长时间运行时 GDI 句柄会持续增长最后界面绘制变成一片空白。如果希望丢帧更少可以把 Timer.Interval 调成 16ms但代价是解码 JPEG 的 CPU 占用会明显上升。内网监控建议 33ms跨公网建议 50ms因为网络延迟本身已经决定了流畅度上限。4.3 远程桌面连接失败的典型排错表两端联调时最常见的错误不是代码问题而是环境问题。整理成一张表直接对着排查效率更高现象排查点排查路径客户端发起连接后一直转圈服务端 IP 不可达 / 端口未监听在客户端机器telnet 服务端IP 5900服务端防火墙拦截入站规则未放行netsh advfirewall firewall add rule nameRemoteMonitor dirin actionallow protocolTCP localport5900连接成功但画面不刷新帧头 Magic 不匹配检查 BuildFrame 里的 magic 与接收端校验是否一致画面花屏接收端未按 12 字节头循环读取检查 ReadFullFrame 是否处理了半包运行一段时间后黑屏GDI 句柄泄漏确认 PictureBox.Image 替换前是否 Dispose服务端连接数到上限未释放断线客户端在 RemoteDesktopMonitor 的读线程异常分支里调用 tcpClient.Close()远程桌面连接失败的常见原因是客户端能 ping 通但连不上端口。这往往不是源码问题而是服务端监听地址写成了 127.0.0.1。如果把 TcpListener 的监听地址固定成本机环路地址外部机器永远连不进来。处理方式是改成 IPAddress.Any 或从配置文件读取本机局域网 IP这也是整理源码时最容易改错的一处。5. 按序号校验丢帧并把采集源替换成 USB 摄像头监控对象不一定是屏幕。很多生产项目会把这套结构改造成“USB 摄像头画面回传”比如巡检机器人、工位状态检测。替换点其实非常小RemoteDeskTopClient 里原来的 GrabScreen 返回的是屏幕截图改成从摄像头取帧就行后面的 Socket、协议、服务端显示完全不动。常见的替换方式是使用 AForge.NET 或开源相机库拉取摄像头帧转成 Bitmap 后再复用原来的 JPEG 编码方法。Bitmap frame camera.GetCurrentFrame(); // 替换原来的 GrabScreen() byte[] jpeg BitmapHelper.EncodeJpeg(frame, 80); // 复用公共类库封装 int frameNo Interlocked.Increment(ref _frameNo); byte[] packet BuildFrame(jpeg, frameNo); stream.Write(packet, 0, packet.Length);这里有一个容易被忽略的坑屏幕截图是固定分辨率的而摄像头分辨率可能在采集过程中因驱动自动调整而变化导致服务端显示区域跳动。建议在采集循环里显式判断 frame.Width 和 frame.Height发生变化时先发送一帧“分辨率变更”控制帧再发送图像数据。丢帧统计是验证监控链路质量最直接的方法。TCP 本身不丢数据但服务端如果来不及解码或者接收缓冲排队过长实际显示帧数会低于采集帧数。给每帧的 FrameNo 加一个递减校验就能精确算出丢帧数。private int _lastSequence -1; private int _lossCount 0; private void CheckLoss(int sequence) { if (_lastSequence 0 sequence _lastSequence 1) { _lossCount sequence - _lastSequence - 1; } _lastSequence sequence; }在服务端 OnFrameReceived 分支里调用 CheckLoss 后把 _lossCount 显示在 MonitorWinForm 的状态栏上。判断标准不是“必须为零”而是当 _lossCount 出现持续增长时说明 UI 定时器的刷新间隔已跟不上采集端的帧率。优先调整 _uiTimer.Interval 或降低摄像头帧率而不是盲目提高 JpegQuality。反过来如果丢帧为 0 但延迟很大则要检查服务端是否在收帧前做了多余的文件写入或日志打印把这些 io 操作移到独立线程延迟通常能下降一半以上。本文还有配套的精品资源点击获取
返回列表