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

资讯详情

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

C# UDP多播实现局域网屏幕广播:从抓屏到接收的完整原型

C# UDP多播实现局域网屏幕广播:从抓屏到接收的完整原型

简介:这份资源面向局域网屏幕共享与多播通信的开发者与学习者,提供一套可运行的屏幕图像实时广播程序源码,帮助理解多播传输、屏幕采集与同步显示的实现思路。压缩包共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+ 的 CopyFromScreen
Form1.Designer.cs界面布局,包含 IP 输入框、端口、帧率设置
Program.cs应用入口,标准 WinForms 启动

接收端 SocketRecieve.csproj 的结构对称:

文件职责
Form1.cs主窗口,绑定 UDP 客户端到多播组,触发画面刷新
recieve.cs接收循环,从 Socket 读取数据报,解码为 Bitmap
picture.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-100,50 左右在画质和体积间比较平衡 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 有两个重载:一个只传多播地址,系统自动选网卡;另一个传多播地址和本地接口地址。如果机器有多张网卡(比如有线加无线),自动选可能选错。稳妥做法是显式指定本地 IP:

udpClient.JoinMulticastGroup( IPAddress.Parse("224.0.1.100"), IPAddress.Parse("192.168.1.50")); // 本机有线网卡地址

这样确保接收端从正确的网卡加入组播组,避免数据从无线网卡进来导致丢包。

4.2 接收循环与帧重组:处理丢包和乱序

接收端的核心循环在 recieve.cs 里,典型结构:

// 帧缓冲区,key 是帧号,value 是分片列表 Dictionary<int, List<byte[]>> frameBuffer = new Dictionary<int, List<byte[]>>(); 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 List<byte[]>(); } frameBuffer[frameId].Add(payload); // 判断是否收齐(需要知道总片数,或者用超时判断) }

这里有个设计取舍:代码里没有在头部放总片数,所以接收端不知道什么时候收齐。常见做法是设一个短超时(比如 50ms),超时后把当前帧的分片按序号拼接,缺片就丢弃整帧。另一种做法是在头部加总片数字段,接收端收齐后立即解码。前者实现简单但延迟稍高,后者需要发送端多写 4 字节。

4.3 解码与显示:Bitmap 重建和 PictureBox 刷新

收到完整帧数据后,解码回 Bitmap:

MemoryStream 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 Include="System.Drawing" />和<Reference Include="System.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 帧),按帧号顺序播放。这样即使中间丢了一帧,后续帧到达后仍然能连续播放,只是整体延迟增加了几十毫秒。对于演示场景,这点延迟完全可接受。

// 接收端帧队列,按帧号排序 SortedDictionary<int, Bitmap> frameQueue = new SortedDictionary<int, Bitmap>(); // 收到完整帧后入队 frameQueue[frameId] = decodedBitmap; // 播放线程从队列头部取帧 if (frameQueue.Count > 0) { var first = frameQueue.First(); pictureBox1.Image = first.Value; frameQueue.Remove(first.Key); }

队列长度设为 3 到 5 帧比较合适。太短起不到缓冲作用,太长会导致延迟明显。

从那以后我每次部署屏幕广播,都会先在回环地址上跑通发送和接收,再换到两台真实机器上测,最后才批量部署。这个顺序能省掉大量排查网络的时间。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表