ZLMediaKit源码分析(六)http 点播

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 请求。不知是何含义。
使用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 
### 回答1: zlmediakit是一个开源的流媒体服务器软件,其源码可以用于搭建自己的流媒体服务器。该软件使用C++编写,具有高性能和低资源消耗的特点。 zlmediakit源码提供了丰富的功能和模块,可以支持RTSP、RTMP、HLS、HTTP/HTTPS等流媒体协议的直播和点播。它可以用于构建具有较高并发量的流媒体平台,适用于各种场景,如视频直播、音频直播、视频点播等。 zlmediakit源码采用了多线程和事件驱动的设计,可以同时处理多个客户端连接和媒体流传输。它还支持实时录制功能,可以将接收到的流媒体数据实时保存到本地磁盘中,方便后续回放和存储。 zlmediakit源码的使用相对较为简单,只需要在服务器上编译和安装即可。同时,它还提供了丰富的配置选项和API接口,以便于用户进行个性化定制和二次开发。 总之,zlmediakit源码是一个强大而灵活的流媒体服务器软件,通过使用它,用户可以搭建自己的流媒体平台,实现高并发的流媒体传输和处理,适用于各种直播和点播场景。 ### 回答2: zlmediakit是一款基于C++语言开发的开源流媒体解决方案,旨在提供高性能的实时音视频传输和处理功能。其源码提供了丰富的功能和模块,可以用于构建各种音视频应用。 zlmediakit源码具有的特点包括: 1. 高性能:通过使用底层优化技术和多线程处理,zlmediakit能够实现高效的音视频传输和处理,保证了应用的实时性和流畅性。 2. 支持多种协议:zlmediakit支持常见的音视频传输协议,如RTSP、RTMP、HTTP等,使得应用能够与各类设备和平台进行互通。 3. 灵活的扩展性:zlmediakit源码提供了丰富的接口和模块,可以根据具体需求进行定制和扩展,满足不同应用场景的需求。 4. 多平台支持:zlmediakit源码可以在多个平台上运行,如Windows、Linux等,且可以与常见的开发框架和工具协同使用。 5. 丰富的功能:zlmediakit提供了各种功能模块,如音视频编码、解码、录制、转码、推流、拉流等,可以实现多种实时音视频处理需求。 通过使用zlmediakit源码,开发者可以快速构建和部署高性能的音视频应用,如视频直播、视频会议、监控系统等。同时,源码的开放性也意味着开发者可以根据自己的需求进行二次开发和定制,以满足更加复杂的应用场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值