基于Linux的流媒体桌面客户端开发
项目描述:本项目实现基于C/S模型的网络音频点播系统。该软件分为服务器和客户机两个部分,服务器以点播/多播的方式向局域网中所有的客户端发送数据,客户机可以根据自己的选择来决定要接收的数据。
主要工作:
- 采用socket编程来实现不同主机之间的通信
- 采用多线程的机制,增加处理频道节目单和音频并行服务数量
- 采用管道,实现客户端父子进程间的通信
- 使用状态机解析 HTTP 请求报文,支持解析 GET 和 POST 请求
- 采用令牌桶算法,实现读取MP3文件的流量控制
- 使用 Reactor高并发模型处理连接,实现可同时处理多个客户端连接
- 使用 epoll(ET或LT)技术实现高效的 IO 多路复用
个人收获:熟悉Linux平台的网络编程、对UDP以及TCP协议服务的过程有了更清晰的认识,对于进程间的通信、多线程的并发及流控算法有了⼀定的理解,增加项目经验(约2k行代码量)。
广播/点播/多播
概念:
D类IP地址-abcdef拓展:https://blog.csdn.net/kzadmxz/article/details/73658168
广播点播概念:单播、多播(组播)和广播的区别 - 简书
应用:
如何在Linux c/c++ 进行多播(组播)编程_linux 组播-CSDN博客
UDP特别的:多点通信
多播服务器端
- socket()(socket()函数用法详解:创建套接字)
- 初始化sockaddr_in类型的变量,包括协议族、地址、端口
- sockoption,设置
//tcp
//创建多播组,加入多播组,发送数据
#include <iostream>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
const char* multicast_group = "239.0.0.1"; // 设置多播组的IP地址
int multicast_port = 12345; // 设置多播组的端口号
int sockfd;
struct sockaddr_in addr;
char message[] = "Hello, Multicast!";
// 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "Error creating socket" << std::endl;
return 1;
}
// 初始化多播组地址
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(multicast_group);
addr.sin_port = htons(multicast_port);
// 启用多播功能
int enable = 1;
if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &enable, sizeof(enable)) < 0) {
std::cerr << "Error setting multicast loop" << std::endl;
}
// 加入多播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(multicast_group);
mreq.imr_interface.s_addr = INADDR_ANY;
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
std::cerr << "Error joining multicast group" << std::endl;
}
// 发送数据到多播组
sendto(sockfd, message, sizeof(message), 0, (struct sockaddr*)&addr, sizeof(addr));
close(sockfd);
return 0;
}
多播客户端:
#include <iostream>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
const char* multicast_group = "239.0.0.1"; // 多播组的IP地址
int multicast_port = 12345; // 多播组的端口号
int sockfd;
struct sockaddr_in addr;
// 1. 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "Error creating socket" << std::endl;
return 1;
}
// 2. 设置多播组地址
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(multicast_group);
addr.sin_port = htons(multicast_port);
// 3. 加入多播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(multicast_group);
mreq.imr_interface.s_addr = INADDR_ANY;
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
std::cerr << "Error joining multicast group" << std::endl;
close(sockfd);
return 1;
}
// 4. 接收多播数据
char buffer[1024];
int bytes_received;
while (true) {
bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);
if (bytes_received < 0) {
std::cerr << "Error receiving data" << std::endl;
break;
}
// 处理接收到的数据
}
// 5. 关闭套接字
close(sockfd);
return 0;
}
拓展知识点:
网络中的数据分割问题_tcp消息分段_ninekwll0791的博客-CSDN博客
有用的网站:
c++网络编程目录总结
https://www.cnblogs.com/DOMLX/p/9663167.html
采用socket编程来实现不同主机之间的通信
概念:
UDP如何实现可靠传输
应用:
讨论:跨主机的传输要注意的问题
1. 字节序问题:
大端:低地址处放高字节
小端:低地址出放低字节
主机字节序:host
网络字节序:network
解决:_ to _ _: htons, htonl, ntohs, ntohl (s 两字节, l四字节)
2. 对齐
struct {
int i;
float f;
char ch;
};
解决:不对齐
3. 类型长度问题:
int
char
解决: int32_t, unit32_t, int64_t, int8_t, uint8_t
4.用到的函数
socket();
bind();
sockaddr -> man 7 ip -> 找Address format
sendto();
rcvfrom();
inet_pton(); 点分式转为二进制格式
inet_ntop();
setsockopt();
getsockopt();
多点通讯:广播(全网广播,子网广播), 多波/组播
224.0.0.1 所有支持多播的节点默认都存在这个组中,并且无法离开
报式套接字代码过程:
被动端(先运行)
-
取得socket: 使用
socket()
系统调用创建一个套接字,例如socket(AF_INET, SOCK_DGRAM, 0);
创建一个IPv4的报式套接字。 -
给socket取得地址: 使用
bind()
绑定套接字到本地地址和端口。这是可选的,但通常用于指定接收数据的本地端口和地址。 -
收/发消息: 使用
recvfrom()
接收来自客户端的数据报,使用sendto()
发送数据报给客户端。在报式套接字通信中,每个数据报都是独立的,没有持续的连接。 -
关闭socket: 使用
close()
函数关闭套接字连接。
主动端
-
取得socket: 使用
socket()
创建一个套接字,例如socket(AF_INET, SOCK_DGRAM, 0);
创建一个IPv4的报式套接字。 -
给socket取得地址(可省略): 如果需要,使用
bind()
将套接字绑定到本地地址。这通常是在主动端不指定本地端口时由系统自动选择的,所以通常可以省略。 -
收/发消息: 使用
sendto()
发送数据报给被动端,使用recvfrom()
接收被动端的响应。在报式套接字通信中,每个数据报都是独立的,没有持续的连接。 -
关闭socket: 使用
close()
函数关闭套接字连接。
流式套接字:
C端(主动端)
-
取得socket: 使用
socket()
系统调用创建一个套接字。例如:socket(AF_INET, SOCK_STREAM, 0);
创建一个IPv4的TCP套接字。 -
给socket取得地址(可省略): 如果需要,使用
bind()
将套接字绑定到本地地址。这通常是在客户端不指定本地端口号时由系统自动选择的,所以通常可以省略。 -
发送连接: 使用
connect()
函数连接到服务器。将服务器的地址信息传递给connect()
函数,这个地址包括服务器的IP地址和端口号。 -
收/发消息: 使用
send()
发送数据到服务器,使用recv()
接收来自服务器的响应。 -
关闭socket: 使用
close()
函数关闭套接字连接。
S端(被动端,先运行)
-
取得socket: 使用
socket()
创建一个套接字,同样可以是socket(AF_INET, SOCK_STREAM, 0);
来创建IPv4的TCP套接字。 -
给socket取得地址: 使用
bind()
绑定套接字到服务器的IP地址和端口号。 -
将socket设置为监听模式: 使用
listen()
函数将套接字设置为监听模式,以便接受客户端的连接请求。 -
接受连接: 使用
accept()
函数等待客户端的连接请求。当有客户端连接时,accept()
会返回一个新的套接字,用于与客户端进行通信。 -
收/发消息: 使用新的套接字进行数据的收发,同样使用
send()
发送数据到客户端,使用recv()
接收客户端的数据。 -
关闭socket: 使用
close()
函数关闭服务器的套接字连接。
使用的模型框架
⾼性能⽹络模式:Reactor 和 Proactor
1、reactor和proactor区别
- 主线程的工作内容、下发给工作线程的工作内容不一样
2、reactor
3、proactor
采用多线程的机制,增加处理频道节目单和音频并行服务数量
【Linux】——多线程_linux 多线程_hrimkn的博客-CSDN博客
1. 线程的概念
一个正在运行的函数
posix 线程是一套标准,而不是实现
openmp线程
线程标识:pthread_t
pthread_equal();——比较线程ID
pthread_self();——获取调用线程的ID
最好不要和信号大规模混用
2. 线程的创建
pthread_create();
**线程的调度取决于调度器策略**
线程的终止
3种方式:
+ 线程从启动例程返回,返回值就是线程的id码
+ 线程可以被同一进程中的其他线程取消
+ 线程叫调用pthread_exit()函数
pthread_join(); --> 相当于wait();收尸函数--等待由thread
指定的线程结束。如果该线程已经终止,则pthread_join()
立即返回。
栈的清理
pthread_cleanup_push();
pthread_cleanup_pop();
这两句话必须一对出现,要不然编译错误
线程的取消选项
线程取消:pthread_cancel();
取消有两种状态:允许和不允许
允许取消又分为:异步cancel,推迟cancel(默认)->推迟至cancel点再响应
cancel点:POSIX定义测cancel点,都是可能引发阻塞的系统调用
pthread_setcancelstate():设置是否允许取消
pthread_setcanceltype():设置取消方式---可以设置延迟取消
pthread_testcancel():本函数什么都不做,就是一个取消点
线程分离
pthread_detach(); 已经分离的不能再用pthread_join()收尸,各安天命
不能使用 pthread_join()
等函数等待分离状态的线程的终止,分离状态的线程仍然与父进程共享相同的进程空间,包括内存地址空间、文件描述符、全局变量等。线程的分离状态主要影响线程的退出处理方式,以及线程是否可以被其他线程等待。
同一进程中的代码段时共用的,栈是独立的----就是说进程空间能创建多少个栈----查看系统的限制命令ulimit -a
64位环境下c程序的虚拟空间为128T----
3. 线程同步
互斥量:
类型:pthread_mutex_t
pthread_mutex_init();
pthread_mutex_destroy();
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;----静态初始化
pthread_mutex_lock();
pthread_mutex_trylock();---以非阻塞方式锁定互斥锁
pthread_mutex_unlock();
pthread_once(); 用来实现某个模块的单次初始化
条件变量:
pthread_cond_t
pthread_cond_init();
pthread_cond_destroy();
pthread_cond_broadcast();
pthread_cond_signal();
pthread_cond_wait();
pthread_cond_timewait();
信号量:
读写锁:读锁 -> 共享锁
写锁 -> 互斥锁
注意:临界区的跳转,需要注意解锁,否则容易造成死锁
4. 线程属性
pthread_attr_init();
pthread_attr_destroy();
pthread_attr_setstacksize();
见man pthread_attr_init的see also
线程同步的属性
互斥量属性:
pthread_mutexattr_init();
pthread_mutexattr_destroy();
pthread_mutexattr_getpshared(); getp这个p指的是是否跨进程使用
phtread_mutexattr_setpshared();
clone();
pthread_mutexattr_gettype();
pthread_mutexattr_settype();
条件变量属性:
pthread_condattr_init();
pthread_condattr_destroy();
读写锁属性:
..............
线程池----->0 可抢
=0 可放
<0 exit
5. 重入
多线程中的IO
线程和信号的关系
pthread_pending();
sigwait();
pthread_kill();
线程与fork
#### 6. openmp(www.OpenMP.org)
io复用--使用 epoll(ET或LT)技术实现高效的 IO 多路复用
进程间通信--采用管道,实现客户端父子进程间的通信
管道:
匿名管道
匿名管道的创建,需要通过下面这个系统调用:
int pipe(int fd[2])
这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0]
,另一个是管道的写入端描述符 fd[1]
。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中。
http的web
流量控制
数据库
哨兵