实现一个高性能的HTTP-FLV流媒体服务器


前言

在当今的互联网时代,流媒体技术已经成为传输音视频内容的核心技术之一。为了更好地理解和应用流媒体技术,开始学习并实现了一个高性能的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);
    }

}



三、第三方库介绍:

Boost网络库
【C++】开源:jsoncpp库


参考资料:

RTMP、HTTP-FLV、HLS三大直播协议

总结

通过这次学习和实践,成功实现了一个高性能的HTTP-FLV流媒体服务器,并对各个主要功能模块进行了详细的介绍,包括连接管理、数据传输、视频编码解码等方面的内容。。此外,我还介绍了两个在项目中使用的第三方库——Boost和JsonCpp。Boost库提供了强大的异步I/O功能,极大地提高了服务器的性能和并发处理能力;JsonCpp库则简化了JSON数据的解析和生成过程,使得数据处理更加高效和便捷。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
nginx-1.19.3-http-flv是一种基于Nginx服务器HTTP-FLV流媒体协议模块。HTTP-FLV指的是基于HTTP协议传输的FLV格式视频流。下面是对nginx-1.19.3-http-flv的简要说明: Nginx是一个轻量级的高性能Web服务器,常用于反向代理、负载均衡和HTTP缓存等。而nginx-1.19.3-http-flv是基于Nginx的一个开源模块,用于支持HTTP-FLV流媒体协议。 HTTP-FLV是一种用于在Web上通过HTTP协议传输的流媒体协议。它的特点是可以通过HTTP协议直接发送FLV格式的音视频数据流到客户端,而无需使用RTMP等专有协议。这使得HTTP-FLV在Web端播放视频时非常方便,并且能够兼容现有的HTTP基础设施,如CDN、防火墙等。 Nginx的http-flv模块允许将FLV格式的音视频文件转为HTTP-FLV流,通过HTTP协议传输到客户端。它可以作为一个流媒体服务器,接收FLV数据流的输入,并通过HTTP-FLV的方式将数据流分发给客户端。 Nginx提供了高性能的数据传输和处理能力,能够有效地处理大量的并发请求。通过使用nginx-1.19.3-http-flv模块,可以方便地搭建一个高性能HTTP-FLV流媒体服务器实现优质的视频传输和播放体验。 使用nginx-1.19.3-http-flv模块,可以在Web端实现基于FLV格式的实时音视频传输和播放。这对于直播、互动视频和在线教育等领域非常有用。同时,nginx-1.19.3-http-flv也可以与其他模块(如HLS模块、RTMP模块)配合使用,实现更丰富的流媒体服务。总之,nginx-1.19.3-http-flv提供了一种高效、稳定和灵活的流媒体解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值