基于IPV4的流媒体广播
基于IPV4的流媒体广播
这个项目和webserver挺像的,但是这个是基于UDP的,实现了一个组播。有服务端和客户端两个。
服务端:主要是不停的向外广播自己的节目单和节目内容(没错,就算没有客户端开启,也会一直广播)
客户端:主要是根据自己的需求订阅服务端的节目单,然后接收相应的节目数据,传递给ffmpeg,让ffmpeg来解析一下内容,然后播放出来
(是不是感觉挺高大上的,其实不是,只是简单的接收所有的数据,然后判断这个数据里面的ID是不是自己选择的就好,是自己选择的就保留,不是就丢弃)
运行起来
原项目git:https://github.com/litbubo/Streaming_media_broadcasting_system_based_on_IPv4
环境: Ubuntu 20.04 ffmpeg
还需要下载一个ffmepg包,这个是啥包我忘记了,但是在运行client的时候会发现只会接收一个UDP包的数据
这是因为缺少了这个包。接收数据之后会馈入ffmpeg里面,因为缺少了这个包,所以才会只接收一个数据之后就断掉
终端上会有提示,到时候直接ubuntu命令下载即可
// 终端-1 server |
结构
6_IPV4UDP/ |
复现
协议
protocol.h 文件,这里面定义了很多的基本的配置,比如:多播组的地址,默认的端口号,还有节目单,频道的格式等。
代码:这里面不涉及啥复杂的操作,看一看即可
|
服务端:
需要完成的任务:
1.将medialib目录下的文件转化为广播频道
2.广播节目单
3.广播频道内容
整体的复现思路:
1.server_conf.h 里面有一些服务端的基础配置
2.threadpool 这个比较独立,就是基础的线程池,用来管理线程。不依赖于其他模块
3.tokenbucket 令牌桶,用来控制流量,防止流量过大
4.medialib 主要是将medialib目录下的文件转化为广播频道,然后通过list广播节目单,通过channel广播频道内容
5.channel 主要是广播频道内容
6.list 主要是广播节目单
7.server 主要是管理线程池,然后调用list和channel来广播节目单和频道内容
按照这个思路,依次完成各个模块,然后最后整合起来,就可以完成服务端了
server_conf.h
没啥好说的,直接看代码
|
threadpool
这个要说一下,我之前做过webserver的项目,那个里面的线程池比较简单,就是单纯的addtask,然后执行任务就可以了,但是这个项目里面的线程池构造的很巧妙,如果看了代码就可以很好理解。
首先最基本的功能肯定是有的,就是addTask,然后执行任务
这里面还附加了另外的功能,就是设置了一个管理的线程,这个管理的线程用于管理线程池中线程的数量,当任务不多的时候,没必要保存那么多线程,这个时候就可以kill掉一些,当然会有一个最小的数量。当任务很多的时候,就需要额外申请一些线程,但是这个数量也是有限制的。
这样来实现线程数量的动态管理,不会因为空线程而浪费太多的CPU资源
有几个注意的点:
1.NUMSTEP, 每次进行添加和删除线程都会增加或者删除 NUMSTEP 多的线程。不会说一下子减少到最少,或者是一下子添加到最大。拿减少的来举例:如果检测到很多线程空闲,我先减少NUMSTEP个线程,然后再次检测,如果还是空闲,我再减少NUMSTEP多线程。
2.numExit 这个设置的就很巧妙,就是如果我要删除掉一个线程,我就把numExit设置为1,并且notify一下,线程在working (在working函数里面) 的时候,先检测一下numExit是不是1 (这里肯定是要加锁的哈),如果是1,那么我就不去task,直接退出(自杀)就好了,顺便把numExit–(上锁哈)。是不是感觉很巧妙
3.这里面有个独立出来的manage的线程哈,这个别忘记了,执行的是manager函数,里面就涉及到了之前说的一些线程的增加与删除等操作
代码:
//threadpool.h |
// threadpool.cs |
tokenbucket
令牌桶,涉及到一些流量控制,就是我们在读mp3媒体文件的时候,不是要传递一个读取多少字节的参数吗? 这个时候咱们先从令牌桶里面取令牌,如果说令牌数量很充足,就是大于size (size是咱们想要读的字节) 的话就取出size个令牌,然后读size个字节。但是如果说令牌桶里面取令牌不足size,比如只有size1这么多了,那么咱们读数据的时候就取出size1这么多字节。
这里面有个计时器,每一秒会使得令牌桶里面的令牌的数量增加
struct tokenbt_t{ |
这里看一下单个令牌桶的结构
注意的点:
1.这个定时器是额外定义的线程,他不会占用主线程。
2.并不是只有一个令牌桶,这里设计了一个令牌桶的数组 job数组。里面存储了好多令牌桶,这样针对每个频道,我们都可以为其分配一个令牌桶。让每个频道独立起来。而不是所有的频道公用一个令牌桶。
3.注意这个令牌桶使用的位置哈,并不是sendto和recvfrom这两个函数用,而是在读取mp3文件的时候来用,就是通过控制文件读取的速度来控制sendto发送数据包的速度。
4.这里有两种类型的锁,一个是job队列的锁,就是对job队列进行操作的时候必须得加锁。还有一个是桶内部的锁,看上面的结构,是不是每个桶都有一个锁,这是为了在实现安全的修改每个桶里面的数据。
|
|
medialib
这个的任务就是将我们本地的medialib文件夹里面的内容转化为广播频道
主要完成两项任务:
1.节目单的组合(mlib_getchnlist函数)
2.读取频道内部的数据(mlib_readchn函数)
这两个函数是最关键的,之前所说的令牌桶就是在mlib_readchn函数里面用的,就是读取mp3文件的操作
首先我们来说一下结构
├── medialib |
每个ch之中都会有一个desc.txt的文件,这个里面就写的一些频道的基本的描述,比如:
ch1中内容就一行: pop music,起风了,世界这么大还是遇见你
所以我们要获取一下这个节目单的描述啥的还是很简单的,就是读取desc.txt文件就行了
我们看一下每个频道的管理的结构体
|
这里主要是几个参数可能不好理解
1.glob_t globes 这个主要是用来搜索一下*mp3的内容的,搜索出来的文件路径会存在结构体的gl_pathv里面
2.pos 就是因为会有很多.mp3文件,这个就标志着这是第几个文件,从0开始,这样的话方便列表循环播放
3.fd 这个就是目前打开的mp3的文件描述符,用以后面读文件
4.offset 这个文件读到哪里了,用这个来表示,方便下次继续读
5.tokenbt_t *tb; 令牌桶就用在这里
代码:
|
|
list
之前不是medialib把咱么medialib文件夹里面的东西转化为频道了吗
这里就是组装一下,节目单频道,用来发送节目单的数据,代码很简单,看一下就好。
就是,拿到组装号的 节目点list,然后处理一下,然后发送就好。
这里的发送时调用的线程池(addtask)
就是一直发,1s发一次。另外频道号之前在protocol.h已经定义好了,频道列表的频道号固定是0
|
|
channel
这个就是某个频道,一直发送数据了,不管有没有客户端都要一直发送
sendchannel里面调用了mlib_readchn,发送完一个mp3文件之后会列表循环,不会停止发送哈
之前也说了mlib_readchn里面使用了令牌桶进行了流量控制
|
|
server
前面都讲了,这里就没啥好讲的了,就是将所有的东西组合一下就好
两点注意:
1.守护进程
就是将整个的进程置于后台,终端就不输出了
就是fork一个子进程,然后脱离父进程,使用子进程来执行代码
将输出重定向到 /dev/null 里面,/dev/null是一个linux内置的一个设备,往里输出的话就表示,我不需要这些输出,直接丢弃
setsid 创建新会话,脱离终端,即使终端关闭了也不会断开
static int daemon_init(){ |
后面就没啥了,就是socket的创建以及各个函数的调用,这个看代码一看就明白
|
客户端
任务就是接收数据,两个进程,父进程进行数据的接收,输入到管道里面,然后子进程拿数据输入到ffmpeg里面来播放,就是这个流程, 基本没有啥坑
唯一需要注意的点就是如何选择自己的频道
首先要明白一点,加入到组播里面之后,client会接收所有的数据
因此需要先进行判断,首先得先拿到节目单,也就是ID为0的包。不是ID为0的包就丢弃。
然后把包里面的内容输出到终端让客户来选择那个频道,比如输入1,那么就选择1频道
后面拿到包之后先对比一下是不是1频道,是的话就拿过来,处理。不是的话就丢弃就好。
其他的就没啥的,下面是代码
|
|
总结
感觉比webserver还要简单一些,就是这个里面的结构体各种定义很容易迷,这就是不如C++ class封装之处了。整体来看还是很值的做的。