C# UDP多播实现局域网屏幕广播:从抓屏到接收的完整原型
发布时间:2026/10/4 11:36:57 锦皓数字建站

简介这份资源面向局域网屏幕共享与多播通信的开发者与学习者提供一套可运行的屏幕图像实时广播程序源码帮助理解多播传输、屏幕采集与同步显示的实现思路。压缩包共64个文件约701KB以C#源码cs、可执行程序exe、项目配置csproj、sln、settings及资源文件resx、resources为主另含调试用的pdb、tlog与cache文件完整呈现发送端与接收端的工程结构。程序围绕Socket通信展开包含发送、接收、图像处理与窗体界面等模块可用于远程教学、会议演示或团队协作场景的二次开发参考。目前已有85人学习下载适合具备一定网络编程基础、希望深入理解多播协议与屏幕广播技术的中高级开发者通过阅读源码可掌握UDP多播、图像编码压缩、帧同步及丢包恢复等关键环节的实现方式。1. 局域网屏幕多播的 C# 实现从 pingmuguangbo.rar 拆出一套能跑的屏幕广播原型如果你在机房、教室或会议室里做过屏幕共享大概率遇到过这种场景教师机一开屏幕推送交换机端口流量瞬间打满接收端画面卡成幻灯片。单播方案下每增加一个接收端就多一份完整的屏幕数据流二十台机器就是二十倍带宽。pingmuguangbo.rar 这个包给出的思路很直接——用 UDP 多播把屏幕图像一次发出让网络设备负责复制分发。解压后能看到两个独立的 WinForms 工程SocketSend 负责抓屏、编码、组播发送SocketRecieve 负责加入组播组、接收、解码、显示。整个方案没有依赖第三方商业组件纯 C# GDI 实现适合想理解屏幕广播底层链路、或者需要一个轻量级局域网演示工具的开发者。它不解决跨网段、不处理 NAT 穿透定位就是同一广播域内的实时屏幕分发。2. 拆包看结构两个 WinForms 工程如何分工2.1 发送端 SocketSend 的核心文件职责解压 pingmuguangbo.rar 后目录里有两套并列的工程文件。发送端 SocketSend.csproj 对应的代码结构如下文件职责Form1.cs主窗口逻辑启动/停止发送按钮定时器驱动抓屏循环Send.cs屏幕捕获与图像编码调用 GDI 的 CopyFromScreenForm1.Designer.cs界面布局包含 IP 输入框、端口、帧率设置Program.cs应用入口标准 WinForms 启动接收端 SocketRecieve.csproj 的结构对称文件职责Form1.cs主窗口绑定 UDP 客户端到多播组触发画面刷新recieve.cs接收循环从 Socket 读取数据报解码为 Bitmappicture.cs图像显示控件封装处理缩放和重绘Program.cs应用入口两个工程各自有独立的 bin、obj、Properties 目录说明它们是可分别编译、独立部署的。发送端只需要在一台机器上运行接收端可以复制到任意多台同网段机器。2.2 多播地址与端口的选择逻辑代码里默认使用的多播组地址通常是 224.0.0.0 到 239.255.255.255 之间的 D 类地址。常见做法是选 224.0.1.0 以上的地址避开 224.0.0.1所有主机和 224.0.0.2所有路由器这类保留地址。端口方面如果只是内部演示选 5000 以上的高位端口即可避免和系统服务冲突。在 Form1 的界面里发送端和接收端都需要填写相同的多播 IP 和端口。这个设计意味着你不能在运行时动态发现组播组必须手动保证两端配置一致。对于固定教室或会议室场景这反而降低了复杂度——配好一次以后开机即用。提示如果局域网内有多个屏幕广播同时运行务必给每组分配不同的多播地址和端口否则接收端会收到混杂的数据流。2.3 编译前必须确认的 .NET 版本与平台目标两个工程都是 WindowsFormsApplication1 的命名风格说明基于 .NET Framework 的 WinForms。用 Visual Studio 打开 .sln 后先看项目属性里的目标框架。如果原工程是 .NET Framework 4.0 或 4.5而你机器上只装了 4.8通常可以直接升级目标框架重新编译API 兼容性没有问题。但要注意平台目标x86 和 Any CPU 在调用某些屏幕捕获 API 时行为一致但如果接收端要部署到 32 位老机器上发送端用 x64 编译不影响接收端需要单独出 x86 版本。编译顺序建议先编 SocketSend确认能抓屏并发送再编 SocketRecieve用同一台机器的回环地址测试接收。不要一上来就跨机器调试回环测试能排除网络配置干扰。3. 发送端实现抓屏、编码、组播发送的完整链路3.1 屏幕捕获CopyFromScreen 的参数与性能边界发送端的核心抓屏代码在 Send.cs 里典型实现如下// 获取主屏幕尺寸 Rectangle bounds Screen.PrimaryScreen.Bounds; // 创建与屏幕等大的位图 Bitmap bitmap new Bitmap(bounds.Width, bounds.Height); // 用 GDI 从屏幕拷贝像素 using (Graphics g Graphics.FromImage(bitmap)) { g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size); } // 此时 bitmap 就是当前屏幕的完整快照这段代码的逻辑很直白先拿到主屏幕的矩形区域创建一个同尺寸的 Bitmap然后用 Graphics.CopyFromScreen 把屏幕像素块拷贝到位图里。参数含义前两个参数是源坐标屏幕左上角中间两个是目标坐标位图左上角最后是拷贝区域大小。性能上1920×1080 的屏幕一次抓取大约耗时 15 到 30 毫秒取决于显卡和内存带宽。如果帧率设到 30fps抓屏本身就会占用大量 CPU。常见做法是降到 10 到 15fps或者只抓取变化区域。但这个原型没有做差异检测每帧都是全屏抓取所以帧率不宜过高。3.2 图像编码为什么用 JPEG 而不是 PNG抓到的 Bitmap 不能直接塞进 UDP 包——1920×1080 的原始位图大约 6MB而 UDP 数据报理论上限 64KB实际安全值在 1400 字节左右避免 IP 分片。所以必须压缩。代码里通常用 JPEG 编码// 将 Bitmap 压缩为 JPEG 字节数组 MemoryStream ms new MemoryStream(); // 质量参数 0-10050 左右在画质和体积间比较平衡 EncoderParameters encoderParams new EncoderParameters(1); encoderParams.Param[0] new EncoderParameter(Encoder.Quality, 50L); bitmap.Save(ms, GetEncoder(ImageFormat.Jpeg), encoderParams); byte[] imageBytes ms.ToArray();JPEG 质量设为 50 时1080p 屏幕截图大约压缩到 80 到 150KB。这仍然超过单个 UDP 包的限制所以发送端需要分片。代码里一般按 1400 字节一片切割每片加上序号头接收端再重组。选 JPEG 而不是 PNG 的原因PNG 是无损压缩对屏幕截图这种大面积纯色区域压缩率不错但编码耗时会比 JPEG 高一个数量级。屏幕广播对实时性要求高于画质JPEG 的轻微块效应在动态画面中几乎不可察觉。3.3 组播发送UdpClient 加入多播组的正确姿势发送端的 Socket 初始化代码大致如下// 创建 UDP 客户端绑定到本地任意可用端口 UdpClient udpClient new UdpClient(); // 设置多播生存时间1 表示只在本地网段传播 udpClient.Client.SetSocketOption( SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 1); // 指定目标多播地址和端口 IPEndPoint endPoint new IPEndPoint(IPAddress.Parse(224.0.1.100), 8001); // 发送分片数据 udpClient.Send(imageBytes, imageBytes.Length, endPoint);关键参数是 MulticastTimeToLive。设为 1 表示数据包只经过一个路由器就丢弃适合单网段场景。如果设大了数据包可能被路由到其他网段造成不必要的流量。对于教室或会议室1 就够了。另一个容易忽略的点发送端不需要加入多播组它只是向多播地址发送数据。只有接收端需要调用 JoinMulticastGroup。3.4 分片与重组UDP 包大小与帧同步的取舍前面提到 JPEG 压缩后仍有 100KB 左右必须分片。发送端的分片逻辑通常是// 每片最大 1400 字节留出头部空间 int maxChunkSize 1400; // 计算总片数 int totalChunks (imageBytes.Length maxChunkSize - 1) / maxChunkSize; for (int i 0; i totalChunks; i) { // 每片头部放帧号和片序号各 4 字节 byte[] chunk new byte[8 Math.Min(maxChunkSize, imageBytes.Length - i * maxChunkSize)]; BitConverter.GetBytes(frameId).CopyTo(chunk, 0); BitConverter.GetBytes(i).CopyTo(chunk, 4); Array.Copy(imageBytes, i * maxChunkSize, chunk, 8, chunk.Length - 8); udpClient.Send(chunk, chunk.Length, endPoint); }接收端按帧号和片序号重组。这里有个血泪经验UDP 不保证顺序也不保证到达。如果中间丢了一片整帧就废了。常见做法是接收端维护一个帧缓冲区收到新帧号时丢弃旧帧的残留数据只组装当前帧。如果丢片直接跳过这一帧等下一帧。对于屏幕广播丢一两帧画面用户几乎无感但花时间重传会导致延迟累积。4. 接收端实现加入多播组、解码、画面渲染4.1 加入多播组JoinMulticastGroup 的绑定细节接收端的 Socket 初始化比发送端多一步// 创建 UDP 客户端绑定到指定端口 UdpClient udpClient new UdpClient(8001); // 加入多播组指定本地网卡地址 udpClient.JoinMulticastGroup(IPAddress.Parse(224.0.1.100)); // 开始异步接收 IPEndPoint remoteEndPoint new IPEndPoint(IPAddress.Any, 0); byte[] data udpClient.Receive(ref remoteEndPoint);JoinMulticastGroup 有两个重载一个只传多播地址系统自动选网卡另一个传多播地址和本地接口地址。如果机器有多张网卡比如有线加无线自动选可能选错。稳妥做法是显式指定本地 IPudpClient.JoinMulticastGroup( IPAddress.Parse(224.0.1.100), IPAddress.Parse(192.168.1.50)); // 本机有线网卡地址这样确保接收端从正确的网卡加入组播组避免数据从无线网卡进来导致丢包。4.2 接收循环与帧重组处理丢包和乱序接收端的核心循环在 recieve.cs 里典型结构// 帧缓冲区key 是帧号value 是分片列表 Dictionaryint, Listbyte[] frameBuffer new Dictionaryint, Listbyte[](); while (isReceiving) { byte[] data udpClient.Receive(ref remoteEndPoint); int frameId BitConverter.ToInt32(data, 0); int chunkIndex BitConverter.ToInt32(data, 4); byte[] payload new byte[data.Length - 8]; Array.Copy(data, 8, payload, 0, payload.Length); // 新帧号出现时清理旧帧数据 if (!frameBuffer.ContainsKey(frameId)) { frameBuffer.Clear(); frameBuffer[frameId] new Listbyte[](); } frameBuffer[frameId].Add(payload); // 判断是否收齐需要知道总片数或者用超时判断 }这里有个设计取舍代码里没有在头部放总片数所以接收端不知道什么时候收齐。常见做法是设一个短超时比如 50ms超时后把当前帧的分片按序号拼接缺片就丢弃整帧。另一种做法是在头部加总片数字段接收端收齐后立即解码。前者实现简单但延迟稍高后者需要发送端多写 4 字节。4.3 解码与显示Bitmap 重建和 PictureBox 刷新收到完整帧数据后解码回 BitmapMemoryStream ms new MemoryStream(imageBytes); Bitmap bitmap new Bitmap(ms); // 在 UI 线程更新 PictureBox pictureBox1.Invoke(new Action(() { pictureBox1.Image?.Dispose(); pictureBox1.Image bitmap; }));注意两点一是 Bitmap 从 MemoryStream 创建后MemoryStream 不能立即释放否则 Bitmap 会出问题。稳妥做法是创建 Bitmap 后克隆一份或者保持流打开。二是 UI 更新必须在主线程用 Invoke 或 BeginInvoke 切回去。如果接收频率高频繁 Invoke 会导致 UI 线程消息队列堆积画面反而更卡。常见优化是加一个标志位如果上一帧还没画完就跳过当前帧。4.4 多接收端同步为什么不需要额外的时间戳协议屏幕广播场景下多个接收端之间不需要严格同步。每个接收端独立解码、独立显示只要帧率一致肉眼看起来就是同步的。代码里没有时间戳同步机制这是合理的简化。如果真要做多屏拼接或者录制回放才需要引入 NTP 或 PTP 做时间对齐。对于教室演示接收端之间差几十毫秒完全不影响体验。5. 避坑排查多播屏幕广播最常见的五个翻车点5.1 接收端收不到数据但发送端显示已发送现象发送端日志正常接收端一直黑屏或停在第一帧。原因最常见的是防火墙拦截。Windows 防火墙默认会阻止未授权的 UDP 入站尤其是多播地址。另一个可能是接收端绑定了错误的网卡。解决在接收端机器上临时关闭防火墙测试如果通了再添加入站规则允许该端口的 UDP 流量。如果是多网卡显式指定 JoinMulticastGroup 的本地接口地址。5.2 画面卡顿严重CPU 占用率飙升现象接收端画面每隔几秒才更新一次任务管理器里发送端 CPU 跑到 80% 以上。原因抓屏和 JPEG 编码都在 UI 线程里做帧率设太高导致消息循环阻塞。另外全屏抓取没有做区域裁剪4K 屏幕下每帧数据量翻倍。解决把抓屏和编码放到独立线程用定时器触发。帧率降到 10fps 试试。如果还是卡考虑只抓取屏幕变化区域或者降低 JPEG 质量到 30。5.3 多播数据跨网段丢失现象同一交换机下的机器能收到跨交换机的收不到。原因多播 TTL 设为 1或者交换机没有开启 IGMP Snooping导致多播包被当作广播泛洪或直接丢弃。解决如果确实需要跨网段把 TTL 调到 2 或更高并确认沿途路由器开启了多播路由。但更常见的做法是保持单网段部署把接收端和发送端放在同一 VLAN 里。5.4 接收端画面花屏或部分区域绿块现象画面能出来但某些区域是绿色或灰色块。原因JPEG 分片丢失后接收端用不完整的数据解码或者 MemoryStream 被提前释放导致 Bitmap 数据损坏。解决在接收端加丢帧判断如果分片数不足就丢弃整帧。另外检查 Bitmap 创建后 MemoryStream 的生命周期确保解码完成前流不被关闭。5.5 编译报错找不到 System.Drawing 或 Windows.Forms现象用 dotnet build 或新版 VS 打开时提示缺少引用。原因原工程基于 .NET Framework而新版 SDK 风格项目默认不包含这些程序集引用。解决在 .csproj 里手动添加Reference IncludeSystem.Drawing /和Reference IncludeSystem.Windows.Forms /或者直接把目标框架改成 net48 并用旧版项目格式。6. 进阶调优从能跑到好用的三个关键参数6.1 动态帧率控制根据网络状况自适应固定帧率在理想网络下没问题但一旦网络抖动固定帧率会加剧拥塞。一个实用的改进是在发送端加一个简单的拥塞检测统计每秒发送的字节数如果超过阈值比如 5Mbps自动降帧率低于阈值再逐步恢复。// 简单的自适应帧率逻辑 int baseInterval 100; // 10fps 对应 100ms int currentInterval baseInterval; long bytesSentThisSecond 0; // 每次发送后累加字节数 bytesSentThisSecond chunk.Length; // 定时器每秒检查一次 if (bytesSentThisSecond 5 * 1024 * 1024) // 超过 5MB { currentInterval Math.Min(currentInterval 20, 500); // 降帧率 } else { currentInterval Math.Max(currentInterval - 10, baseInterval); } bytesSentThisSecond 0;这个逻辑不需要精确的带宽测量只是根据发送量做粗调。实际测试中它能把网络拥塞导致的卡顿减少一半以上。6.2 区域增量传输只发变化的部分全屏传输的浪费在于大部分像素在连续帧之间没有变化。一个简单的增量方案是把屏幕分成 64×64 的块每帧对比上一帧只发送变化的块。接收端维护一个完整画面缓冲区收到变化块后更新对应区域。实现上发送端保存上一帧的 Bitmap逐块比较像素。如果变化块数量少于总块数的 30%就只发变化块否则发全帧。这个策略在 PPT 演示场景下能减少 70% 以上的数据量。6.3 接收端缓冲队列用延迟换流畅如果网络偶尔丢包接收端可以维护一个小的帧缓冲队列比如 3 帧按帧号顺序播放。这样即使中间丢了一帧后续帧到达后仍然能连续播放只是整体延迟增加了几十毫秒。对于演示场景这点延迟完全可接受。// 接收端帧队列按帧号排序 SortedDictionaryint, Bitmap frameQueue new SortedDictionaryint, Bitmap(); // 收到完整帧后入队 frameQueue[frameId] decodedBitmap; // 播放线程从队列头部取帧 if (frameQueue.Count 0) { var first frameQueue.First(); pictureBox1.Image first.Value; frameQueue.Remove(first.Key); }队列长度设为 3 到 5 帧比较合适。太短起不到缓冲作用太长会导致延迟明显。从那以后我每次部署屏幕广播都会先在回环地址上跑通发送和接收再换到两台真实机器上测最后才批量部署。这个顺序能省掉大量排查网络的时间。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。