ZLMediaKit源码分析(一)服务启动
ZLMediaKit源码分析(二)推流创建
ZLMediaKit源码分析(三)拉流创建
ZLMediaKit源码分析(四)重封装
ZLMediaKit源码分析(五)代理服务
ZLMediaKit源码分析(六)http 点播
ZLMediaKit 使用 http1.1,而 http1.1 是基于 TCP 的。
HttpSession::onRecv() 接收数据
这里是 http 服务的数据入口
src/Http/HttpSession.cpp
void HttpSession::onRecv(const Buffer::Ptr &pBuf) {
_ticker.resetTime();
input(pBuf->data(),pBuf->size());
}
HttpSession 继承自 HttpRequestSplitter
HttpRequestSplitter::input() 调用 onRecvHeader()
Http/HttpRequestSplitter.cpp
void HttpRequestSplitter::input(const char *data,size_t len) {
{
auto size = remainDataSize();
if (size > kMaxCacheSize) {
//缓存太多数据无法处理则上抛异常
reset();
throw std::out_of_range("remain data size is too huge, now cleared:" + to_string(size));
}
}
const char *ptr = data;
if(!_remain_data.empty()){
_remain_data.append(data,len);
data = ptr = _remain_data.data();
len = _remain_data.size();
}
splitPacket:
/*确保ptr最后一个字节是0,防止strstr越界
*由于ZLToolKit确保内存最后一个字节是保留未使用字节并置0,
*所以此处可以不用再次置0
*但是上层数据可能来自其他渠道,保险起见还是置0
*/
char &tail_ref = ((char *) ptr)[len];
char tail_tmp = tail_ref;
tail_ref = 0;
//数据按照请求头处理
const char *index = nullptr;
_remain_data_size = len;
while (_content_len == 0 && _remain_data_size > 0 && (index = onSearchPacketTail(ptr,_remain_data_size)) != nullptr) {
if (index == ptr) {
break;
}
if (index < ptr || index > ptr + _remain_data_size) {
throw std::out_of_range("上层分包逻辑异常");
}
//_content_len == 0,这是请求头
const char *header_ptr = ptr;
ssize_t header_size = index - ptr;
ptr = index;
_remain_data_size = len - (ptr - data);
_content_len = onRecvHeader(header_ptr, header_size);
}
// 到这里返回了,以下未执行
if(_remain_data_size <= 0){
//没有剩余数据,清空缓存
_remain_data.clear();
return;
}
// 以下均为执行 省略部分
......
//_content_len < 0;数据按照不固定长度content处理
onRecvContent(ptr,_remain_data_size);//消费掉所有剩余数据
_remain_data.clear();
}
src/Http/HttpSession.cpp
ssize_t HttpSession::onRecvHeader(const char *header,size_t len) {
typedef void (HttpSession::*HttpCMDHandle)(ssize_t &);
static unordered_map<string, HttpCMDHandle> s_func_map;
static onceToken token([]() {
s_func_map.emplace("GET",&HttpSession::Handle_Req_GET);
s_func_map.emplace("POST",&HttpSession::Handle_Req_POST);
s_func_map.emplace("HEAD",&HttpSession::Handle_Req_HEAD);
s_func_map.emplace("OPTIONS",&HttpSession::Handle_Req_OPTIONS);
}, nullptr);
_parser.Parse(header);
urlDecode(_parser);
string cmd = _parser.Method();
auto it = s_func_map.find(cmd);
if (it == s_func_map.end()) {
WarnP(this) << "不支持该命令:" << cmd;
sendResponse(405, true);
return 0;
}
//跨域
_origin = _parser["Origin"];
//默认后面数据不是content而是header
ssize_t content_len = 0;
auto &fun = it->second;
try {
(this->*fun)(content_len);
}catch (exception &ex){
shutdown(SockException(Err_shutdown,ex.what()));
}
//清空解析器节省内存
_parser.Clear();
//返回content长度
return content_len;
}
HttpSession::Handle_Req_GET()
src/Http/HttpSession.cpp
void HttpSession::Handle_Req_GET(ssize_t &content_len) {
Handle_Req_GET_l(content_len, true);
}
// 直接执行 Handle_Req_GET_l
void HttpSession::Handle_Req_GET_l(ssize_t &content_len, bool sendBody) {
//先看看是否为WebSocket请求
if (checkWebSocket()) {
content_len = -1;
_contentCallBack = [this](const char *data, size_t len) {
WebSocketSplitter::decode((uint8_t *) data, len);
//_contentCallBack是可持续的,后面还要处理后续数据
return true;
};
return;
}
if (emitHttpEvent(false)) {
//拦截http api事件
return;
}
if (checkLiveStreamFlv()) {
//拦截http-flv播放器
return;
}
if (checkLiveStreamTS()) {
//拦截http-ts播放器
return;
}
if (checkLiveStreamFMP4()) {
//拦截http-fmp4播放器
return;
}
bool bClose = !strcasecmp(_parser["Connection"].data(),"close");
weak_ptr<HttpSession> weakSelf = dynamic_pointer_cast<HttpSession>(shared_from_this());
// 回调是这里的 lamda 函数
HttpFileManager::onAccessPath(*this, _parser, [weakSelf, bClose](int code, const string &content_type,
const StrCaseMap &responseHeader, const HttpBody::Ptr &body) {
auto strongSelf = weakSelf.lock();
if (!strongSelf) {
return;
}
// HttpSession继承自 TcpSession 继承自 Session 继承自SocketHelper
strongSelf->async([weakSelf, bClose, code, content_type, responseHeader, body]() {
auto strongSelf = weakSelf.lock();
if (!strongSelf) {
return;
}
strongSelf->sendResponse(code, bClose, content_type.data(), responseHeader, body);
});
});
}
HttpFileManager::onAccessPath() 鉴权
src/Http/HttpFileManager.cpp
/**
* 访问文件或文件夹
* @param sender 事件触发者
* @param parser http请求
* @param cb 回调对象
*/
void HttpFileManager::onAccessPath(TcpSession &sender, Parser &parser, const HttpFileManager::invoker &cb) {
auto fullUrl = string(HTTP_SCHEMA) + "://" + parser["Host"] + parser.FullUrl();
MediaInfo mediaInfo(fullUrl);
auto strFile = getFilePath(parser, mediaInfo, sender);
// strFile:/home/*/bin/www/proxy/file/720x1280p30.wintersecret59s_file.mp4
// 这里只分析文件,未执行下面文件夹操作
//访问的是文件夹
if (File::is_dir(strFile.data())) {
auto indexFile = searchIndexFile(strFile);
if (!indexFile.empty()) {
//发现该文件夹下有index文件
strFile = pathCat(strFile, indexFile);
parser.setUrl(pathCat(parser.Url(), indexFile));
accessFile(sender, parser, mediaInfo, strFile, cb);
return;
}
string strMenu;
//生成文件夹菜单索引
if (!makeFolderMenu(parser.Url(), strFile, strMenu)) {
//文件夹不存在
sendNotFound(cb);
return;
}
//判断是否有权限访问该目录
canAccessPath(sender, parser, mediaInfo, true, [strMenu, cb](const string &errMsg, const HttpServerCookie::Ptr &cookie) mutable{
if (!errMsg.empty()) {
strMenu = errMsg;
}
StrCaseMap headerOut;
if (cookie) {
headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kCookieName].get<HttpCookieAttachment>()._path);
}
cb(errMsg.empty() ? 200 : 401, "text/html", headerOut, std::make_shared<HttpStringBody>(strMenu));
});
return;
}
//访问的是文件
accessFile(sender, parser, mediaInfo, strFile, cb);
};
accessFile() 判断文件访问权限
静态函数。
src/Http/HttpFileManager.cpp
/**
* 访问文件
* @param sender 事件触发者
* @param parser http请求
* @param mediaInfo http url信息
* @param strFile 文件绝对路径
* @param cb 回调对象
*/
static void accessFile(TcpSession &sender, const Parser &parser, const MediaInfo &mediaInfo, const string &strFile, const HttpFileManager::invoker &cb) {
bool is_hls = end_with(strFile, kHlsSuffix);
bool file_exist = File::is_file(strFile.data());
if (!is_hls && !file_exist) {
//文件不存在且不是hls,那么直接返回404
sendNotFound(cb);
return;
}
if (is_hls) {
//hls,那么移除掉后缀获取真实的stream_id并且修改协议为HLS
const_cast<string &>(mediaInfo._schema) = HLS_SCHEMA;
replace(const_cast<string &>(mediaInfo._streamid), kHlsSuffix, "");
}
weak_ptr<TcpSession> weakSession = sender.shared_from_this();
//判断是否有权限访问该文件
// canAccessPath 函数内部有鉴权操作
canAccessPath(sender, parser, mediaInfo, false, [cb, strFile, parser, is_hls, mediaInfo, weakSession , file_exist](const string &errMsg, const HttpServerCookie::Ptr &cookie) {
auto strongSession = weakSession.lock();
if (!strongSession) {
//http客户端已经断开,不需要回复
return;
}
if (!errMsg.empty()) {
//文件鉴权失败
StrCaseMap headerOut;
if (cookie) {
auto lck = cookie->getLock();
headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kCookieName].get<HttpCookieAttachment>()._path);
}
cb(401, "text/html", headerOut, std::make_shared<HttpStringBody>(errMsg));
return;
}
// 主要执行里面的回调函数
auto response_file = [file_exist](const HttpServerCookie::Ptr &cookie, const HttpFileManager::invoker &cb, const string &strFile, const Parser &parser) {
StrCaseMap httpHeader;
if (cookie) {
auto lck = cookie->getLock();
httpHeader["Set-Cookie"] = cookie->getCookie((*cookie)[kCookieName].get<HttpCookieAttachment>()._path);
}
HttpSession::HttpResponseInvoker invoker = [&](int code, const StrCaseMap &headerOut, const HttpBody::Ptr &body) {
if (cookie && file_exist) {
auto lck = cookie->getLock();
auto is_hls = (*cookie)[kCookieName].get<HttpCookieAttachment>()._is_hls;
if (is_hls) {
(*cookie)[kCookieName].get<HttpCookieAttachment>()._hls_data->addByteUsage(body->remainSize());
}
}
// run
// Handle_Req_GET() 注册lamda函数, 最终执行HttpSession::sendResponse()
cb(code, HttpFileManager::getContentType(strFile.data()), headerOut, body);
};
invoker.responseFile(parser.getHeader(), httpHeader, strFile);
};
if (!is_hls) {
//不是hls,直接回复文件或404
// 执行这里
response_file(cookie, cb, strFile, parser);
return;
}
// 以下未执行
//是hls直播,判断HLS直播流是否已经注册
bool have_find_media_src = false;
if (cookie) {
auto lck = cookie->getLock();
have_find_media_src = (*cookie)[kCookieName].get<HttpCookieAttachment>()._have_find_media_source;
if (!have_find_media_src) {
(*cookie)[kCookieName].get<HttpCookieAttachment>()._have_find_media_source = true;
}
}
if (have_find_media_src) {
//之前该cookie已经通过MediaSource::findAsync查找过了,所以现在只以文件系统查找结果为准
response_file(cookie, cb, strFile, parser);
return;
}
//hls文件不存在,我们等待其生成并延后回复
MediaSource::findAsync(mediaInfo, strongSession, [response_file, cookie, cb, strFile, parser](const MediaSource::Ptr &src) {
if (cookie) {
auto lck = cookie->getLock();
//尝试添加HlsMediaSource的观看人数(HLS是按需生成的,这样可以触发HLS文件的生成)
(*cookie)[kCookieName].get<HttpCookieAttachment>()._hls_data->addByteUsage(0);
}
if (src && File::is_file(strFile.data())) {
//流和m3u8文件都存在,那么直接返回文件
response_file(cookie, cb, strFile, parser);
return;
}
auto hls = dynamic_pointer_cast<HlsMediaSource>(src);
if (!hls) {
//流不存在,那么直接返回文件(相当于纯粹的HLS文件服务器,但是会挂起播放器15秒左右(用于等待HLS流的注册))
response_file(cookie, cb, strFile, parser);
return;
}
//流存在,但是m3u8文件不存在,那么等待生成m3u8文件(HLS源注册后,并不会立即生成HLS文件,有人观看才会按需生成HLS文件)
hls->waitForFile([response_file, cookie, cb, strFile, parser]() {
response_file(cookie, cb, strFile, parser);
});
});
});
}
NoticeCenter::Instance().emitEvent() 触发 hook 鉴权函数
这里执行空操作。
src/Http/HttpFileManager.cpp
/**
* 判断http客户端是否有权限访问文件的逻辑步骤
* 1、根据http请求头查找cookie,找到进入步骤3
* 2、根据http url参数查找cookie,如果还是未找到cookie则进入步骤5
* 3、cookie标记是否有权限访问文件,如果有权限,直接返回文件
* 4、cookie中记录的url参数是否跟本次url参数一致,如果一致直接返回客户端错误码
* 5、触发kBroadcastHttpAccess事件
*/
static void canAccessPath(TcpSession &sender, const Parser &parser, const MediaInfo &mediaInfo, bool is_dir,
const function<void(const string &errMsg, const HttpServerCookie::Ptr &cookie)> &callback) {
//获取用户唯一id
auto uid = parser.Params();
auto path = parser.Url();
//先根据http头中的cookie字段获取cookie
HttpServerCookie::Ptr cookie = HttpCookieManager::Instance().getCookie(kCookieName, parser.getHeader());
//如果不是从http头中找到的cookie,我们让http客户端设置下cookie
bool cookie_from_header = true;
if (!cookie && !uid.empty()) {
//客户端请求中无cookie,再根据该用户的用户id获取cookie
cookie = HttpCookieManager::Instance().getCookieByUid(kCookieName, uid);
cookie_from_header = false;
}
// 未走这里
if (cookie) {
//找到了cookie,对cookie上锁先
auto lck = cookie->getLock();
auto attachment = (*cookie)[kCookieName].get<HttpCookieAttachment>();
if (path.find(attachment._path) == 0) {
//上次cookie是限定本目录
if (attachment._err_msg.empty()) {
//上次鉴权成功
if (attachment._is_hls) {
//如果播放的是hls,那么刷新hls的cookie(获取ts文件也会刷新)
cookie->updateTime();
cookie_from_header = false;
}
callback("", cookie_from_header ? nullptr : cookie);
return;
}
//上次鉴权失败,但是如果url参数发生变更,那么也重新鉴权下
if (parser.Params().empty() || parser.Params() == cookie->getUid()) {
//url参数未变,或者本来就没有url参数,那么判断本次请求为重复请求,无访问权限
callback(attachment._err_msg, cookie_from_header ? nullptr : cookie);
return;
}
}
//如果url参数变了或者不是限定本目录,那么旧cookie失效,重新鉴权
HttpCookieManager::Instance().delCookie(cookie);
}
bool is_hls = mediaInfo._schema == HLS_SCHEMA;
SockInfoImp::Ptr info = std::make_shared<SockInfoImp>();
info->_identifier = sender.getIdentifier();
info->_peer_ip = sender.get_peer_ip();
info->_peer_port = sender.get_peer_port();
info->_local_ip = sender.get_local_ip();
info->_local_port = sender.get_local_port();
//该用户从来未获取过cookie,这个时候我们广播是否允许该用户访问该http目录
HttpSession::HttpAccessPathInvoker accessPathInvoker = [callback, uid, path, is_dir, is_hls, mediaInfo, info]
(const string &errMsg, const string &cookie_path_in, int cookieLifeSecond) {
HttpServerCookie::Ptr cookie;
if (cookieLifeSecond) {
//本次鉴权设置了有效期,我们把鉴权结果缓存在cookie中
string cookie_path = cookie_path_in;
if (cookie_path.empty()) {
//如果未设置鉴权目录,那么我们采用当前目录
cookie_path = is_dir ? path : path.substr(0, path.rfind("/") + 1);
}
cookie = HttpCookieManager::Instance().addCookie(kCookieName, uid, cookieLifeSecond);
//对cookie上锁
auto lck = cookie->getLock();
HttpCookieAttachment attachment;
//记录用户能访问的路径
attachment._path = cookie_path;
//记录能否访问
attachment._err_msg = errMsg;
//记录访问的是否为hls
attachment._is_hls = is_hls;
if (is_hls) {
//hls相关信息
attachment._hls_data = std::make_shared<HlsCookieData>(mediaInfo, info);
//hls未查找MediaSource
attachment._have_find_media_source = false;
}
(*cookie)[kCookieName].set<HttpCookieAttachment>(std::move(attachment));
callback(errMsg, cookie);
} else {
callback(errMsg, nullptr);
}
};
if (is_hls) {
//是hls的播放鉴权,拦截之
emitHlsPlayed(parser, mediaInfo, accessPathInvoker, sender);
return;
}
//事件未被拦截,则认为是http下载请求
//直接执行了这里,鉴权相关。因为没有开启鉴权,这里执行了空判断,直接返回了。
bool flag = NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastHttpAccess, parser, path, is_dir, accessPathInvoker, static_cast<SockInfo &>(sender));
if (!flag) {
//此事件无人监听,我们默认都有权限访问
callback("", nullptr);
}
}
发送响应数据
同步操作。
3rdpart/ZLToolKit/src/Network/Socket.cpp
Task::Ptr SocketHelper::async(TaskIn task, bool may_sync) {
return _poller->async(std::move(task), may_sync);
}
HttpSession::sendResponse() 响应数据
src/Http/HttpSession.cpp
void HttpSession::sendResponse(int code,
bool bClose,
const char *pcContentType,
const HttpSession::KeyValue &header,
const HttpBody::Ptr &body,
bool no_content_length ){
GET_CONFIG(string,charSet,Http::kCharSet);
GET_CONFIG(uint32_t,keepAliveSec,Http::kKeepAliveSecond);
//body默认为空
ssize_t size = 0;
if (body && body->remainSize()) {
//有body,获取body大小
size = body->remainSize();
}
if(no_content_length){
//http-flv直播是Keep-Alive类型
bClose = false;
}else if((size_t) size >= SIZE_MAX || size < 0 ){
//不固定长度的body,那么发送完body后应该关闭socket,以便浏览器做下载完毕的判断
bClose = true;
}
HttpSession::KeyValue &headerOut = const_cast<HttpSession::KeyValue &>(header);
headerOut.emplace(kDate, dateStr());
headerOut.emplace(kServer, SERVER_NAME);
headerOut.emplace(kConnection, bClose ? "close" : "keep-alive");
if(!bClose){
string keepAliveString = "timeout=";
keepAliveString += to_string(keepAliveSec);
keepAliveString += ", max=100";
headerOut.emplace(kKeepAlive,std::move(keepAliveString));
}
if(!_origin.empty()){
//设置跨域
headerOut.emplace(kAccessControlAllowOrigin,_origin);
headerOut.emplace(kAccessControlAllowCredentials, "true");
}
if(!no_content_length && size >= 0 && (size_t)size < SIZE_MAX){
//文件长度为固定值,且不是http-flv强制设置Content-Length
headerOut[kContentLength] = to_string(size);
}
if(size && !pcContentType){
//有body时,设置缺省类型
pcContentType = "text/plain";
}
if((size || no_content_length) && pcContentType){
//有body时,设置文件类型
string strContentType = pcContentType;
strContentType += "; charset=";
strContentType += charSet;
headerOut.emplace(kContentType,std::move(strContentType));
}
//发送http头
string str;
str.reserve(256);
str += "HTTP/1.1 " ;
str += to_string(code);
str += ' ';
str += getHttpStatusMessage(code) ;
str += "\r\n";
for (auto &pr : header) {
str += pr.first ;
str += ": ";
str += pr.second;
str += "\r\n";
}
str += "\r\n";
SockSender::send(std::move(str));
_ticker.resetTime();
if(!size){
//没有body
if(bClose){
shutdown(SockException(Err_shutdown,StrPrinter << "close connection after send http header completed with status code:" << code));
}
return;
}
GET_CONFIG(uint32_t, sendBufSize, Http::kSendBufSize);
if(body->remainSize() > sendBufSize){
//文件下载提升发送性能
setSocketFlags();
}
//发送http body
AsyncSenderData::Ptr data = std::make_shared<AsyncSenderData>(shared_from_this(),body,bClose);
getSock()->setOnFlush([data](){
// 先走下面onSocketFlushed, 然后再走这里
return AsyncSender::onSocketFlushed(data);
});
// 先走这里,然后再走上面的回调onSocketFlushed
AsyncSender::onSocketFlushed(data);
}
AsyncSenderData 组装数据
src/Http/HttpSession.cpp
class AsyncSenderData {
public:
friend class AsyncSender;
typedef std::shared_ptr<AsyncSenderData> Ptr;
AsyncSenderData(const TcpSession::Ptr &session, const HttpBody::Ptr &body, bool close_when_complete) {
_session = dynamic_pointer_cast<HttpSession>(session);
_body = body;
_close_when_complete = close_when_complete;
}
~AsyncSenderData() = default;
private:
std::weak_ptr<HttpSession> _session;
// HttpBody 派生出三个类 HttpStringBody、HttpFileBody、HttpMultiFormBody
HttpBody::Ptr _body;
bool _close_when_complete;
bool _read_complete = false;
};
AsyncSender::onSocketFlushed() 读取并发送数据
src/Http/HttpSession.cpp
class AsyncSender {
public:
typedef std::shared_ptr<AsyncSender> Ptr;
static bool onSocketFlushed(const AsyncSenderData::Ptr &data) {
if (data->_read_complete) {
if (data->_close_when_complete) {
//发送完毕需要关闭socket
shutdown(data->_session.lock());
}
return false;
}
GET_CONFIG(uint32_t, sendBufSize, Http::kSendBufSize);
// HttpBody 派生出三个类 HttpStringBody、HttpFileBody、HttpMultiFormBody
// 这里取 HttpBody::readDataAsync()
// 从文件中读取数据,然后调用回调返回
data->_body->readDataAsync(sendBufSize, [data](const Buffer::Ptr &sendBuf) {
auto session = data->_session.lock();
if (!session) {
//本对象已经销毁
return;
}
session->async([data, sendBuf]() {
auto session = data->_session.lock();
if (!session) {
//本对象已经销毁
return;
}
// 发送数据。
onRequestData(data, session, sendBuf);
}, false);
});
return true;
}
private:
static void onRequestData(const AsyncSenderData::Ptr &data, const std::shared_ptr<HttpSession> &session, const Buffer::Ptr &sendBuf) {
session->_ticker.resetTime();
// HttpSession继承自 TcpSession 继承自 Session 继承自SocketHelper
if (sendBuf && session->send(sendBuf) != -1) {
//文件还未读完,还需要继续发送
if (!session->isSocketBusy()) {
//socket还可写,继续请求数据
onSocketFlushed(data);
}
return;
}
//文件写完了
data->_read_complete = true;
if (!session->isSocketBusy() && data->_close_when_complete) {
shutdown(session);
}
}
static void shutdown(const std::shared_ptr<HttpSession> &session) {
if(session){
session->shutdown(SockException(Err_shutdown, StrPrinter << "close connection after send http body completed."));
}
}
};
HttpBody::readDataAsync() 读取文件
src/Http/HttpBody.h
/**
* http content部分基类定义
*/
class HttpBody : public std::enable_shared_from_this<HttpBody>{
public:
typedef std::shared_ptr<HttpBody> Ptr;
HttpBody(){}
virtual ~HttpBody(){}
/**
* 剩余数据大小,如果返回-1, 那么就不设置content-length
*/
virtual ssize_t remainSize() { return 0;};
/**
* 读取一定字节数,返回大小可能小于size
* @param size 请求大小
* @return 字节对象,如果读完了,那么请返回nullptr
*/
virtual Buffer::Ptr readData(size_t size) { return nullptr;};
/**
* 异步请求读取一定字节数,返回大小可能小于size
* @param size 请求大小
* @param cb 回调函数
*/
virtual void readDataAsync(size_t size,const function<void(const Buffer::Ptr &buf)> &cb){
//由于unix和linux是通过mmap的方式读取文件,所以把读文件操作放在后台线程并不能提高性能
//反而会由于频繁的线程切换导致性能降低以及延时增加,所以我们默认同步获取文件内容
//(其实并没有读,拷贝文件数据时在内核态完成文件读)
cb(readData(size));
}
};
src/Http/HttpBody.cpp
Buffer::Ptr HttpFileBody::readData(size_t size) {
size = MIN((size_t)remainSize(),size);
if(!size){
//没有剩余字节了
return nullptr;
}
if(!_map_addr){
//fread模式
ssize_t iRead;
auto ret = _pool.obtain();
ret->setCapacity(size + 1);
do{
iRead = fread(ret->data(), 1, size, _fp.get());
}while(-1 == iRead && UV_EINTR == get_uv_error(false));
if(iRead > 0){
//读到数据了
ret->setSize(iRead);
_offset += iRead;
return std::move(ret);
}
//读取文件异常,文件真实长度小于声明长度
_offset = _max_size;
WarnL << "read file err:" << get_uv_errmsg();
return nullptr;
}
//mmap模式
auto ret = std::make_shared<BufferMmap>(_map_addr,_offset,size);
_offset += size;
return ret;
}
SocketHelper::send() 发送数据
3rdpart/ZLToolKit/src/Network/Socket.cpp
ssize_t SocketHelper::send(Buffer::Ptr buf) {
if (!_sock) {
return -1;
}
return _sock->send(std::move(buf), nullptr, 0, _try_flush);
}
http 点播实例
config.ini
[http]
#http服务器字符编码,windows上默认gb2312
charSet=utf-8
#http链接超时时间
keepAliveSecond=30
#http请求体最大字节数,如果post的body太大,则不适合缓存body在内存
maxReqSize=40960
#404网页内容,用户可以自定义404网页
#notFound=<html><head><title>404 Not Found</title></head><body bgcolor="white"><center><h1>您访问的资源不存在!</h1></center><hr><center>ZLMediaKit-4.0</center></body></html>
#http服务器监听端口
#port=80
port=806
#http文件服务器根目录
#可以为相对(相对于本可执行程序目录)或绝对路径
rootPath=./www
#http文件服务器读文件缓存大小,单位BYTE,调整该参数可以优化文件io性能
sendBufSize=65536
#https服务器监听端口
#sslport=443
sslport=4436
#是否显示文件夹菜单,开启后可以浏览文件夹
dirMenu=1
#虚拟目录, 虚拟目录名和文件路径使用","隔开,多个配置路径间用";"隔开
#例如赋值为 app_a,/path/to/a;app_b,/path/to/b 那么
#访问 http://127.0.0.1/app_a/file_a 对应的文件路径为 /path/to/a/file_a
#访问 http://127.0.0.1/app_b/file_b 对应的文件路径为 /path/to/b/file_b
#访问其他http路径,对应的文件路径还是在rootPath内
virtualPath=app_a,./www/proxy/file
#禁止后缀的文件使用mmap缓存,使用“,”隔开
#例如赋值为 .mp4,.flv
#那么访问后缀为.mp4与.flv 的文件不缓存
forbidCacheSuffix=
#可以把http代理前真实客户端ip放在http头中:https://github.com/ZLMediaKit/ZLMediaKit/issues/1388
#切勿暴露此key,否则可能导致伪造客户端ip
forwarded_ip_header=
port 默认为 80 可以不用改,我这里改成了 806;
rootPath 是 http 服务的根目录地址,默认是./www(相对于 MediaServer),需要自行创建目录,如下:
test@zlmediakit:~$ tree build/
build/
├── bin
│ ├── log
│ │ └── 2023-07-20_00.log
│ ├── MediaServer
│ └── www
│ ├── proxy
│ │ ├── 720x1280p30.wintersecret59s.mp4
│ │ ├── 720x1280p30.wintersecret59s.flv
│ │ └── file
│ │ └── 720x1280p30.wintersecret59s_file.mp4
│ └── record
├── config
│ └── config.ini
├── include
│ ├── mk_common.h
│ ├── mk_events.h
│ ├── mk_events_objects.h
│ ├── mk_export.h
│ ├── mk_frame.h
│ ├── mk_h264_splitter.h
│ ├── mk_httpclient.h
│ ├── mk_media.h
│ ├── mk_mediakit.h
│ ├── mk_player.h
│ ├── mk_proxyplayer.h
│ ├── mk_pusher.h
│ ├── mk_recorder.h
│ ├── mk_rtp_server.h
│ ├── mk_tcp.h
│ ├── mk_thread.h
│ ├── mk_track.h
│ ├── mk_transcode.h
│ └── mk_util.h
└── lib
└── libmk_api.so
9 directories, 26 files
test@zlmediakit:~$
vlc 拉流:
# proxy 为 appName,对应.www/proxy
http://10.49.44.60:806/proxy/720x1280p30.wintersecret59s.mp4
http://10.49.44.60:806/proxy/720x1280p30.wintersecret59s.flv
# ./www/proxy/目录下还可以再创建目录,但是需要在拉流的时候对应添加
http://10.49.44.60:806/proxy/file/720x1280p30.wintersecret59s_file.mp4
# 使用 virtualPath
http://10.49.44.60:806/app_a/720x1280p30.wintersecret59s_file.mp4
Ps. FFmpeg MP4 转 FLV
ffmpeg -i 720x1280p30.wintersecret59s.mp4 -vcodec copy -acodec copy 720x1280p30.wintersecret59s.mp4.flv
参考官方wiki:
https://gitee.com/xia-chu/ZLMediaKit/wikis/Home
遇到的疑惑
使用 VLC 拉流的时候,服务器日志显示:有两次 http 请求。不知是何含义。
使用 FFmpeg 拉流的时候,http 请求次数更多。
ffmpeg -y -i http://10.49.44.60:806/proxy/720x1280p30.wintersecret59s.mp4 -c:v copy -c:a copy -f mp4 /dev/null