文章目录
前言
在当今的互联网时代,流媒体技术已经成为传输音视频内容的核心技术之一。为了更好地理解和应用流媒体技术,开始学习并实现了一个高性能的HTTP-FLV流媒体服务器。HTTP-FLV是一种常用的实时流媒体传输协议,结合了HTTP协议的广泛兼容性和FLV(Flash Video)格式的高效性,非常适用于实时视频流的传输。在博客中,将详细介绍HTTP-FLV的基本概念,并分享在实现高性能HTTP-FLV流媒体服务器过程中积累的经验和技巧。
一、HTTP-FLV是什么?
FLV (Flash Video) 是 Adobe 公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个 FLV 由 The FLV Header, The FLV Body 以及其它 Tag 组成。因此加载速度极快。采用 FLV 格式封装的文件后缀为 .flv。
HTTP-FLV 依靠 MIME 的特性,根据协议中的 Content-Type 来选择相应的程序去处理相应的内容,使得流媒体可以通过 HTTP 传输。相较于 RTMP 协议,HTTP-FLV 能够好的穿透防火墙,它是基于 HTTP/80 传输,有效避免被防火墙拦截。除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。
说了这么多优点,也来顺便说下 HTTP-FLV 的缺点,由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好。因为网络流量较大,它也不适合做拉流协议。
地址是以http://开头的,是基于HTTP协议的,HTTP-FLV可以简单地理解为RTMP的HTTP协议版本。但HTTP-FLV协议一般只能用拉流观看,HTTP-FLV协议的延迟也是比较低的,大概在1-3秒左右。HTTP-FLV相对于RTMP适配更多的播放场景。
HTTP-FLV直播流一般需要加入插件才能播放,如网页需要引入flv.js后,浏览器才能播放HTTP-FLV直播流。B站开源的flv.js促进了HTTP-FLV在浏览器的普及。
一般HTTP-FLV的流媒体服务,推流是以RTMP协议,拉流是用HTTP-FLV协议
HTTP-FLV 使用类似 RTMP流式的 HTTP 长连接,需由特定流媒体服务器分发的,兼顾两者的优点。以及可以复用现有 HTTP 分发资源的流式协议。它的实时性和 RTMP 相等,与 RTMP 相比又省去了部分协议交互时间,首屏时间更短,可拓展的功能也更多。
二、实现一个高性能的HTTP-FLV流媒体服务器
1.main.cpp
代码及详细注释如下:
#include "Server/BoostServer.h"
#include "Scheduler.h"
#include "Utils/Config.h"
/*
ffmpeg 播放http-flv视频流
ffplay -i http://localhost:8080/test.flv
*/
int main(int argc, char* argv[])
{
const char* file = NULL;
file = "config.json"; // 配置文件,启动参数放置在config.json
Config config(file); // 利用第三方库jsoncpp解析config.json
if (!config.state) { // 配置参数读取失败
printf("failed to read config file: %s\n", file);
return -1;
}
/*
BoostServer server(&config);: 这行代码创建了一个名为 server 的 BoostServer 对象,
并使用名为 config 的参数进行初始化。
std::thread([&]() { ... }).detach();: 这是一个 std::thread 对象的构造,
它使用了一个 lambda 表达式作为线程的执行体。
lambda 表达式 [&]() 表示捕获外部作用域的所有变量(以引用方式),
{ ... } 中的代码是线程的主体部分。
在 lambda 表达式内部,server.start(); 调用了 BoostServer 对象的 start() 方法,这是启动服务器的操作。
detach() 方法将新创建的线程分离,意味着它会在后台运行,
与主线程独立执行,不会等待该线程的结束。这样做是为了使主线程继续执行而不被阻塞。
*/
BoostServer server(&config);
std::thread([&]() {
server.start();
}).detach();
Scheduler sch(&server,&config);
sch.loop();
return 0;
}
2.配置相关类
代码及详细注释如下:
1. config.json
// config.json
{
"ip": "0.0.0.0",
"port": 8080,
"threadNum": 1
}
2. config.h
#ifndef CONFIG_H
#define CONFIG_H
#include <string>
class Config
{
public:
Config(const char* file);
Config() = delete;
~Config();
public:
bool state = false;
void show();
const char* getIp() const {
return ip.data();
}
int getPort() const {
return port;
}
int getThreadNum() const {
return threadNum;
}
private:
std::string mFile;
std::string ip;
int port;
int threadNum;
};
#endif //CONFIG_H
3. config.cpp
#include "Config.h"
#include <fstream>
#include <json/json.h>
#include "Log.h"
Config::Config(const char* file) : mFile(file) // 传递配置文件的文件路径
{
std::ifstream ifs(mFile, std::ios::binary);
if (!ifs.is_open()) {
LOGE("open %s error", file);
return;
}
else {
Json::CharReaderBuilder builder;
builder["collectComments"] = true;
JSONCPP_STRING errs;
Json::Value root;
if (parseFromStream(builder, ifs, &root, &errs)) {
this->ip = root["ip"].asString();
this->port = root["port"].asInt();
this->threadNum = root["threadNum"].asInt();
state = true;
}
else {
LOGE("parse %s error", file);
}
ifs.close();
}
}
Config::~Config()
{
}
void Config::show() {
//printf("config.file=%s\n", mFile.data());
}
3.BoostServer
1. BoostServer.h
#ifndef BOOSTSERVER_H
#define BOOSTSERVER_H
#include "boost.h"
#include <map>
class Config;
class HttpServerConnection;
class BoostServer
{
public:
BoostServer(Config* config);
~BoostServer();
public:
void start();
bool addConn(HttpServerConnection* conn); // 添加连接
bool removeConn(std::string session);
HttpServerConnection* getConn(std::string session);
bool sendData(char* data, int size);
static void cbDisconnection(void* arg, std::string session);
std::string generateSession();
private:
void handleDisconnection(std::string session);
void setOnAccept();
void onAccept(beast::error_code ec, tcp::socket socket);
private:
Config* mConfig;
net::io_context* mIoc; // io缓存
tcp::acceptor* mAcceptor; // 接收器
std::map<std::string, HttpServerConnection*> m_connMap; // <session,conn> 维护所有被创建的连接
std::mutex m_connMap_mtx;
};
#endif //BOOSTSERVER_H
2. BoostServer.cpp
#include "BoostServer.h"
#include "HttpServerConnection.h"
#include "../Utils/Config.h"
#include "../Utils/Log.h"
BoostServer::BoostServer(Config* config) : mConfig(config)
{
}
BoostServer::~BoostServer() { // 便于理解,基于传统new delete实现
if (mAcceptor) {
delete mAcceptor;
mAcceptor = nullptr;
}
if (mIoc) {
delete mIoc;
mIoc = nullptr;
}
}
void BoostServer::start()
{
int threadNum = std::max<int>(1, mConfig->getThreadNum()); // 启动线程数
unsigned short port = mConfig->getPort();
LOGI("BoostServer::start() ip=%s,port=%d,threadNum=%d ", mConfig->getIp() , port, threadNum);
boost::asio::ip::address address = net::ip::make_address(mConfig->getIp());
tcp::endpoint endpoint{ address, port };
mIoc = new net::io_context{ threadNum };
mAcceptor = new tcp::acceptor(*mIoc);
beast::error_code ec;
mAcceptor->open(endpoint.protocol(), ec); // 打开,支持协议
if (ec)
{
LOGE("acceptor.open error: %s", ec.message().data());
return;
}
mAcceptor->set_option(net::socket_base::reuse_address(true), ec); // 端口复用设置
if (ec)
{
LOGE("acceptor.set_option error: %s", ec.message().data());
return;
}
mAcceptor->bind(endpoint, ec);
if (ec)
{
LOGE("acceptor.bind error: %s", ec.message().data());
return;
}
mAcceptor->listen(net::socket_base::max_listen_connections, ec);
if (ec)
{
LOGE("acceptor.listen error: %s", ec.message().data());
return;
}
setOnAccept(); // 异步accept
std::vector<std::thread> ts;
ts.reserve(threadNum - 1);
for (auto i = 0; i < threadNum - 1; ++i)
{
ts.emplace_back([this] { //在每次循环中,使用 emplace_back 将一个 lambda 表达式添加到 ts 容器中。lambda 表达式中的代码会在新创建的子线程中执行。
std::thread::id threadId = std::this_thread::get_id();
LOGI("ioc sub threadId=%d", threadId);
mIoc->run(); // 启动子线程 在当前线程中运行 io_context 对象的 run() 函数,用于处理异步操作
});
}
std::thread::id threadId = std::this_thread::get_id();
LOGI("ioc main threadId=%d", threadId);
mIoc->run(); // 启动主线程
}
/*
1.mAcceptor->async_accept(...): 这一行代码是使用 Boost.Asio 库中的 async_accept 函数来异步接受新的连接请求。
mAcceptor 是服务器对象中的一个成员变量,代表用于接受连接的 Acceptor 对象。
async_accept 函数用于异步等待新的连接请求,并在连接请求到来时执行指定的处理函数。
2.net::make_strand(*mIoc): 这里使用 net::make_strand 函数来创建一个用于同步处理网络操作的 Strand 对象,
*mIoc 是指向服务器的 I/O 上下文对象的指针。Strand 可以确保在多个线程中对同一 I/O 上下文的访问是同步的,从而避免了线程安全性问题。
3.beast::bind_front_handler(&BoostServer::onAccept, this):
这部分使用了 Boost.Beast 库中的 bind_front_handler 函数来绑定处理函数 BoostServer::onAccept 到当前对象 this。
onAccept 是一个在接受新连接时调用的成员函数,用于处理新连接。
*/
void BoostServer::setOnAccept()
{
mAcceptor->async_accept(net::make_strand(*mIoc),
beast::bind_front_handler(&BoostServer::onAccept, this));
}
void BoostServer::onAccept(beast::error_code ec, tcp::socket socket)
{
if (ec)
{
LOGE("onAccept error: %s", ec.message().data());
return;
}
else
{
HttpServerConnection* conn = new HttpServerConnection(this, socket);
if (this->addConn(conn)) { // 加入新连接到容器里面
conn->setDisconnectionCallback(BoostServer::cbDisconnection, this); // 回调函数, 新连接断开或故障时内部触发,调用此回调函数
conn->run();
}
else { // 失败,如新连接存储不够或连接已经存在
delete conn;
conn = nullptr;
}
}
setOnAccept(); // 继续设置新的accept
}
bool BoostServer::addConn(HttpServerConnection* conn) {
m_connMap_mtx.lock();
if (m_connMap.find(conn->getSession()) != m_connMap.end()) {
m_connMap_mtx.unlock();
return false;
}
else {
m_connMap.insert(std::make_pair(conn->getSession(), conn));
m_connMap_mtx.unlock();
return true;
}
}
bool BoostServer::removeConn(std::string session) {
m_connMap_mtx.lock();
std::map<std::string, HttpServerConnection*>::iterator it = m_connMap.find(session);
if (it != m_connMap.end()) {
m_connMap.erase(it);
m_connMap_mtx.unlock();
return true;
}
else {
m_connMap_mtx.unlock();
return false;
}
}
HttpServerConnection* BoostServer::getConn(std::string session) {
m_connMap_mtx.lock();
std::map<std::string, HttpServerConnection*>::iterator it = m_connMap.find(session);
if (it != m_connMap.end()) {
m_connMap_mtx.unlock();
return it->second;
}
else {
m_connMap_mtx.unlock();
return nullptr;
}
}
void BoostServer::cbDisconnection(void* arg, std::string session) {
BoostServer* server = (BoostServer*)arg;
server->handleDisconnection(session);
}
bool BoostServer::sendData(char* data, int size) {
bool result = false;
m_connMap_mtx.lock();
std::map<std::string, HttpServerConnection*>::iterator it;
if (m_connMap.size() > 0) {
result = true;
}
//LOGI("实时在线客户端数量=%lld,size=%d", m_connMap.size(),size);
for (it = m_connMap.begin(); it != m_connMap.end(); it++) {
HttpServerConnection* conn = it->second;
}
m_connMap_mtx.unlock();
return result;
}
void BoostServer::handleDisconnection(std::string session) { // 根据session名称来找到这个连接,一旦连接断开,就会被彻底删除
LOGI("session=%s,disconnection", session.data());
HttpServerConnection* conn = this->getConn(session);
this->removeConn(session);
if (conn) {
delete conn;
conn = nullptr;
}
}
std::string BoostServer::generateSession()
{
std::string numStr;
numStr.append(std::to_string(rand() % 9 + 1));
numStr.append(std::to_string(rand() % 10));
numStr.append(std::to_string(rand() % 10));
numStr.append(std::to_string(rand() % 10));
numStr.append(std::to_string(rand() % 10));
numStr.append(std::to_string(rand() % 10));
numStr.append(std::to_string(rand() % 10));
numStr.append(std::to_string(rand() % 10));
//int num = stoi(numStr);
return numStr;
}
4.HttpServerConnection
1. HttpServerConnection.h
#ifndef HTTPSERVERCONNECTION_H
#define HTTPSERVERCONNECTION_H
#include "boost.h"
#include <queue>
#include <mutex>
class BoostServer;
class HttpServerConnection
{
public:
HttpServerConnection(BoostServer* server, tcp::socket& socket);
~HttpServerConnection();
public:
void run();
std::string getSession();
typedef void (*DisconnectionCallback)(void*, std::string);//(server, session)
void setDisconnectionCallback(DisconnectionCallback cb, void* arg) {
m_disconnectionCallback = cb;
m_arg = arg;
}
private:
std::string mSession;
BoostServer* mServer;
tcp::socket mSocket;
DisconnectionCallback m_disconnectionCallback = nullptr;
void* m_arg = nullptr;// server *
net::steady_timer mTimer{ mSocket.get_executor(), std::chrono::seconds(10) };
beast::flat_buffer mTempBuffer{ 8192 };
http::request<http::dynamic_body> mHttpRequest;
http::response<http::dynamic_body> mHttpResponse;
FILE* fp = nullptr;
void handle();
void keepWrite();
};
#endif //HTTPSERVERCONNECTION_H
2. HttpServerConnection.cpp
#include "HttpServerConnection.h"
#include "BoostServer.h"
#include "../Utils/Log.h"
HttpServerConnection::HttpServerConnection(BoostServer* server, tcp::socket& socket)
: mSession(server->generateSession()),
mServer(server),
mSocket(std::move(socket)) // 避免拷贝操作,提高效率
{
LOGI("");
}
HttpServerConnection::~HttpServerConnection() {
LOGI("");
if (fp) {
fclose(fp);
fp = nullptr;
}
this->mSocket.close();
this->mTimer.cancel();
}
std::string HttpServerConnection::getSession() {
return mSession;
}
void HttpServerConnection::run()
{
http::async_read(
mSocket,
mTempBuffer,
mHttpRequest,
[this](beast::error_code ec,
std::size_t bytes_transferred)
{
boost::ignore_unused(bytes_transferred);
if (ec) {
//接收可读数据失败
LOGE("run error,msg=%s", ec.message().data());
m_disconnectionCallback(m_arg, mSession);
}
else {
this->handle();
}
});
/*
mTimer.async_wait(
[this](beast::error_code ec)
{
if (ec)
{
}
}
);*/
}
void HttpServerConnection::handle()
{
mHttpResponse.version(mHttpRequest.version());
mHttpResponse.set(http::field::server, "Boost");
switch (mHttpRequest.method())
{
case http::verb::get:
{
if (mHttpRequest.target() == "/test.flv")
{
mHttpResponse.result(http::status::ok);
mHttpResponse.keep_alive(true);
mHttpResponse.set(http::field::access_control_allow_origin, "*");
mHttpResponse.set(http::field::content_type, "video/x-flv");
mHttpResponse.set(http::field::connection, "close");
mHttpResponse.set(http::field::expires, "-1");
mHttpResponse.set(http::field::pragma, "no-cache");
mHttpResponse.content_length(-1);
http::async_write(
mSocket,
mHttpResponse,
[this](beast::error_code ec, std::size_t)
{
if (ec)
{
//发送失败
LOGE("play flv error,msg=%s", ec.message().data());
//this->mSocket.shutdown(tcp::socket::shutdown_send, ec);
m_disconnectionCallback(m_arg, mSession);
}
else {
//发送成功
LOGI("play flv success");
//const char* filename = "../data/test.flv";
const char* filename = "../data/笑傲江湖天地作合.flv";
fp = fopen(filename, "rb");
this->keepWrite();
}
});
}
else
{
mHttpResponse.result(http::status::ok);
mHttpResponse.keep_alive(false);
mHttpResponse.result(http::status::not_found);
mHttpResponse.set(http::field::content_type, "text/plain");
beast::ostream(mHttpResponse.body()) << "File not found\r\n";
http::async_write(
mSocket,
mHttpResponse,
[this](beast::error_code ec, std::size_t)
{
if (ec)
{
//发送失败
LOGE("http::async_write error,msg=%s", ec.message().data());
//this->mSocket.shutdown(tcp::socket::shutdown_send, ec
}
else {
//发送成功
LOGI("http::async_write success,msg=%s", ec.message().data());
}
m_disconnectionCallback(m_arg, mSession);
});
}
break;
}
default:
{
mHttpResponse.result(http::status::bad_request);
mHttpResponse.set(http::field::content_type, "text/plain");
beast::ostream(mHttpResponse.body())
<< "Invalid request-method '"
<< std::string(mHttpRequest.method_string())
<< "'";
mHttpResponse.content_length(mHttpResponse.body().size());
http::async_write(
mSocket,
mHttpResponse,
[this](beast::error_code ec, std::size_t)
{
if (ec)
{
//发送失败
LOGE("http::async_write error,msg=%s", ec.message().data());
//this->mSocket.shutdown(tcp::socket::shutdown_send, ec);
}
else {
//发送成功
LOGI("http::async_write success,msg=%s", ec.message().data());
}
m_disconnectionCallback(m_arg, mSession);
});
break;
}
}
}
void HttpServerConnection::keepWrite() {
char data[5000];
int size;
size = fread(data, 1, sizeof(data), fp);
if (size > 0) {
boost::asio::async_write(
mSocket,
boost::asio::buffer(data, size),
[this](beast::error_code ec, std::size_t)
{
if (ec)
{
//发送失败
LOGE("keepWrite error,msg=%s", ec.message().data());
m_disconnectionCallback(m_arg, mSession);
}
else {
//发送成功
//LOGI("keepWrite successs");
this->keepWrite();
}
});
}
else {
LOGE("keepWrite error,msg= flv buffer finish");
m_disconnectionCallback(m_arg, mSession);
}
}
三、第三方库介绍:
参考资料:
总结
通过这次学习和实践,成功实现了一个高性能的HTTP-FLV流媒体服务器,并对各个主要功能模块进行了详细的介绍,包括连接管理、数据传输、视频编码解码等方面的内容。。此外,我还介绍了两个在项目中使用的第三方库——Boost和JsonCpp。Boost库提供了强大的异步I/O功能,极大地提高了服务器的性能和并发处理能力;JsonCpp库则简化了JSON数据的解析和生成过程,使得数据处理更加高效和便捷。