源码:https://github.com/FzhangSpace/net_radio.git
(一) 概要
实现的功能,广播流媒体文件,发送端和接收端,发送端分为200个频道,其中有一个菜单频道,和其他的数据频道,接收端通过客户端通过菜单选择数据频道,并且通过mpg123播放
(二)模块划分
发送端
菜单发送模块:
负责发送菜单,包括频道号,以及该频道相应的描述,以供接收端选择频道
媒体数据发送模块:
负责发送各个频道的媒体数据
媒体资源处理模块:
负责从媒体目录中相应文件里面读取媒体数据提交给媒体数据发送模块
令牌桶:
负责控制发送数据的速度,以确保不会因为发送的速度过快导致接收端缓冲区满溢出而丢包
发送和接收时的数据结构
程序的主体
(三)实现细节
3.0 发送和接收时的数据包结构
./src/include/proto.h
./src/include/site_types.h
#defien CHNNR 200 /* 频道个数 */
#define LISTCHNID 0 /* 节目单的频道号 */
#define MINCHNID 1 /* 最小频道号 */
#define MAXCHNID (MINCHNID+CHNNR-1) /* 最大频道号 */
#define MSG_CHANNEL_MAX (65536-20-8) /* 一个最大的字节数 */
typedef uint8_t chnid_t;
struct msg_channel_st {
chnid_t id;
uint8_t data[1];
} __attribute__((packed));
#define MSG_LIST_MAX (65536-20-8)
struct msg_listentry_st {
chnid_t id;
uint16_t len;
uint8_t desrc[1];
} __attribute__((packed));
struct msg_list_st {
chnid_t id;
struct msg_listentry_st entry[1];
} __attribute__((packed));
新遇到的函数或用法
不要任意直接使用常量,如果这个常量有意义,那就定义宏来使用,便于后期的维护和代码的可读性
在两台电脑上通过网络传输数据的时候,不仅要注意大小端的问题,还要注意两台电脑之间的内存对齐问题
struct list {
struct list *next;
char data[0];
};
在32位下,sizeof(struct list)是4个字节,data里面没有元素,所以是0个字节,但是data的地址是紧贴着struct list末尾的地址的,所以可以这样使用
struct list *ptr = malloc(sizeof(struct list)+20);
这个结构体指针指向的空间多出20个字节来,可以通过data来得到这块内存的首地址,这样就实现了一个类似一个可变长字节的结构体
3.1 令牌桶
./src/server/mytbf.h
./src/server/mytbf.c
大体的功能是有一个容器,存有多个令牌,当去读文件的时候先去令牌桶拿令牌,拿到多少个就读出多少个字节,
规定令牌桶的最大容量(burst),每秒钟能拿多少个令牌(字节)(cps),当前桶内剩余的令牌数(token);
桶内的令牌会自动增长,每秒增长cps个令牌
struct mytbf_st {
int cps;
int burst;
int token;
pthread_mutex_t mut;
pthread_cond_t cond;
};
tpyedef void mytbf_t;
static struct mytbf_st *tbf[TBFMAX];
static pthread_mutex_t mut_tbf = PTHREAD_MUTEX_INITIALIZER; 用于锁这个令牌桶数组
static pthread_t tid_timer;
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
mytbf_t *mytbf_init(int cps, int burst);
static void module_load(void);
static void *thr_timer_func(void *unused);
static void *module_unload(void);
static int get_free_pos();
返回令牌桶的地址,当做句柄
int mytbf_fetchtoken(mytbf_t *,int n);
int mytbf_returntoken(mytbf_t *, int n);
int mytbf_destroy(mytbf_t *);
令牌桶指针数组的存在是为了令牌桶自增方便,把所有的令牌桶统计一下,通过一个线程轮询的方式自增,就避免每个令牌桶都使用单独的自增线程,节省资源,便于管理.
当然,这显然是一个需要多个线程同时操作的变量,所以需要加锁
新遇到的函数或用法
struct timespec t;
t.tv_sec = 1;
t.tv_nsec = 0;
while (nanosleep(&t, &t) != 0) {
if (errno != EINTR) {
syslog(LOG_ERR, "nanosleep(): %s", strerror(errno));
exit(1);
}
}
int atexit(void (*function)(void));
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control,
void (*init_routine)(void));
3.2 媒体资源处理模块
./src/server/medialib.c
./src/server/medialib.h
媒体文件的存储方式是在./tmp/media文件中,一个单独的文件,里面有存储着.MP3流媒体文件和desc.test文件(缺一不可),我们根据满足要求的子文件的个数来确定频道的个数,频道数新定位200个
struct mlib_listentry_st {
chnid_t id;
char *desc;
};
struct channel_context_st {
chnid_t id;
char *desc;
glob_t mp3glob;
int pos;
off_t offset;
int fd;
mytbf_t *tbf;
};
/***************API**********/
static struct channel_context_st channel[MAXCHNID+1];
int mlib_getchnlist(struct mlib_listentry_st **, int *);
static struct channel_context_st *path2entry(const char *path)
mytbf_t *mytbf_init(int cps, int burst);
ssize_t mlib_readchn(chnid_t cid, void* buf, ssize_t len);
int mytbf_fetchtoken(mytbf_t *,int n);
static int open_next(chnid_t id);
int mytbf_returntoken(mytbf_t *, int n);
int mlib_freechnlist(struct mlib_listentry_st *);
函数的实现细节
`int mlib_getchnlist(struct mlib_listentry_st **result, int *resnum)`
通过`snprintf(path, PATHSIZE, "%s/*", server_conf.media_dir)`;这一步拼接出"/var/media/*"带有通配符字符串,`server_conf.media_dir`这个就是默认的资源路径("/var/media")(绝对路径)
再使用`glob(path, 0, NULL, &globres)`得到符合这个通配符的所有文件名信息,放到`globres`里面
再根据`globres.gl_pathc`符合条件的文件路径个数确定一共需要创建的频道个数,在malloc内存相应的菜单项内存
通过轮询`globres.gl_pathv[i]`,范围`globres.gl_pathc`,来遍历每个频道的媒体文件,
再通过`res = path2entry(globres.gl_pathv[i]);`获取每个频道的内容(id,描述,流媒体文件等),
然后把path2entry返回的内容转化成`mlib_listentry_st`,都放到一个数组中
然后把频道的菜单列表和菜单的大小传出去.
`ssize_t mlib_readchn(chnid_t cid, void* buf, ssize_t len);` 读取媒体数据
`tbfsize = mytbf_fetchtoken(channel[id].tbf, size);`首先从;令牌桶获取令牌
`len = pread(channel[id].fd, buf, tbfsize, channel[id].offset);` 从offset的偏移位置读取数据
`mytbf_returntoken(channel[id].tbf, tbfsize-len);`把剩余的令牌换回去
新遇到的函数或用法
#include
int glob(const char *pattern, int flags,
int (*errfunc) (const char *epath, int eerrno),
glob_t *pglob);
typedef struct {
size_t gl_pathc;
char **gl_pathv;
size_t gl_offs;
} glob_t;
#include
char *strdup(const char *s);
strdup会先用malloc()配置与参数s字符串相同的空间大小,然后将参数s字符串的内容复制到该内存地址,
然后把返回.返回的地址最后可以利用free来释放
ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
和read差不多,不过是从offset这个偏置的位置开始读文件,而不是从文件指针位置
3.3 菜单发送模块
./src/server/thr_list.h
./src/server/thr_list.c
把通过媒体资源处理模块得到的菜单信息通过组播发送出去,每秒一次
int thr_list_create(struct mlib_listentry_st *listp, int nr_ent)
把listp和nr_ent传入到线程函数中去,这里是用的全局变量的形式
当前的菜单信息是struct mlib_listentry_st这个结构体的,要把它转化成菜单发送的数据包struct msg_list_st
首先根据listp确定struct msg_list_st菜单发送数据包的大小.然后malloc内存
然后把菜单项拷过去
死循环发送菜单包 间隔1s
3.4 菜单发送模块
给每个频道创建一个子线程发送各自频道的数据
int thr_channel_create(struct mlib_listentry_st *ptr)
创建一个线程函数
static void *thr_channel_func(void *ptr)
通过len = mlib_readchn(ent->id, sbufp->data, datasize);来读取流媒体内容,发送出去
新遇到的函数或用法
#include
int sched_yield(void);
当线程调用这个函数时,会让主动出CPU使用权,让其他线程得到执行
(四) 程序的主体
主要的业务流程
int serversd;
struct sockaddr_in sndaddr;
struct server_conf_st server_conf = {
.rcvport = DEFAULT_RCVPORT,
.mgroup = DEFAULT_MGROUP,
.media_dir = DEFAULT_MEDIADIR,
.ifname = DEFAULT_IF,
.runmode = run_daemon
};
主要的业务流程
1. 首先是设置日志文件
2. 读取命令行参数,修改相应的server_conf的值
3. 是否变成守护进程
4. socket环境初始化,创建socket文件,UPD,组播地址,端口号,端口可复用,
5. 获取加载媒体资源,得到菜单列表, mlib_getchnlist
6. 创建发送菜单的线程
7. 循环创建发送各个频道数据的线程
8. 主线程暂定,由各个线程向组播地址发送数据,等待信号终止程序
新遇到的函数或用法
守护进程需要设置信号捕捉函数,以便结束进程是不会丢失文件或数据
#include
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
#include
int getopt(int argc, char * const argv[],
const char *optstring);
extern char *optarg;
解析命令行的参数
(四) 总结
在项目规划就需要进行模块的划分,数据结构的设计.
在开始写代码的时候,不要过早的陷入细节,应该先把框架搭起来,模块的功能划分清楚,函数的封装以及低耦合行;然后再进行模块的编写,
并且在每个模块完成之后需要进行单元测试,以便提早发现问题,解决问题,避免后期直接合并的时候,程序调BUG的难度增加.
两台电脑进行网络通信的时候,需要注意网络字节序,还有数据包结构体的内存对齐问题,
解决方法是单字节的堆积是不会有内存对齐问题的,或者双方规定好内存对齐的方式.
还有就是在进行这种流媒体文件实时播放传输时,需要注意解析软件解析的速率问题,应该在发送的时候就需要限定速率,
并且在接收的时候最好先缓存一部分内容,以便对应网络的颠簸造成的收包速率不稳定,而导致播放质量差,卡顿等现象.
在程序运行期间,日志的记录是很有必要的,对帮助了解程序的运行状态很有帮助,应合理使用,并且也要划分好日志的级别,
以便在程序出现问题是快速定位问题点.
在编写程序过程中,应尽量少的直接使用常量赋值,对于有意义的常量应该使用宏,或者枚举来实现,提高程序的可读性
在进行模块编写的时候,属于模块内部调用的函数或者全局变量是应添加static关键字,只要把给其他模块调用的API提供出来就可.
注意内存管理,如果malloc出来的内存使用完毕后,马上free,避免内存泄露