
简介RTP.NET是一款基于.NET框架实现的RTP实时传输库面向需要处理音视频、VoIP及流媒体等实时数据的C#开发者。库内封装了RTPSession会话管理、参与者标识、数据包化、负载类型识别、事件驱动API与丢包恢复等关键机制能够显著降低基于RTP构建通信应用的协议实现门槛便于聚焦业务逻辑。整个压缩包仅约300KB共26个文件以cs源码、dll类库、exe演示程序、sln/csproj工程文件及chm帮助文档为主另有少量配置、图片和样式文件目录结构简明适合对照工程快速理解RTP核心流程。目前已有104人学习下载适合具备一定.NET基础、希望系统掌握RTP协议工程实现的开发者。借助包内示例与帮助文档读者可学会RTP会话创建、实时数据收发、与RTCP配合进行质量监控、多播传输等关键实现并为进一步开发VoIP、视频会议或在线推流等完整实时通信系统提供可复用的基础模块。1. 先把实时传输讲明白RTP.NET在解决什么我刚拿到 RTP.NET 这个库时第一反应是又要啃一套协议先别慌你可以把它想成快递里的生鲜专线。普通网页数据走 TCP像谨慎的快递员丢一个包裹就停下来重送稳但慢而视频通话、游戏语音这类数据过期就没意义得走一条丢了就继续往前冲的实时通道这条通道就是 RTPReal-time Transport Protocol。RTP.NET 是 .NET 世界里把这条通道封装好的类库封包、编号、收发、会话管理都替你做了你不用从零拼 RTP 包头也不用手动维护时序。适合谁想在 C# 里做音视频通话、流媒体、局域网实时传输但对底层协议还不熟的 .NET 开发者。2. 先搞懂三个关键词时间戳、序列号、负载类型很多新手直接打开 RTP.NET 的文档看到一堆类和方法就懵了。其实你不需要先懂全部 API只需要先懂 RTP 协议里最核心的三个概念序列号、时间戳、负载类型。这三个词会在你后面写的每一行代码里反复出现。2.1 为什么实时数据不能走 HTTP 或者 TCP要理解 RTP先要理解它为什么存在。HTTP 和 TCP 是可靠传输的代表你把一个文件发给别人如果中间丢了一个包发送方会重新传接收方也会一直等着直到数据完整。这在传网页、传文件时是优点但在传实时音频时就是灾难。想象你在打电话你说了你好两个字这两个字被切成很多小包发出去。如果其中一个包丢了TCP 会停下来等重传电话那头就会有一段空白然后突然听到一句好——中间那个你字永远补不回来了因为等你等到重传的包时话已经说完了。实时数据讲究的是按时到达不是一定到达。所以 RTP 跑在 UDP 上面UDP 不保证可靠交付丢了就丢了接收端根据序列号和时间戳判断少了什么、哪个先来、按什么节奏播放。RTP 是给数据包做编号、打时间标记、标明格式的那一层。它不负责保证送达只负责把数据整理成接收端能用的形状。2.2 序列号判断谁先谁后、谁丢了序列号Sequence Number是 RTP 包头里的一个数字每发送一个 RTP 包序列号就加一。接收端拿到一串包之后一看序列号 3、4、5、7就知道 6 号包丢了或还没到一看 1、4、3、2就知道顺序乱了需要重新排队。这个逻辑在 RTP.NET 里不需要你手动计算发送的时候库会自动为每个包编号接收端也会把序列号暴露在事件参数里给你看。但你理解它的意义很重要因为判断有没有丢包是实时通信里面 debugging 的第一步。// 示意代码接收端拿到一包数据后打印它的序列号 // 你不需要自己实现编号逻辑只需要读出来 Console.WriteLine(收到第 e.SequenceNumber 个包);序列号还有一个特点它只反映发送顺序不反映播放节奏。两个包各自的发送间隔是 5 毫秒还是 1 秒从序列号上看不出来这就要靠时间戳。2.3 时间戳告诉播放器这句声音什么时候念出来时间戳Timestamp是 RTP 里最容易误解的概念。它不表示现实中的几点几分而是表示这份数据的采样时刻。发送方每隔固定时间采样一次音频或视频时间戳就是采样时钟的刻度。比如某音频负载类型的采样频率是 8000Hz也就是每秒采样 8000 次那么两次采样之间的时间戳差值就是 160有些负载类型一个时间戳单位对应 1/8000 秒。接收端看到相邻两个包的时间戳差值就知道要在相隔 20 毫秒的时间点上依次播放这样声音的语速才是对的。如果你在视频通话里遇到画面正常但声音对不上多半就是时间戳没处理对。用 RTP.NET 时发送端只要你指定负载类型和采样频率它会在打包时自动填时间戳接收端要做的是把收到的包按时间戳排好序而不是按到达顺序直接播放。2.4 负载类型数字背后藏着音频和视频格式负载类型Payload Type用一个数字告诉接收端这包数据的真实格式是什么。比如数字 0 代表 G.711 μ-law 音频8 代表 G.711 A-law 音频这两种是 VoIP 电话里最常见的格式。而 H.264 视频、Opus 音频这类格式没有固定编号通常使用 96 到 127 之间的动态负载类型由双方在会话建立时协商好。先给一个常见负载类型的对照混个眼熟就行不用背负载类型值含义常见场景0PCM μ-law 音频传统电话、VoIP8PCM A-law 音频传统电话、VoIP96 ~ 127动态负载类型H.264、Opus 等视频会议、流媒体为什么这个很重要因为发送端和接收端必须用同一个负载类型编号否则接收端只知道收到了一个包却不知道它是什么格式自然没法播放。RTP.NET 里发送方法通常会接收一个 payloadType 参数你传 0 或 8 或 96取决于你的数据是什么。另外RTP 一般都是和 RTCPRTP Control Protocol一起出现的。RTCP 负责传输关于传输质量的信息丢了多少包、延迟多大、有多少参与者。在 RTP.NET 里RTCP 通常是自动启动的你不用专门去实现它但你可以通过它提供的统计数据来监控网络质量这会在后面的避坑章节里讲到。3. 认识 RTP.NET解压之后先看哪个文件、哪个类现在你已经知道 RTP 在解决什么问题了接下来可以打开 RTP.NET 这个资源看看里面到底有什么。很多新手拿到一个压缩包第一反应是把里面的源码全部打开挨个读其实完全没必要。你先得知道这个库的门面是什么。3.1 解压后先认出这几类文件RTP.NET 解压后你会看到一堆东西比如 .sln、.csproj、Program.cs、RTP.NET.dll、RTP.NET.HELP.chm、obj、Backup、UpgradeLog.XML 等。第一次看到这些名字第一反应多半是谁是谁。我帮你分个类文件或目录作用你现在需要管它吗RTP.NET.dll编译好的类库本体这是核心你的项目要引用它RTP.NET.HELP.chm帮助文档这是最重要的阅读材料RtpNetCsharp.sln / .csproj整个示例工程的解决方案和项目文件用开发工具打开时会用到Program.cs示例程序的入口代码可以读也可以直接改obj / Backup / UpgradeLog.XML编译中间产物、备份和升级记录暂时不用管其中最值得你花时间的是那两个RTP.NET.dll 和 RTP.NET.HELP.chm。dll 是库本身chm 是它的使用说明。RTP.NET 的方案里附带了一个 C# 工程也就是说你解压后可以直接用开发工具打开这个工程把 Program.cs 当成你的实验场地。一定不要一上来就试图读完所有源码。你先要知道的是这个库对外提供哪些类、每个类能干什么。这部分信息从 HELP.chm 里查是最快的。3.2 核心类 RTPSessionRTP 会话的总指挥RTPSession 是 RTP.NET 里最核心的类可以把它理解成一个电话会议管理员。你创建一个 RTPSession就相当于在某个端口上设立了一个通信中心。它负责维护跟所有远端设备之间的会话状态包括发送数据、接收数据、生成序列号、管理时间戳、维护参与者列表等。在你自己的代码里几乎所有关键操作都是围绕这个类展开的你要创建的它发送要调用它的方法接收要注册它的事件结束时也要关闭它。可以这么说RTP.NET 的其他类都是配角RTPSession 是主角。第一次学的时候你不用把这个类的所有方法都背下来只需要记住它的四个关键职责建立会话、发送 RTP 包、接收 RTP 包、管理参与者。后面写第一个例子时你会发现每步都用得到。3.3 RTPParticipant 与数据包化谁在说话、数据怎么变成包RTPParticipant 表示一个参与 RTP 会话的实体可以是一个发送方也可以是一个接收方。每个参与者都有地址、端口、SSRC同步源标识符。SSRC 是整个会话中唯一标识你这个发送方的 ID接收方靠它来区分这句话是谁说的。举个例子一个视频会议里有三个人三个人的声音都可能到达你的耳机。如果没有参与者标识你分不清哪包是 A 同学说的哪包是 B 同学说的。SSRC 就是给每个说话者分配的工牌号。另一个重要功能是数据包化Packetization。你手上有一串原始数据比如一段 PCM 音频字节RTP.NET 会把它切成适合网络传输的块加上 RTP 包头填好序列号和时间戳再交到 UDP 层发出去。这个过程你完全不需要手动处理字节拼接只需要把原始数据交给 RTPSession 的发送方法即可。3.4 事件驱动的 API为什么你不需要自己写死循环如果你学过基础网络编程可能会有一种惯性想法接收数据是不是要开一个线程不停循环 recv RTP.NET 提供的是一种事件驱动模型类似你在 C# 里写按钮点击事件那样你注册一个回调函数当库收到了 RTP 包时它会主动调用你的回调函数。事件驱动的最大好处是把你从轮询中解放出来。你的主程序不需要时刻盯在网上只需要把收到包之后干什么的代码写好剩下的监听、解包、触发时机全部交给库。这对新手友好得多至少你不会因为在一个死循环里忘记处理异常而导致整个程序卡死。4. 跑通第一个例子发一个自定义的 RTP 数据包纸上谈兵到这里差不多了。这一章我们做一个真正能跑的示例目标很朴素让两个控制台程序通过 RTP.NET 互发一条消息一个发一个收收发都在本机完成。它是你后续所有 RTP 练习的骨架。4.1 准备工作把库请进你的 C# 工程第一步打开开发工具比如 Visual Studio新建一个控制台应用项目目标框架建议选择 .NET Framework 4.x。然后添加对 RTP.NET.dll 的引用方法是在解决方案资源管理器中右键引用→添加引用→浏览找到解压目录里的 RTP.NET.dll。这里有个非常容易踩的小坑添加了引用之后代码最上方一定要写using RtpNet;具体命名空间以你下载版本的 HELP.chm 为准。很多新手明明引了 dll却因为漏了这一句而报找不到类型的错误然后卡在第一步。如果你的开发工具能直接打开 RtpNetCsharp.sln那就更省事了直接用自带的 Program.cs 改也行。我建议你用新建工程 添加引用来走一遍因为以后你自己写项目时也必须亲手完成这步。4.2 最小发送端把你好RTP变成一个 RTP 包发出去下面这段代码是发送端的完整骨架。你新建一个控制台项目后把代码替换进去先不用完全理解每一行跑起来再说。using System; using System.Text; using RtpNet; // 根据你下载的库实际命名空间调整 class Sender { static void Main() { // 1. 创建 RTP 会话监听本机的 9000 端口 RTPSession session new RTPSession(9000); // 2. 指定一个目标参与者IP 127.0.0.1端口 9001 RTPParticipant target new RTPParticipant(127.0.0.1, 9001); session.AddParticipant(target); // 3. 把字符串转成字节数组然后发送 byte[] payload Encoding.UTF8.GetBytes(你好RTP); session.Send(target, payload, 0); // 最后一个参数 0 是负载类型 Console.WriteLine(已发送你好RTP); session.Close(); } }我来解释几处关键点。第一步的new RTPSession(9000)是让会话在本机 9000 端口上就绪这个端口是我这个程序用来接收数据的端口。第二步创建了一个目标参与者IP 是 127.0.0.1本机回环地址端口是 9001意思是我准备把数据发给本机的 9001 端口。第三步的session.Send(target, payload, 0)中的 0 就是上一章说的负载类型这里先用 0 表示最基础的音频格式虽然我们实际发的是文字但格式编号要有一个。有一个细节提醒你Send方法的具体参数顺序在库的不同版本里可能略有差别有的版本可能不要求传入参与者对象而是直接传 IP 和端口。遇到不一致时打开 RTP.NET.HELP.chm在索引里搜 RTPSession.Send看清方法签名再改。这本身就是程序员的标准工作方式。4.3 最小接收端注册事件收到包就打印出来发送端写完接下来写接收端。接收端也是一个控制台程序监听的端口要跟发送端指定的目标端口保持一致也就是 9001。using System; using System.Text; using RtpNet; class Receiver { static void Main() { // 创建 RTP 会话监听本机 9001 端口 RTPSession session new RTPSession(9001); // 注册事件每当收到一个 RTP 包就触发 OnPacket session.PacketReceived OnPacket; // 开始接收主线程挂在这里等待包到来 session.Start(); Console.WriteLine(接收端已启动按任意键退出...); Console.ReadKey(); session.Close(); } // 事件回调它会在库收到 RTP 包时被自动调用 static void OnPacket(object sender, RtpPacketEventArgs e) { string text Encoding.UTF8.GetString(e.Payload); Console.WriteLine(收到消息 text); Console.WriteLine(负载类型 e.PayloadType); Console.WriteLine(序列号 e.SequenceNumber); } }接收端的核心模式是注册事件 挂起等待session.PacketReceived OnPacket;就像一个电话铃声接口库收到包后会自动拨过来你预先准备好的OnPacket方法就会执行。session.Start()让接收器开始工作。Console.ReadKey()只是为了让程序不退出否则你还没来得及看结果窗口就关了。RtpPacketEventArgs里的Payload是去掉 RTP 包头之后真正有用的数据PayloadType就是负载类型SequenceNumber就是这一包的序列号。你不需要自己去解析 RTP 包的头部字节这就是用类库的好处。4.4 跑起来先开接收端再开发送端实际操作顺序是这样的先运行接收端程序让它在 9001 端口待命然后运行发送端程序。两个窗口都在本机所以避开了防火墙跨机器的干扰是最干净的测试环境。如果一切正常接收端窗口会打印出收到消息你好RTP同时显示负载类型和序列号。如果没反应先按顺序检查三件事接收端监听端口是不是 9001发送端目标端口是不是 9001接收端有没有调用Start()这三个问题的排查细节下一章我会展开说。跑通之后你可以试着改几个参数来加深感觉把负载类型从 0 改成 96接收端打印的负载类型也会变把字符串换成一句长一点的话再连续多次调用Send你会发现接收端打印出的序列号依次递增。这些改动不会破坏程序却会帮你建立这些字段跟我操作直接相关的直觉。4.5 把它从发一次变成一直发现在这个例子还只是一个发一个包就结束的 demo离音视频传输还差得远。你可以做一个最简单的升级在发送端循环发送每次循环之间暂停 20 毫秒模拟语音数据每 20 毫秒打包一次的节奏。for (int i 0; i 100; i) { byte[] payload Encoding.UTF8.GetBytes(第 i 条消息); session.Send(target, payload, 0); System.Threading.Thread.Sleep(20); // 暂停 20 毫秒模拟实时节奏 }改完之后再运行接收端会连续弹出 100 条消息。虽然我们传的还是文字但以固定节奏不断发送这个模式已经和真实音频传输在结构上完全一致了。下一步要做的只是把字节数组的内容从文字换成麦克风采集到的音频数据。这就是从能跑到能传音视频的关键一步。5. 新手避坑指南五个最容易翻车的地方RTP.NET 本身不复杂但新手在实际操作中遇到的麻烦大多来自一些看起来很小、却特别耗时间的细节。我把这几个高频问题按现象→原因→解决写出来遇到相似情况时你可以直接对号入座。5.1 编译不通过找不到 RTPSession 类型现象代码写完了编译时报错提示找不到类型或命名空间。原因绝大多数情况是没有正确引用 RTP.NET.dll或者在代码文件顶部漏了using语句。也可能是引用了但 dll 版本跟你项目的目标框架不匹配导致类型加载失败。解决先确认解决方案资源管理器中能看到该引用再确认.cs文件第一行有对应的using语句最后检查项目目标框架。把目标框架改为 .NET Framework 4.x 一般能解决兼容问题。如果你是从压缩包自带的工程直接改的优先怀疑工程升级时引用了旧路径删掉引用重新添加一次即可。5.2 发送端不报错接收端一个字都没收到现象发送端跑完没有异常控制台也提示已发送但接收端窗口一直是空的。原因最常见的是端口没配对。记住一个关键逻辑发送端的目标端口必须等于接收端的监听端口。如果发送端写的是 9001接收端监听的是 9002那数据就发到了一个没人接收的地址上。另外IP 也要小心发给 127.0.0.1 只能在自己这台机器上收到想跨机器测试就得换成对方机器的局域网 IP。解决先确认两边端口一致再用 127.0.0.1 本机测试排除网络因素最后检查 Windows 防火墙是否拦截了 UDP 入站。如果是可以先为你的程序添加入站规则或临时关闭防火墙做一次验证验证完记得开启。还有一个隐蔽原因接收端没有调用Start()或等价方法只创建了会话但没真正开始监听。5.3 收到了消息但是乱码或者字节数对不上现象接收端打印出的文本是乱码或者明明是中文显示出来却是几个英文字母加符号。原因两端编码方式不一致。RTP 只负责传输字节不管字符编码。发送端用 UTF-8 编码接收端用 GBK 解码就会乱码。另外负载类型不一致也可能让接收端按错误的方式识别数据。解决统一编码。我建议发送端和接收端都使用Encoding.UTF8。把Encoding.UTF8.GetBytes(...)和Encoding.UTF8.GetString(...)成对使用就不会出问题。关于负载类型至少在测试阶段让两端使用同样的数字等以后真的接音频或视频时再去学习格式协商的细节。5.4 语音断断续续包已经到了却像卡壳现象把程序从发送文字改成发送音频后声音一顿一顿或者有大量延迟。原因很多人第一次调音视频会习惯性地收到一个包就立即播放一个包。但网络不可靠包可能乱序到达。如果某段时间网络抖动了接收端会因为没有及时收到下一个包而短暂静音听感就是断断续续。本质上你没给接收端留出重新排序和等待的余地。解决加一个抖动缓冲Jitter Buffer。最简单的做法接收端不立刻播放收到的包而是先攒 50 到 100 毫秒的数据量等缓冲填满再开始播放。虽然这引入了少量延迟但能大幅度消除网络抖动造成的卡顿。这一步是很多新手最容易忽略的因为先攒再播听起来像浪费时间但对实时通信来说稳定比绝对低延迟更重要。5.5 打开旧工程时弹出一堆升级报告现象用新版开发工具打开 RtpNetCsharp.sln弹窗提示升级并生成 UpgradeLog.XML工程里的 obj、Backup 目录看起来特别庞大。原因这个库的工程文件可能是在较旧版本的开发工具中创建的新工具打开时会尝试自动转换。那些 XML 日志和 obj 目录是转换过程的记录和编译缓存不是给你的代码添加了额外逻辑完全可以不用理会。解决接受升级提示重新生成解决方案一次。如果编译过程中报引用路径失效检查一下 RTP.NET.dll 的引用来源路径是否仍然指向有效位置必要时删除引用再重新添加。清理 obj 和 Backup 目录也可以让项目更清爽但无论删不删都不影响最终结果。6. 下一步怎么学从能发能收到能传音视频到这里你已经学会了 RTP 的核心概念也亲手跑通了第一个收发示例。下一步最关键的事是别急着学一大坨理论而是沿着下面这条路径一点一点把例子变得更接近真实应用。先给自己定一个目标做一个模拟项目X内容是用两个控制台程序模拟一个局域网语音传话器。你不需要真的接麦克风先用一个数组模拟音频数据按 20 毫秒的节奏发送接收端攒成完整的一句话再输出。这个项目做完你就算真正入门了 RTP。学习阶段练什么做到什么程度算过阶段一发送数字、文字、任意字节两端能稳定收发序列号连续阶段二按固定间隔连续发送接收端不掉包、不卡顿阶段三加入抖动缓冲人为制造丢包时播放依然平稳阶段四接入真实音频采集能听到自己和对方的通话声阶段五用 RTCP 统计丢包率能解释网络质量变化的原因每个阶段都做一次小实验然后完成它。我特别建议你尽早接触 RTCP 带给你的统计数据。RTP.NET 如果提供了会话的统计信息接口比如丢包数、累计收发包数你可以定时打印出来。丢包率一旦升高你就知道是网络问题而不是你的代码问题。还有一个值得早早上手的技巧把接收端的回调函数里除了处理正常数据再加上一个输出日志的路径比如把每次收到的序列号写进文本文件。这个小习惯会在你排查问题时帮你大忙。我第一次写收发程序时就是因为没在回调里记日志导致想排查问题都没法回看现场之后我强制自己在每个含网络收发的 Demo 里都加日志输出问题定位的速度快了很多。希望你一开始就养成这个习惯少走这些弯路希望帮到你。本文还有配套的精品资源点击获取