diff --git a/3rdpart/ZLToolKit b/3rdpart/ZLToolKit index 72013f51..be08f018 160000 --- a/3rdpart/ZLToolKit +++ b/3rdpart/ZLToolKit @@ -1 +1 @@ -Subproject commit 72013f5128d8c04bad8b973370da463c311081c0 +Subproject commit be08f01869ebf1391245c24c1e328422eb11ab77 diff --git a/api/source/mk_common.cpp b/api/source/mk_common.cpp index 19849621..15eff659 100755 --- a/api/source/mk_common.cpp +++ b/api/source/mk_common.cpp @@ -70,8 +70,10 @@ API_EXPORT void API_CALL mk_stop_all_server(){ CLEAR_ARR(rtsp_server); CLEAR_ARR(rtmp_server); CLEAR_ARR(http_server); +#ifdef ENABLE_RTPPROXY udpRtpServer = nullptr; tcpRtpServer = nullptr; +#endif stopAllTcpServer(); } diff --git a/build_docker_images.sh b/build_docker_images.sh new file mode 100644 index 00000000..a642584f --- /dev/null +++ b/build_docker_images.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker build -t gemfield/zlmediakit:20.01-runtime-ubuntu18.04 -f docker/ubuntu18.04/Dockerfile.runtime . +#docker build -t gemfield/zlmediakit:20.01-devel-ubuntu18.04 -f docker/ubuntu18.04/Dockerfile.devel . diff --git a/docker/Dockerfile b/docker/ubuntu16.04/Dockerfile.devel similarity index 82% rename from docker/Dockerfile rename to docker/ubuntu16.04/Dockerfile.devel index d939ab14..650bd206 100644 --- a/docker/Dockerfile +++ b/docker/ubuntu16.04/Dockerfile.devel @@ -9,7 +9,9 @@ EXPOSE 443/tcp EXPOSE 10000/udp EXPOSE 10000/tcp -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ build-essential \ cmake \ git \ @@ -22,6 +24,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libx264-dev \ libfaac-dev \ libmp4v2-dev && \ + apt autoremove -y && \ + apt clean -y && \ rm -rf /var/lib/apt/lists/* RUN mkdir -p /opt/media @@ -36,5 +40,4 @@ RUN cmake -DCMAKE_BUILD_TYPE=Release .. && \ make -j4 ENV PATH /opt/media/ZLMediaKit/release/linux/Release/:$PATH - CMD MediaServer diff --git a/docker/ubuntu16.04/Dockerfile.runtime b/docker/ubuntu16.04/Dockerfile.runtime new file mode 100644 index 00000000..4bd6382f --- /dev/null +++ b/docker/ubuntu16.04/Dockerfile.runtime @@ -0,0 +1,62 @@ +FROM ubuntu:16.04 AS build +#shell,rtmp,rtsp,rtsps,http,https,rtp +EXPOSE 9000/tcp +EXPOSE 1935/tcp +EXPOSE 554/tcp +EXPOSE 322/tcp +EXPOSE 80/tcp +EXPOSE 443/tcp +EXPOSE 10000/udp +EXPOSE 10000/tcp + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + curl \ + vim \ + ca-certificates \ + tzdata \ + libssl-dev \ + libmysqlclient-dev \ + libx264-dev \ + libfaac-dev \ + libmp4v2-dev && \ + apt autoremove -y && \ + apt clean -y && \ + rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /opt/media + +WORKDIR /opt/media +RUN git clone --depth=1 https://github.com/xiongziliang/ZLMediaKit && \ + cd ZLMediaKit && git submodule update --init --recursive && \ + mkdir -p build release/linux/Release/ + +WORKDIR /opt/media/ZLMediaKit/build +RUN cmake -DCMAKE_BUILD_TYPE=Release .. && \ + make -j4 + +FROM ubuntu:16.04 +LABEL maintainer "Gemfield " + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + vim \ + ca-certificates \ + tzdata \ + libssl-dev \ + libx264-dev \ + libfaac-dev \ + libmp4v2-dev && \ + apt autoremove -y && \ + apt clean -y && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/media/bin/ +COPY --from=build /opt/media/ZLMediaKit/release/linux/Release/MediaServer /opt/media/bin/MediaServer +ENV PATH /opt/media/bin:$PATH +CMD MediaServer \ No newline at end of file diff --git a/docker/ubuntu18.04/Dockerfile.devel b/docker/ubuntu18.04/Dockerfile.devel new file mode 100644 index 00000000..335c9a3f --- /dev/null +++ b/docker/ubuntu18.04/Dockerfile.devel @@ -0,0 +1,44 @@ +FROM ubuntu:18.04 +LABEL maintainer "Gemfield " +#shell,rtmp,rtsp,rtsps,http,https,rtp +EXPOSE 9000/tcp +EXPOSE 1935/tcp +EXPOSE 554/tcp +EXPOSE 322/tcp +EXPOSE 80/tcp +EXPOSE 443/tcp +EXPOSE 10000/udp +EXPOSE 10000/tcp + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + curl \ + vim \ + ca-certificates \ + tzdata \ + libssl-dev \ + libmysqlclient-dev \ + libx264-dev \ + libfaac-dev \ + libmp4v2-dev && \ + apt autoremove -y && \ + apt clean -y && \ + rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /opt/media + +WORKDIR /opt/media +RUN git clone --depth=1 https://github.com/xiongziliang/ZLMediaKit && \ + cd ZLMediaKit && git submodule update --init --recursive && \ + mkdir -p build release/linux/Release/ + +WORKDIR /opt/media/ZLMediaKit/build +RUN cmake -DCMAKE_BUILD_TYPE=Release .. && \ + make -j4 + +ENV PATH /opt/media/ZLMediaKit/release/linux/Release:$PATH +CMD MediaServer \ No newline at end of file diff --git a/docker/ubuntu18.04/Dockerfile.runtime b/docker/ubuntu18.04/Dockerfile.runtime new file mode 100644 index 00000000..f10c2658 --- /dev/null +++ b/docker/ubuntu18.04/Dockerfile.runtime @@ -0,0 +1,62 @@ +FROM ubuntu:18.04 AS build +#shell,rtmp,rtsp,rtsps,http,https,rtp +EXPOSE 9000/tcp +EXPOSE 1935/tcp +EXPOSE 554/tcp +EXPOSE 322/tcp +EXPOSE 80/tcp +EXPOSE 443/tcp +EXPOSE 10000/udp +EXPOSE 10000/tcp + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + curl \ + vim \ + ca-certificates \ + tzdata \ + libssl-dev \ + libmysqlclient-dev \ + libx264-dev \ + libfaac-dev \ + libmp4v2-dev && \ + apt autoremove -y && \ + apt clean -y && \ + rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /opt/media + +WORKDIR /opt/media +RUN git clone --depth=1 https://github.com/xiongziliang/ZLMediaKit && \ + cd ZLMediaKit && git submodule update --init --recursive && \ + mkdir -p build release/linux/Release/ + +WORKDIR /opt/media/ZLMediaKit/build +RUN cmake -DCMAKE_BUILD_TYPE=Release .. && \ + make -j4 + +FROM ubuntu:18.04 +LABEL maintainer "Gemfield " + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + vim \ + ca-certificates \ + tzdata \ + libssl-dev \ + libx264-dev \ + libfaac-dev \ + libmp4v2-dev && \ + apt autoremove -y && \ + apt clean -y && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/media/bin/ +COPY --from=build /opt/media/ZLMediaKit/release/linux/Release/MediaServer /opt/media/bin/MediaServer +ENV PATH /opt/media/bin:$PATH +CMD MediaServer \ No newline at end of file diff --git a/server/WebApi.cpp b/server/WebApi.cpp index c303b154..28cce944 100644 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -48,38 +48,10 @@ #include "Thread/WorkThreadPool.h" #include "Rtp/RtpSelector.h" #include "FFmpegSource.h" - using namespace Json; using namespace toolkit; using namespace mediakit; - -typedef map ApiArgsType; - - -#define API_ARGS TcpSession &sender, \ - HttpSession::KeyValue &headerIn, \ - HttpSession::KeyValue &headerOut, \ - ApiArgsType &allArgs, \ - Json::Value &val - -#define API_REGIST(field, name, ...) \ - s_map_api.emplace("/index/"#field"/"#name,[](API_ARGS,const HttpSession::HttpResponseInvoker &invoker){ \ - static auto lam = [&](API_ARGS) __VA_ARGS__ ; \ - lam(sender,headerIn, headerOut, allArgs, val); \ - invoker("200 OK", headerOut, val.toStyledString()); \ - }); - -#define API_ARGS_VALUE sender,headerIn,headerOut,allArgs,val,invoker - -#define API_REGIST_INVOKER(field, name, ...) \ - s_map_api.emplace("/index/"#field"/"#name,[](API_ARGS,const HttpSession::HttpResponseInvoker &invoker) __VA_ARGS__); - -//异步http api lambad定义 -typedef std::function AsyncHttpApi; -//api列表 -static map s_map_api; - namespace API { typedef enum { InvalidArgs = -300, @@ -128,6 +100,27 @@ public: ~SuccessException() = default; }; +#define API_ARGS1 TcpSession &sender,HttpSession::KeyValue &headerIn, HttpSession::KeyValue &headerOut, ApiArgsType &allArgs, Json::Value &val +#define API_ARGS2 API_ARGS1, const HttpSession::HttpResponseInvoker &invoker +#define API_ARGS_VALUE1 sender,headerIn,headerOut,allArgs,val +#define API_ARGS_VALUE2 API_ARGS_VALUE1, invoker + +typedef map ApiArgsType; +//http api列表 +static map > s_map_api; + +template +static void api_regist1(const string &api_path, FUNC &&func) { + s_map_api.emplace(api_path, [func](API_ARGS2) { + func(API_ARGS_VALUE1); + invoker("200 OK", headerOut, val.toStyledString()); + }); +} + +template +static void api_regist2(const string &api_path, FUNC &&func) { + s_map_api.emplace(api_path, std::forward(func)); +} //获取HTTP请求中url参数、content参数 static ApiArgsType getAllArgs(const Parser &parser) { @@ -275,12 +268,11 @@ static recursive_mutex s_ffmpegMapMtx; */ void installWebApi() { addHttpListener(); - GET_CONFIG(string,api_secret,API::kSecret); //获取线程负载 //测试url http://127.0.0.1/index/api/getThreadsLoad - API_REGIST_INVOKER(api, getThreadsLoad, { + api_regist2("/index/api/getThreadsLoad",[](API_ARGS2){ EventPollerPool::Instance().getExecutorDelay([invoker, headerOut](const vector &vecDelay) { Value val; auto vec = EventPollerPool::Instance().getExecutorLoad(); @@ -298,7 +290,7 @@ void installWebApi() { //获取后台工作线程负载 //测试url http://127.0.0.1/index/api/getWorkThreadsLoad - API_REGIST_INVOKER(api, getWorkThreadsLoad, { + api_regist2("/index/api/getWorkThreadsLoad", [](API_ARGS2){ WorkThreadPool::Instance().getExecutorDelay([invoker, headerOut](const vector &vecDelay) { Value val; auto vec = WorkThreadPool::Instance().getExecutorLoad(); @@ -316,7 +308,7 @@ void installWebApi() { //获取服务器配置 //测试url http://127.0.0.1/index/api/getServerConfig - API_REGIST(api, getServerConfig, { + api_regist1("/index/api/getServerConfig",[](API_ARGS1){ CHECK_SECRET(); Value obj; for (auto &pr : mINI::Instance()) { @@ -328,7 +320,7 @@ void installWebApi() { //设置服务器配置 //测试url(比如关闭http api调试) http://127.0.0.1/index/api/setServerConfig?api.apiDebug=0 //你也可以通过http post方式传参,可以通过application/x-www-form-urlencoded或application/json方式传参 - API_REGIST(api, setServerConfig, { + api_regist1("/index/api/setServerConfig",[](API_ARGS1){ CHECK_SECRET(); auto &ini = mINI::Instance(); int changed = API::Success; @@ -352,19 +344,29 @@ void installWebApi() { }); - //获取服务器api列表 - //测试url http://127.0.0.1/index/api/getApiList - API_REGIST(api,getApiList,{ + static auto s_get_api_list = [](API_ARGS1){ CHECK_SECRET(); for(auto &pr : s_map_api){ val["data"].append(pr.first); } + }; + + //获取服务器api列表 + //测试url http://127.0.0.1/index/api/getApiList + api_regist1("/index/api/getApiList",[](API_ARGS1){ + s_get_api_list(API_ARGS_VALUE1); + }); + + //获取服务器api列表 + //测试url http://127.0.0.1/index/ + api_regist1("/index/",[](API_ARGS1){ + s_get_api_list(API_ARGS_VALUE1); }); #if !defined(_WIN32) //重启服务器,只有Daemon方式才能重启,否则是直接关闭! //测试url http://127.0.0.1/index/api/restartServer - API_REGIST(api,restartServer,{ + api_regist1("/index/api/restartServer",[](API_ARGS1){ CHECK_SECRET(); EventPollerPool::Instance().getPoller()->doDelayTask(1000,[](){ //尝试正常退出 @@ -387,7 +389,7 @@ void installWebApi() { //测试url0(获取所有流) http://127.0.0.1/index/api/getMediaList //测试url1(获取虚拟主机为"__defaultVost__"的流) http://127.0.0.1/index/api/getMediaList?vhost=__defaultVost__ //测试url2(获取rtsp类型的流) http://127.0.0.1/index/api/getMediaList?schema=rtsp - API_REGIST(api,getMediaList,{ + api_regist1("/index/api/getMediaList",[](API_ARGS1){ CHECK_SECRET(); //获取所有MediaSource列表 MediaSource::for_each_media([&](const MediaSource::Ptr &media){ @@ -419,14 +421,14 @@ void installWebApi() { }); //测试url http://127.0.0.1/index/api/isMediaOnline?schema=rtsp&vhost=__defaultVhost__&app=live&stream=obs - API_REGIST(api,isMediaOnline,{ + api_regist1("/index/api/isMediaOnline",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("schema","vhost","app","stream"); val["online"] = (bool) (MediaSource::find(allArgs["schema"],allArgs["vhost"],allArgs["app"],allArgs["stream"],false)); }); //测试url http://127.0.0.1/index/api/getMediaInfo?schema=rtsp&vhost=__defaultVhost__&app=live&stream=obs - API_REGIST(api,getMediaInfo,{ + api_regist1("/index/api/getMediaInfo",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("schema","vhost","app","stream"); auto src = MediaSource::find(allArgs["schema"],allArgs["vhost"],allArgs["app"],allArgs["stream"],false); @@ -448,7 +450,7 @@ void installWebApi() { //主动关断流,包括关断拉流、推流 //测试url http://127.0.0.1/index/api/close_stream?schema=rtsp&vhost=__defaultVhost__&app=live&stream=obs&force=1 - API_REGIST(api,close_stream,{ + api_regist1("/index/api/close_stream",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("schema","vhost","app","stream"); //踢掉推流器 @@ -468,7 +470,7 @@ void installWebApi() { //批量主动关断流,包括关断拉流、推流 //测试url http://127.0.0.1/index/api/close_streams?schema=rtsp&vhost=__defaultVhost__&app=live&stream=obs&force=1 - API_REGIST(api,close_streams,{ + api_regist1("/index/api/close_streams",[](API_ARGS1){ CHECK_SECRET(); //筛选命中个数 int count_hit = 0; @@ -504,7 +506,7 @@ void installWebApi() { //获取所有TcpSession列表信息 //可以根据本地端口和远端ip来筛选 //测试url(筛选某端口下的tcp会话) http://127.0.0.1/index/api/getAllSession?local_port=1935 - API_REGIST(api,getAllSession,{ + api_regist1("/index/api/getAllSession",[](API_ARGS1){ CHECK_SECRET(); Value jsession; uint16_t local_port = allArgs["local_port"].as(); @@ -529,7 +531,7 @@ void installWebApi() { //断开tcp连接,比如说可以断开rtsp、rtmp播放器等 //测试url http://127.0.0.1/index/api/kick_session?id=123456 - API_REGIST(api,kick_session,{ + api_regist1("/index/api/kick_session",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("id"); //踢掉tcp会话 @@ -543,7 +545,7 @@ void installWebApi() { //批量断开tcp连接,比如说可以断开rtsp、rtmp播放器等 //测试url http://127.0.0.1/index/api/kick_sessions?local_port=1935 - API_REGIST(api,kick_sessions,{ + api_regist1("/index/api/kick_sessions",[](API_ARGS1){ CHECK_SECRET(); uint16_t local_port = allArgs["local_port"].as(); string &peer_ip = allArgs["peer_ip"]; @@ -609,7 +611,7 @@ void installWebApi() { //动态添加rtsp/rtmp拉流代理 //测试url http://127.0.0.1/index/api/addStreamProxy?vhost=__defaultVhost__&app=proxy&enable_rtsp=1&enable_rtmp=1&stream=0&url=rtmp://127.0.0.1/live/obs - API_REGIST_INVOKER(api,addStreamProxy,{ + api_regist2("/index/api/addStreamProxy",[](API_ARGS2){ CHECK_SECRET(); CHECK_ARGS("vhost","app","stream","url","enable_rtsp","enable_rtmp"); addStreamProxy(allArgs["vhost"], @@ -634,7 +636,7 @@ void installWebApi() { //关闭拉流代理 //测试url http://127.0.0.1/index/api/delStreamProxy?key=__defaultVhost__/proxy/0 - API_REGIST(api,delStreamProxy,{ + api_regist1("/index/api/delStreamProxy",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("key"); lock_guard lck(s_proxyMapMtx); @@ -671,7 +673,7 @@ void installWebApi() { //动态添加rtsp/rtmp拉流代理 //测试url http://127.0.0.1/index/api/addFFmpegSource?src_url=http://live.hkstv.hk.lxdns.com/live/hks2/playlist.m3u8&dst_url=rtmp://127.0.0.1/live/hks2&timeout_ms=10000 - API_REGIST_INVOKER(api,addFFmpegSource,{ + api_regist2("/index/api/addFFmpegSource",[](API_ARGS2){ CHECK_SECRET(); CHECK_ARGS("src_url","dst_url","timeout_ms"); auto src_url = allArgs["src_url"]; @@ -690,7 +692,7 @@ void installWebApi() { }); - static auto api_delFFmpegSource = [](API_ARGS,const HttpSession::HttpResponseInvoker &invoker){ + static auto api_delFFmpegSource = [](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("key"); lock_guard lck(s_ffmpegMapMtx); @@ -699,24 +701,24 @@ void installWebApi() { //关闭拉流代理 //测试url http://127.0.0.1/index/api/delFFmepgSource?key=key - API_REGIST(api,delFFmpegSource,{ - api_delFFmpegSource(API_ARGS_VALUE); + api_regist1("/index/api/delFFmpegSource",[](API_ARGS1){ + api_delFFmpegSource(API_ARGS_VALUE1); }); //此处为了兼容之前的拼写错误 - API_REGIST(api,delFFmepgSource,{ - api_delFFmpegSource(API_ARGS_VALUE); + api_regist1("/index/api/delFFmepgSource",[](API_ARGS1){ + api_delFFmpegSource(API_ARGS_VALUE1); }); //新增http api下载可执行程序文件接口 //测试url http://127.0.0.1/index/api/downloadBin - API_REGIST_INVOKER(api,downloadBin,{ + api_regist2("/index/api/downloadBin",[](API_ARGS2){ CHECK_SECRET(); invoker.responseFile(headerIn,StrCaseMap(),exePath()); }); #if defined(ENABLE_RTPPROXY) - API_REGIST(api,getSsrcInfo,{ + api_regist1("/index/api/getSsrcInfo",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("ssrc"); uint32_t ssrc = 0; @@ -735,7 +737,7 @@ void installWebApi() { #endif//ENABLE_RTPPROXY // 开始录制hls或MP4 - API_REGIST(api,startRecord,{ + api_regist1("/index/api/startRecord",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("type","vhost","app","stream","wait_for_record","continue_record"); @@ -750,7 +752,7 @@ void installWebApi() { }); // 停止录制hls或MP4 - API_REGIST(api,stopRecord,{ + api_regist1("/index/api/stopRecord",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("type","vhost","app","stream"); int result = Recorder::stopRecord((Recorder::type)allArgs["type"].as(), @@ -761,7 +763,7 @@ void installWebApi() { }); // 获取hls或MP4录制状态 - API_REGIST(api,getRecordStatus,{ + api_regist1("/index/api/getRecordStatus",[](API_ARGS1){ CHECK_SECRET(); CHECK_ARGS("type","vhost","app","stream"); auto status = Recorder::getRecordStatus((Recorder::type)allArgs["type"].as(), @@ -826,7 +828,7 @@ void installWebApi() { }); ////////////以下是注册的Hook API//////////// - API_REGIST(hook,on_publish,{ + api_regist1("/index/hook/on_publish",[](API_ARGS1){ //开始推流事件 //转换成rtsp或rtmp val["enableRtxp"] = true; @@ -836,23 +838,23 @@ void installWebApi() { val["enableMP4"] = false; }); - API_REGIST(hook,on_play,{ + api_regist1("/index/hook/on_play",[](API_ARGS1){ //开始播放事件 throw SuccessException(); }); - API_REGIST(hook,on_flow_report,{ + api_regist1("/index/hook/on_flow_report",[](API_ARGS1){ //流量统计hook api throw SuccessException(); }); - API_REGIST(hook,on_rtsp_realm,{ + api_regist1("/index/hook/on_rtsp_realm",[](API_ARGS1){ //rtsp是否需要鉴权,默认需要鉴权 val["code"] = API::Success; val["realm"] = "zlmediakit_reaml"; }); - API_REGIST(hook,on_rtsp_auth,{ + api_regist1("/index/hook/on_rtsp_auth",[](API_ARGS1){ //rtsp鉴权密码,密码等于用户名 //rtsp可以有双重鉴权!后面还会触发on_play事件 CHECK_ARGS("user_name"); @@ -861,14 +863,14 @@ void installWebApi() { val["passwd"] = allArgs["user_name"].data(); }); - API_REGIST(hook,on_stream_changed,{ + api_regist1("/index/hook/on_stream_changed",[](API_ARGS1){ //媒体注册或反注册事件 throw SuccessException(); }); #if !defined(_WIN32) - API_REGIST_INVOKER(hook,on_stream_not_found_ffmpeg,{ + api_regist2("/index/hook/on_stream_not_found_ffmpeg",[](API_ARGS2){ //媒体未找到事件,我们都及时拉流hks作为替代品,目的是为了测试按需拉流 CHECK_SECRET(); CHECK_ARGS("vhost","app","stream"); @@ -898,7 +900,7 @@ void installWebApi() { }); #endif//!defined(_WIN32) - API_REGIST_INVOKER(hook,on_stream_not_found,{ + api_regist2("/index/hook/on_stream_not_found",[](API_ARGS2){ //媒体未找到事件,我们都及时拉流hks作为替代品,目的是为了测试按需拉流 CHECK_SECRET(); CHECK_ARGS("vhost","app","stream"); @@ -924,17 +926,17 @@ void installWebApi() { }); }); - API_REGIST(hook,on_record_mp4,{ + api_regist1("/index/hook/on_record_mp4",[](API_ARGS1){ //录制mp4分片完毕事件 throw SuccessException(); }); - API_REGIST(hook,on_shell_login,{ + api_regist1("/index/hook/on_shell_login",[](API_ARGS1){ //shell登录调试事件 throw SuccessException(); }); - API_REGIST(hook,on_stream_none_reader,{ + api_regist1("/index/hook/on_stream_none_reader",[](API_ARGS1){ //无人观看流默认关闭 val["close"] = true; }); @@ -944,7 +946,7 @@ void installWebApi() { return true; }; - API_REGIST(hook,on_http_access,{ + api_regist1("/index/hook/on_http_access",[](API_ARGS1){ //在这里根据allArgs["params"](url参数)来判断该http客户端是否有权限访问该文件 if(!checkAccess(allArgs["params"])){ //无访问权限 @@ -965,7 +967,7 @@ void installWebApi() { }); - API_REGIST(hook,on_server_started,{ + api_regist1("/index/hook/on_server_started",[](API_ARGS1){ //服务器重启报告 throw SuccessException(); }); diff --git a/src/Extension/AACRtmp.cpp b/src/Extension/AACRtmp.cpp index c3efddb1..6a657aab 100644 --- a/src/Extension/AACRtmp.cpp +++ b/src/Extension/AACRtmp.cpp @@ -76,17 +76,24 @@ AACRtmpEncoder::AACRtmpEncoder(const Track::Ptr &track) { _track = dynamic_pointer_cast(track); } +void AACRtmpEncoder::makeConfigPacket() { + if (_track && _track->ready()) { + //从track中和获取aac配置信息 + _aac_cfg = _track->getAacCfg(); + } + + if (!_aac_cfg.empty()) { + makeAudioConfigPkt(); + } +} + void AACRtmpEncoder::inputFrame(const Frame::Ptr &frame) { - if(_aac_cfg.empty()){ - if(frame->prefixSize() >= 7){ + if (_aac_cfg.empty()) { + if (frame->prefixSize() >= 7) { //包含adts头,从adts头获取aac配置信息 _aac_cfg = makeAdtsConfig(reinterpret_cast(frame->data())); - makeAudioConfigPkt(); - } else if(_track && _track->ready()){ - //从track中和获取aac配置信息 - _aac_cfg = _track->getAacCfg(); - makeAudioConfigPkt(); } + makeConfigPacket(); } if(!_aac_cfg.empty()){ diff --git a/src/Extension/AACRtmp.h b/src/Extension/AACRtmp.h index 661a37a1..8181ebfd 100644 --- a/src/Extension/AACRtmp.h +++ b/src/Extension/AACRtmp.h @@ -88,6 +88,10 @@ public: */ void inputFrame(const Frame::Ptr &frame) override; + /** + * 生成config包 + */ + void makeConfigPacket() override; private: void makeAudioConfigPkt(); private: diff --git a/src/Extension/H264Rtmp.cpp b/src/Extension/H264Rtmp.cpp index c995317f..2aa4a7cb 100644 --- a/src/Extension/H264Rtmp.cpp +++ b/src/Extension/H264Rtmp.cpp @@ -99,7 +99,20 @@ inline void H264RtmpDecoder::onGetH264(const char* pcData, int iLen, uint32_t dt H264RtmpEncoder::H264RtmpEncoder(const Track::Ptr &track) { _track = dynamic_pointer_cast(track); +} +void H264RtmpEncoder::makeConfigPacket(){ + if (_track && _track->ready()) { + //尝试从track中获取sps pps信息 + _sps = _track->getSps(); + _pps = _track->getPps(); + } + + if (!_sps.empty() && !_pps.empty()) { + //获取到sps/pps + makeVideoConfigPkt(); + _gotSpsPps = true; + } } void H264RtmpEncoder::inputFrame(const Frame::Ptr &frame) { @@ -107,37 +120,24 @@ void H264RtmpEncoder::inputFrame(const Frame::Ptr &frame) { auto iLen = frame->size() - frame->prefixSize(); auto type = H264_TYPE(((uint8_t*)pcData)[0]); - if(!_gotSpsPps){ + if (!_gotSpsPps) { //尝试从frame中获取sps pps - switch (type){ - case H264Frame::NAL_SPS:{ + switch (type) { + case H264Frame::NAL_SPS: { //sps - if(_sps.empty()){ - _sps = string(pcData,iLen); - } - } + _sps = string(pcData, iLen); + makeConfigPacket(); break; - case H264Frame::NAL_PPS:{ + } + case H264Frame::NAL_PPS: { //pps - if(_pps.empty()){ - _pps = string(pcData,iLen); - } - } + _pps = string(pcData, iLen); + makeConfigPacket(); break; + } default: break; } - - if(_track && _track->ready()){ - //尝试从track中获取sps pps信息 - _sps = _track->getSps(); - _pps = _track->getPps(); - } - - if(!_sps.empty() && !_pps.empty()){ - _gotSpsPps = true; - makeVideoConfigPkt(); - } } if(type == H264Frame::NAL_SEI){ diff --git a/src/Extension/H264Rtmp.h b/src/Extension/H264Rtmp.h index 68802d68..a1cae242 100644 --- a/src/Extension/H264Rtmp.h +++ b/src/Extension/H264Rtmp.h @@ -90,6 +90,11 @@ public: * @param frame 帧数据 */ void inputFrame(const Frame::Ptr &frame) override; + + /** + * 生成config包 + */ + void makeConfigPacket() override; private: void makeVideoConfigPkt(); private: diff --git a/src/Extension/H264Rtp.cpp b/src/Extension/H264Rtp.cpp index f90f3c56..5df37767 100644 --- a/src/Extension/H264Rtp.cpp +++ b/src/Extension/H264Rtp.cpp @@ -189,9 +189,8 @@ bool H264RtpDecoder::decodeRtp(const RtpPacket::Ptr &rtppack) { //该帧最后一个rtp包 FU-A end _h264frame->_buffer.append((char *)frame + 2, length - 2); _h264frame->_pts = rtppack->timeStamp; - auto key = _h264frame->keyFrame(); onGetH264(_h264frame); - return key; + return false; } default:{ @@ -209,8 +208,15 @@ bool H264RtpDecoder::decodeRtp(const RtpPacket::Ptr &rtppack) { } void H264RtpDecoder::onGetH264(const H264Frame::Ptr &frame) { - //根据pts计算dts auto flag = _dts_generator.getDts(frame->_pts,frame->_dts); + if(!flag){ + if(frame->configFrame() || frame->keyFrame()){ + flag = true; + frame->_dts = frame->_pts; + } + } + + //根据pts计算dts if(flag){ //写入环形缓存 RtpCodec::inputFrame(frame); diff --git a/src/Extension/H265Rtp.cpp b/src/Extension/H265Rtp.cpp index 4b8df94c..8f41ec76 100644 --- a/src/Extension/H265Rtp.cpp +++ b/src/Extension/H265Rtp.cpp @@ -127,9 +127,8 @@ bool H265RtpDecoder::decodeRtp(const RtpPacket::Ptr &rtppack) { //该帧最后一个rtp包 _h265frame->_buffer.append((char *) frame + 3, length - 3); _h265frame->_pts = rtppack->timeStamp; - auto key = _h265frame->keyFrame(); onGetH265(_h265frame); - return key; + return false; } default: // 4.4.1. Single NAL Unit Packets (p24) @@ -146,6 +145,12 @@ bool H265RtpDecoder::decodeRtp(const RtpPacket::Ptr &rtppack) { void H265RtpDecoder::onGetH265(const H265Frame::Ptr &frame) { //计算dts auto flag = _dts_generator.getDts(frame->_pts,frame->_dts); + if(!flag){ + if(frame->configFrame() || frame->keyFrame()){ + flag = true; + frame->_dts = frame->_pts; + } + } if(flag){ //写入环形缓存 RtpCodec::inputFrame(frame); diff --git a/src/Http/WebSocketSession.h b/src/Http/WebSocketSession.h index 682a2559..af302b8a 100644 --- a/src/Http/WebSocketSession.h +++ b/src/Http/WebSocketSession.h @@ -30,16 +30,80 @@ #include "HttpSession.h" #include "Network/TcpServer.h" +/** + * 数据发送拦截器 + */ +class SendInterceptor{ +public: + typedef function onBeforeSendCB; + SendInterceptor() = default; + virtual ~SendInterceptor() = default; + virtual void setOnBeforeSendCB(const onBeforeSendCB &cb) = 0; +}; + +/** + * 该类实现了TcpSession派生类发送数据的截取 + * 目的是发送业务数据前进行websocket协议的打包 + */ +template +class TcpSessionTypeImp : public TcpSessionType, public SendInterceptor{ +public: + typedef std::shared_ptr Ptr; + + TcpSessionTypeImp(const Parser &header, const HttpSession &parent, const Socket::Ptr &pSock) : + _identifier(parent.getIdentifier()), TcpSessionType(pSock) {} + + ~TcpSessionTypeImp() {} + + /** + * 设置发送数据截取回调函数 + * @param cb 截取回调函数 + */ + void setOnBeforeSendCB(const onBeforeSendCB &cb) override { + _beforeSendCB = cb; + } + +protected: + /** + * 重载send函数截取数据 + * @param buf 需要截取的数据 + * @return 数据字节数 + */ + int send(const Buffer::Ptr &buf) override { + if (_beforeSendCB) { + return _beforeSendCB(buf); + } + return TcpSessionType::send(buf); + } + + string getIdentifier() const override { + return _identifier; + } + +private: + onBeforeSendCB _beforeSendCB; + string _identifier; +}; + +template +class TcpSessionCreator { +public: + //返回的TcpSession必须派生于SendInterceptor,可以返回null + TcpSession::Ptr operator()(const Parser &header, const HttpSession &parent, const Socket::Ptr &pSock){ + return std::make_shared >(header,parent,pSock); + } +}; + + /** * 通过该模板类可以透明化WebSocket协议, * 用户只要实现WebSock协议下的具体业务协议,譬如基于WebSocket协议的Rtmp协议等 -* @tparam SessionType 业务协议的TcpSession类 */ -template -class WebSocketSession : public HttpSessionType { +template +class WebSocketSessionBase : public HttpSessionType { public: - WebSocketSession(const Socket::Ptr &pSock) : HttpSessionType(pSock){} - virtual ~WebSocketSession(){} + WebSocketSessionBase(const Socket::Ptr &pSock) : HttpSessionType(pSock){} + virtual ~WebSocketSessionBase(){} //收到eof或其他导致脱离TcpServer事件的回调 void onError(const SockException &err) override{ @@ -69,23 +133,27 @@ protected: */ bool onWebSocketConnect(const Parser &header) override{ //创建websocket session类 - _session = std::make_shared(HttpSessionType::getIdentifier(),HttpSessionType::_sock); + _session = _creator(header, *this,HttpSessionType::_sock); + if(!_session){ + //此url不允许创建websocket连接 + return false; + } auto strongServer = _weakServer.lock(); if(strongServer){ _session->attachServer(*strongServer); } //此处截取数据并进行websocket协议打包 - weak_ptr weakSelf = dynamic_pointer_cast(HttpSessionType::shared_from_this()); - _session->setOnBeforeSendCB([weakSelf](const Buffer::Ptr &buf){ + weak_ptr weakSelf = dynamic_pointer_cast(HttpSessionType::shared_from_this()); + dynamic_pointer_cast(_session)->setOnBeforeSendCB([weakSelf](const Buffer::Ptr &buf) { auto strongSelf = weakSelf.lock(); - if(strongSelf){ + if (strongSelf) { WebSocketHeader header; header._fin = true; header._reserved = 0; header._opcode = DataType; header._mask_flag = false; - strongSelf->WebSocketSplitter::encode(header,buf); + strongSelf->WebSocketSplitter::encode(header, buf); } return buf->size(); }); @@ -155,50 +223,19 @@ protected: void onWebSocketEncodeData(const Buffer::Ptr &buffer) override{ SocketHelper::send(buffer); } -private: - typedef function onBeforeSendCB; - /** - * 该类实现了TcpSession派生类发送数据的截取 - * 目的是发送业务数据前进行websocket协议的打包 - */ - class SessionImp : public SessionType{ - public: - SessionImp(const string &identifier,const Socket::Ptr &pSock) : - _identifier(identifier),SessionType(pSock){} - - ~SessionImp(){} - - /** - * 设置发送数据截取回调函数 - * @param cb 截取回调函数 - */ - void setOnBeforeSendCB(const onBeforeSendCB &cb){ - _beforeSendCB = cb; - } - protected: - /** - * 重载send函数截取数据 - * @param buf 需要截取的数据 - * @return 数据字节数 - */ - int send(const Buffer::Ptr &buf) override { - if(_beforeSendCB){ - return _beforeSendCB(buf); - } - return SessionType::send(buf); - } - string getIdentifier() const override{ - return _identifier; - } - private: - onBeforeSendCB _beforeSendCB; - string _identifier; - }; private: string _remian_data; weak_ptr _weakServer; - std::shared_ptr _session; + TcpSession::Ptr _session; + Creator _creator; }; +template +class WebSocketSession : public WebSocketSessionBase,HttpSessionType,DataType>{ +public: + WebSocketSession(const Socket::Ptr &pSock) : WebSocketSessionBase,HttpSessionType,DataType>(pSock){} + virtual ~WebSocketSession(){} +}; + #endif //ZLMEDIAKIT_WEBSOCKETSESSION_H diff --git a/src/Rtmp/RtmpCodec.h b/src/Rtmp/RtmpCodec.h index ccdb967a..25f2fc5a 100644 --- a/src/Rtmp/RtmpCodec.h +++ b/src/Rtmp/RtmpCodec.h @@ -80,6 +80,7 @@ public: typedef std::shared_ptr Ptr; RtmpCodec(){} virtual ~RtmpCodec(){} + virtual void makeConfigPacket() {}; }; diff --git a/src/Rtmp/RtmpMediaSource.h b/src/Rtmp/RtmpMediaSource.h index 928c9066..e6f9178f 100644 --- a/src/Rtmp/RtmpMediaSource.h +++ b/src/Rtmp/RtmpMediaSource.h @@ -142,7 +142,11 @@ public: } strongSelf->onReaderChanged(size); }; - _ring = std::make_shared(_ring_size, std::move(lam)); + + //rtmp包缓存最大允许512个,如果是纯视频(25fps)大概为20秒数据 + //但是这个是GOP缓存的上限值,真实的GOP缓存大小等于两个I帧之间的包数的两倍 + //而且每次遇到I帧,则会清空GOP缓存,所以真实的GOP缓存远小于此值 + _ring = std::make_shared(_ring_size,512,std::move(lam)); onReaderChanged(0); if(_metadata){ diff --git a/src/Rtmp/RtmpMediaSourceMuxer.h b/src/Rtmp/RtmpMediaSourceMuxer.h index 2cecd127..dce7e70e 100644 --- a/src/Rtmp/RtmpMediaSourceMuxer.h +++ b/src/Rtmp/RtmpMediaSourceMuxer.h @@ -58,6 +58,7 @@ public: } void onAllTrackReady(){ + makeConfigPacket(); _mediaSouce->setMetaData(getMetadata()); } diff --git a/src/Rtmp/RtmpMuxer.cpp b/src/Rtmp/RtmpMuxer.cpp index d81a74fa..89d345ca 100644 --- a/src/Rtmp/RtmpMuxer.cpp +++ b/src/Rtmp/RtmpMuxer.cpp @@ -78,6 +78,14 @@ void RtmpMuxer::inputFrame(const Frame::Ptr &frame) { } } +void RtmpMuxer::makeConfigPacket(){ + for(auto &encoder : _encoder){ + if(encoder){ + encoder->makeConfigPacket(); + } + } +} + const AMFValue &RtmpMuxer::getMetadata() const { return _metadata; } diff --git a/src/Rtmp/RtmpMuxer.h b/src/Rtmp/RtmpMuxer.h index c1a007fa..cc09178e 100644 --- a/src/Rtmp/RtmpMuxer.h +++ b/src/Rtmp/RtmpMuxer.h @@ -71,6 +71,11 @@ public: * 重置所有track */ void resetTracks() override ; + + /** + * 生成config包 + */ + void makeConfigPacket(); private: RtmpRing::RingType::Ptr _rtmpRing; AMFValue _metadata; diff --git a/src/Rtsp/Rtsp.cpp b/src/Rtsp/Rtsp.cpp index bc8dcce4..d6d95258 100644 --- a/src/Rtsp/Rtsp.cpp +++ b/src/Rtsp/Rtsp.cpp @@ -85,74 +85,84 @@ string SdpTrack::toString() const { } return _printer; } -void SdpParser::load(const string &sdp) { - _track_map.clear(); - string key; - SdpTrack::Ptr track = std::make_shared(); - auto lines = split(sdp,"\n"); - for (auto &line : lines){ - trim(line); - if(line.size() < 2 || line[1] != '='){ - continue; - } - char opt = line[0]; - string opt_val = line.substr(2); - switch (opt){ - case 'o': - track->_o = opt_val; - break; - case 's': - track->_s = opt_val; - break; - case 'i': - track->_i = opt_val; - break; - case 'c': - track->_c = opt_val; - break; - case 't': - track->_t = opt_val; - break; - case 'b': - track->_b = opt_val; - break; - case 'm':{ - _track_map[key] = track; - track = std::make_shared(); - key = FindField(opt_val.data(), nullptr," "); - track->_m = opt_val; - } - break; - case 'a':{ - string attr = FindField(opt_val.data(), nullptr,":"); - if(attr.empty()){ - track->_attr[opt_val] = ""; - }else{ - track->_attr[attr] = FindField(opt_val.data(),":", nullptr); - } - } - break; - default: - track->_other[opt] = opt_val; - break; - } +static TrackType toTrackType(const string &str) { + if (str == "") { + return TrackTitle; } - _track_map[key] = track; + if (str == "video") { + return TrackVideo; + } - for (auto &pr : _track_map) { - auto &track = *pr.second; - if (pr.first == "") { - track._type = TrackTitle; - } else if (pr.first == "video") { - track._type = TrackVideo; - } else if (pr.first == "audio") { - track._type = TrackAudio; - } else { - track._type = TrackInvalid; + if (str == "audio") { + return TrackAudio; + } + + return TrackInvalid; +} + +void SdpParser::load(const string &sdp) { + { + _track_vec.clear(); + string key; + SdpTrack::Ptr track = std::make_shared(); + + auto lines = split(sdp, "\n"); + for (auto &line : lines) { + trim(line); + if (line.size() < 2 || line[1] != '=') { + continue; + } + char opt = line[0]; + string opt_val = line.substr(2); + switch (opt) { + case 'o': + track->_o = opt_val; + break; + case 's': + track->_s = opt_val; + break; + case 'i': + track->_i = opt_val; + break; + case 'c': + track->_c = opt_val; + break; + case 't': + track->_t = opt_val; + break; + case 'b': + track->_b = opt_val; + break; + case 'm': { + track->_type = toTrackType(key); + _track_vec.emplace_back(track); + track = std::make_shared(); + key = FindField(opt_val.data(), nullptr, " "); + track->_m = opt_val; + } + break; + case 'a': { + string attr = FindField(opt_val.data(), nullptr, ":"); + if (attr.empty()) { + track->_attr[opt_val] = ""; + } else { + track->_attr[attr] = FindField(opt_val.data(), ":", nullptr); + } + } + break; + default: + track->_other[opt] = opt_val; + break; + } } + track->_type = toTrackType(key); + _track_vec.emplace_back(track); + } + for (auto &track_ptr : _track_vec) { + auto &track = *track_ptr; auto it = track._attr.find("range"); if (it != track._attr.end()) { char name[16] = {0}, start[16] = {0}, end[16] = {0}; @@ -198,9 +208,9 @@ bool SdpParser::available() const { } SdpTrack::Ptr SdpParser::getTrack(TrackType type) const { - for (auto &pr : _track_map){ - if(pr.second->_type == type){ - return pr.second; + for (auto &track : _track_vec){ + if(track->_type == type){ + return track; } } return nullptr; @@ -208,31 +218,42 @@ SdpTrack::Ptr SdpParser::getTrack(TrackType type) const { vector SdpParser::getAvailableTrack() const { vector ret; - auto video = getTrack(TrackVideo); - if(video){ - ret.emplace_back(video); + bool audio_added = false; + bool video_added = false; + for (auto &track : _track_vec){ + if(track->_type == TrackAudio ){ + if(!audio_added){ + ret.emplace_back(track); + audio_added = true; + } + continue; + } + + if(track->_type == TrackVideo ){ + if(!video_added){ + ret.emplace_back(track); + video_added = true; + } + continue; + } } - auto audio = getTrack(TrackAudio); - if(audio){ - ret.emplace_back(audio); - } - return ret; + return std::move(ret); } string SdpParser::toString() const { string title,audio,video; - for(auto &pr : _track_map){ - switch (pr.second->_type){ + for(auto &track : _track_vec){ + switch (track->_type){ case TrackTitle:{ - title = pr.second->toString(); + title = track->toString(); } break; case TrackVideo:{ - video = pr.second->toString(); + video = track->toString(); } break; case TrackAudio:{ - audio = pr.second->toString(); + audio = track->toString(); } break; default: diff --git a/src/Rtsp/Rtsp.h b/src/Rtsp/Rtsp.h index c8eb5554..26e2a979 100644 --- a/src/Rtsp/Rtsp.h +++ b/src/Rtsp/Rtsp.h @@ -122,7 +122,7 @@ public: vector getAvailableTrack() const; string toString() const ; private: - map _track_map; + vector _track_vec; }; /** diff --git a/src/Rtsp/RtspMediaSource.h b/src/Rtsp/RtspMediaSource.h index 2fc0cc99..21263b9d 100644 --- a/src/Rtsp/RtspMediaSource.h +++ b/src/Rtsp/RtspMediaSource.h @@ -179,7 +179,10 @@ public: } strongSelf->onReaderChanged(size); }; - _ring = std::make_shared(_ring_size, std::move(lam)); + //rtp包缓存最大允许2048个,大概最多3MB数据 + //但是这个是GOP缓存的上限值,真实的GOP缓存大小等于两个I帧之间的包数的两倍 + //而且每次遇到I帧,则会清空GOP缓存,所以真实的GOP缓存远小于此值 + _ring = std::make_shared(_ring_size,2048, std::move(lam)); onReaderChanged(0); if (!_sdp.empty()) { regist(); diff --git a/tests/test_wsServer.cpp b/tests/test_wsServer.cpp index 12891889..1821467b 100644 --- a/tests/test_wsServer.cpp +++ b/tests/test_wsServer.cpp @@ -51,6 +51,7 @@ public: } void onRecv(const Buffer::Ptr &buffer) override { //回显数据 + send("from EchoSession:"); send(buffer); } void onError(const SockException &err) override{ @@ -62,6 +63,48 @@ public: } }; + +class EchoSessionWithUrl : public TcpSession { +public: + EchoSessionWithUrl(const Socket::Ptr &pSock) : TcpSession(pSock){ + DebugL; + } + virtual ~EchoSessionWithUrl(){ + DebugL; + } + + void attachServer(const TcpServer &server) override{ + DebugL << getIdentifier() << " " << TcpSession::getIdentifier(); + } + void onRecv(const Buffer::Ptr &buffer) override { + //回显数据 + send("from EchoSessionWithUrl:"); + send(buffer); + } + void onError(const SockException &err) override{ + WarnL << err.what(); + } + //每隔一段时间触发,用来做超时管理 + void onManager() override{ + DebugL; + } +}; + + +/** + * 此对象可以根据websocket 客户端访问的url选择创建不同的对象 + */ +struct EchoSessionCreator { + //返回的TcpSession必须派生于SendInterceptor,可以返回null(拒绝连接) + TcpSession::Ptr operator()(const Parser &header, const HttpSession &parent, const Socket::Ptr &pSock) { +// return nullptr; + if (header.Url() == "/") { + return std::make_shared >(header, parent, pSock); + } + return std::make_shared >(header, parent, pSock); + } +}; + int main(int argc, char *argv[]) { //设置日志 Logger::Instance().add(std::make_shared()); @@ -71,13 +114,19 @@ int main(int argc, char *argv[]) { TcpServer::Ptr httpSrv(new TcpServer()); //http服务器,支持websocket - httpSrv->start>(80);//默认80 + httpSrv->start >(80);//默认80 TcpServer::Ptr httpsSrv(new TcpServer()); //https服务器,支持websocket - httpsSrv->start>(443);//默认443 + httpsSrv->start >(443);//默认443 + + TcpServer::Ptr httpSrvOld(new TcpServer()); + //兼容之前的代码(但是不支持根据url选择生成TcpSession类型) + httpSrvOld->start >(8080); + + DebugL << "请打开网页:http://www.websocket-test.com/,进行测试"; + DebugL << "连接 ws://127.0.0.1/xxxx,ws://127.0.0.1/ 测试的效果将不同,支持根据url选择不同的处理逻辑"; - DebugL << "请打开网页:http://www.websocket-test.com/,连接 ws://127.0.0.1/测试"; //设置退出信号处理函数 static semaphore sem;