diff --git a/.gitattributes b/.gitattributes index b6649863..b7f1a8b2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,2 @@ -release/ filter=lfs diff=lfs merge=lfs -text -*.a filter=lfs diff=lfs merge=lfs -text *.h linguist-language=cpp *.c linguist-language=cpp diff --git a/3rdpart/ZLToolKit b/3rdpart/ZLToolKit index 4a6029b7..8d1681b5 160000 --- a/3rdpart/ZLToolKit +++ b/3rdpart/ZLToolKit @@ -1 +1 @@ -Subproject commit 4a6029b74b4f2339e32b8c546388de51e4ec1bcb +Subproject commit 8d1681b5bb247e7f47ae0f8c414f6eeb376b742b diff --git a/README.md b/README.md index 0eef5ac4..5dcbcb68 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,11 @@ ## Why ZLMediaKit? - Developed based on C++ 11, the code is stable and reliable, avoiding the use of raw pointers, cross-platform porting is simple and convenient, and the code is clear and concise. -- Support rich streaming media protocols(`RTSP/RTMP/HLS/HTTP-FLV`),and support Inter-protocol conversion. +- Support rich streaming media protocols(`RTSP/RTMP/HLS/HTTP-FLV/Websocket-flv`),and support Inter-protocol conversion. - Multiplexing asynchronous network IO based on epoll and multi thread,extreme performance. - Well performance and stable test,can be used commercially. - Support linux, macos, ios, android, Windows Platforms. - Very low latency(lower then one second), video opened immediately. -- **Now Support websocket-flv!** ## Features diff --git a/README_CN.md b/README_CN.md index 918d068a..54199461 100644 --- a/README_CN.md +++ b/README_CN.md @@ -4,14 +4,13 @@ ## 项目特点 - 基于C++11开发,避免使用裸指针,代码稳定可靠;同时跨平台移植简单方便,代码清晰简洁。 -- 打包多种流媒体协议(RTSP/RTMP/HLS),支持协议间的互相转换,提供一站式的服务。 +- 打包多种流媒体协议(RTSP/RTMP/HLS/HTTP-FLV/Websocket-FLV),支持协议间的互相转换,提供一站式的服务。 - 使用epoll+线程池+异步网络IO模式开发,并发性能优越。 - 已实现主流的的H264/H265+AAC流媒体方案,代码精简,脉络清晰,适合学习。 - 编码格式与框架代码解耦,方便自由简洁的添加支持其他编码格式 - 代码经过大量的稳定性、性能测试,可满足商用服务器项目。 - 支持linux、macos、ios、android、windows平台 - 支持画面秒开(GOP缓存)、极低延时([500毫秒内,最低可达100毫秒](https://github.com/zlmediakit/ZLMediaKit/wiki/%E5%BB%B6%E6%97%B6%E6%B5%8B%E8%AF%95)) -- **支持websocket-flv直播** - [ZLMediaKit高并发实现原理](https://github.com/xiongziliang/ZLMediaKit/wiki/ZLMediaKit%E9%AB%98%E5%B9%B6%E5%8F%91%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) ## 项目定位 diff --git a/src/Http/HttpBody.cpp b/src/Http/HttpBody.cpp new file mode 100644 index 00000000..cc4aced4 --- /dev/null +++ b/src/Http/HttpBody.cpp @@ -0,0 +1,237 @@ +/* + * MIT License + * + * Copyright (c) 2016-2019 xiongziliang <771730766@qq.com> + * + * This file is part of ZLMediaKit(https://github.com/xiongziliang/ZLMediaKit). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "HttpBody.h" +#include "Util/util.h" +#include "Util/uv_errno.h" +#include "Util/logger.h" +#include "HttpClient.h" +#ifndef _WIN32 +#include +#endif + +#ifndef _WIN32 +#define ENABLE_MMAP +#endif + +namespace mediakit { + +HttpStringBody::HttpStringBody(const string &str){ + _str = str; +} +uint64_t HttpStringBody::remainSize() { + return _str.size() - _offset; +} + +Buffer::Ptr HttpStringBody::readData(uint32_t size) { + size = MIN(remainSize(),size); + if(!size){ + //没有剩余字节了 + return nullptr; + } + auto ret = std::make_shared(_str,_offset,size); + _offset += size; + return ret; +} + +////////////////////////////////////////////////////////////////// + +HttpFileBody::HttpFileBody(const std::shared_ptr &fp, uint64_t offset, uint64_t max_size) { + _fp = fp; + _max_size = max_size; +#ifdef ENABLE_MMAP + do { + int fd = fileno(fp.get()); + if (fd < 0) { + WarnL << "fileno failed:" << get_uv_errmsg(false); + break; + } + auto ptr = (char *) mmap(NULL, max_size, PROT_READ, MAP_SHARED, fd, offset); + if (ptr == MAP_FAILED) { + WarnL << "mmap failed:" << get_uv_errmsg(false); + break; + } + _map_addr.reset(ptr,[max_size,fp](char *ptr){ + munmap(ptr,max_size); + }); + } while (false); +#endif + if(!_map_addr && offset){ + //未映射,那么fseek设置偏移量 + fseek(fp.get(), offset, SEEK_SET); + } +} + + +class BufferMmap : public Buffer{ +public: + typedef std::shared_ptr Ptr; + BufferMmap(const std::shared_ptr &map_addr,uint64_t offset,int size){ + _map_addr = map_addr; + _data = map_addr.get() + offset; + _size = size; + }; + virtual ~BufferMmap(){}; + //返回数据长度 + char *data() const override { + return _data; + } + uint32_t size() const override{ + return _size; + } +private: + std::shared_ptr _map_addr; + char *_data; + uint32_t _size; +}; + +uint64_t HttpFileBody::remainSize() { + return _max_size - _offset; +} + +Buffer::Ptr HttpFileBody::readData(uint32_t size) { + size = MIN(remainSize(),size); + if(!size){ + //没有剩余字节了 + return nullptr; + } + if(!_map_addr){ + //fread模式 + int 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(_map_addr,_offset,size); + _offset += size; + return std::move(ret); +} + +////////////////////////////////////////////////////////////////// +HttpMultiFormBody::HttpMultiFormBody(const HttpArgs &args,const string &filePath,const string &boundary){ + std::shared_ptr fp(fopen(filePath.data(), "rb"), [](FILE *fp) { + if(fp){ + fclose(fp); + } + }); + if(!fp){ + throw std::invalid_argument(StrPrinter << "open file failed:" << filePath << " " << get_uv_errmsg()); + } + _fileBody = std::make_shared(fp, 0, fileSize(fp.get())); + + auto fileName = filePath; + auto pos = filePath.rfind('/'); + if(pos != string::npos){ + fileName = filePath.substr(pos + 1); + } + _bodyPrefix = multiFormBodyPrefix(args,boundary,fileName); + _bodySuffix = multiFormBodySuffix(boundary); + _totalSize = _bodyPrefix.size() + _bodySuffix.size() + _fileBody->remainSize(); +} + +uint64_t HttpMultiFormBody::remainSize() { + return _totalSize - _offset; +} + +Buffer::Ptr HttpMultiFormBody::readData(uint32_t size){ + if(_bodyPrefix.size()){ + auto ret = std::make_shared(_bodyPrefix); + _offset += _bodyPrefix.size(); + _bodyPrefix.clear(); + return ret; + } + + if(_fileBody->remainSize()){ + auto ret = _fileBody->readData(size); + if(!ret){ + //读取文件出现异常,提前中断 + _offset = _totalSize; + }else{ + _offset += ret->size(); + } + return ret; + } + + if(_bodySuffix.size()){ + auto ret = std::make_shared(_bodySuffix); + _offset = _totalSize; + _bodySuffix.clear(); + return ret; + } + + return nullptr; +} + +string HttpMultiFormBody::multiFormBodySuffix(const string &boundary){ + string MPboundary = string("--") + boundary; + string endMPboundary = MPboundary + "--"; + _StrPrinter body; + body << "\r\n" << endMPboundary; + return body; +} + +uint64_t HttpMultiFormBody::fileSize(FILE *fp) { + auto current = ftell(fp); + fseek(fp,0L,SEEK_END); /* 定位到文件末尾 */ + auto end = ftell(fp); /* 得到文件大小 */ + fseek(fp,current,SEEK_SET); + return end - current; +} + +string HttpMultiFormBody::multiFormContentType(const string &boundary){ + return StrPrinter << "multipart/form-data; boundary=" << boundary; +} + +string HttpMultiFormBody::multiFormBodyPrefix(const HttpArgs &args,const string &boundary,const string &fileName){ + string MPboundary = string("--") + boundary; + _StrPrinter body; + for(auto &pr : args){ + body << MPboundary << "\r\n"; + body << "Content-Disposition: form-data; name=\"" << pr.first << "\"\r\n\r\n"; + body << pr.second << "\r\n"; + } + body << MPboundary << "\r\n"; + body << "Content-Disposition: form-data; name=\"" << "file" << "\";filename=\"" << fileName << "\"\r\n"; + body << "Content-Type: application/octet-stream\r\n\r\n" ; + return body; +} + +}//namespace mediakit diff --git a/src/Http/HttpBody.h b/src/Http/HttpBody.h new file mode 100644 index 00000000..fd2b4318 --- /dev/null +++ b/src/Http/HttpBody.h @@ -0,0 +1,142 @@ +/* + * MIT License + * + * Copyright (c) 2016-2019 xiongziliang <771730766@qq.com> + * + * This file is part of ZLMediaKit(https://github.com/xiongziliang/ZLMediaKit). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef ZLMEDIAKIT_FILEREADER_H +#define ZLMEDIAKIT_FILEREADER_H + +#include +#include +#include "Network/Buffer.h" +#include "Util/ResourcePool.h" +#include "Util/logger.h" + +using namespace std; +using namespace toolkit; + +#ifndef MIN +#define MIN(a,b) ((a) < (b) ? (a) : (b) ) +#endif //MIN + +namespace mediakit { + +/** + * http content部分基类定义 + */ +class HttpBody{ +public: + typedef std::shared_ptr Ptr; + HttpBody(){} + virtual ~HttpBody(){} + + /** + * 剩余数据大小 + */ + virtual uint64_t remainSize() = 0; + + /** + * 读取一定字节数,返回大小可能小于size + * @param size 请求大小 + * @return 字节对象 + */ + virtual Buffer::Ptr readData(uint32_t size) = 0; +}; + +/** + * string类型的content + */ +class HttpStringBody : public HttpBody{ +public: + typedef std::shared_ptr Ptr; + HttpStringBody(const string &str); + virtual ~HttpStringBody(){} + uint64_t remainSize() override ; + Buffer::Ptr readData(uint32_t size) override ; +private: + mutable string _str; + uint64_t _offset = 0; +}; + +/** + * 文件类型的content + */ +class HttpFileBody : public HttpBody{ +public: + typedef std::shared_ptr Ptr; + + /** + * 构造函数 + * @param fp 文件句柄,文件的偏移量必须为0 + * @param offset 相对文件头的偏移量 + * @param max_size 最大读取字节数,未判断是否大于文件真实大小 + */ + HttpFileBody(const std::shared_ptr &fp,uint64_t offset,uint64_t max_size); + ~HttpFileBody(){}; + + uint64_t remainSize() override ; + Buffer::Ptr readData(uint32_t size) override; +private: + std::shared_ptr _fp; + uint64_t _max_size; + uint64_t _offset = 0; + std::shared_ptr _map_addr; + ResourcePool _pool; +}; + +class HttpArgs; + +/** + * http MultiForm 方式提交的http content + */ +class HttpMultiFormBody : public HttpBody { +public: + typedef std::shared_ptr Ptr; + + /** + * 构造函数 + * @param args http提交参数列表 + * @param filePath 文件路径 + * @param boundary boundary字符串 + */ + HttpMultiFormBody(const HttpArgs &args,const string &filePath,const string &boundary = "0xKhTmLbOuNdArY"); + virtual ~HttpMultiFormBody(){} + uint64_t remainSize() override ; + Buffer::Ptr readData(uint32_t size) override; +public: + static string multiFormBodyPrefix(const HttpArgs &args,const string &boundary,const string &fileName); + static string multiFormBodySuffix(const string &boundary); + static uint64_t fileSize(FILE *fp); + static string multiFormContentType(const string &boundary); +private: + string _bodyPrefix; + string _bodySuffix; + uint64_t _offset = 0; + uint64_t _totalSize; + HttpFileBody::Ptr _fileBody; +}; + +}//namespace mediakit + +#endif //ZLMEDIAKIT_FILEREADER_H diff --git a/src/Http/HttpClient.cpp b/src/Http/HttpClient.cpp index d9eafee0..c7483b33 100644 --- a/src/Http/HttpClient.cpp +++ b/src/Http/HttpClient.cpp @@ -242,8 +242,9 @@ void HttpClient::onRecvContent(const char *data, uint64_t len) { void HttpClient::onFlush() { _aliveTicker.resetTime(); + GET_CONFIG(uint32_t,sendBufSize,Http::kSendBufSize); while (_body && _body->remainSize() && !isSocketBusy()) { - auto buffer = _body->readData(); + auto buffer = _body->readData(sendBufSize); if (!buffer) { //数据发送结束或读取数据异常 break; diff --git a/src/Http/HttpClient.h b/src/Http/HttpClient.h index 1f8287e5..59ab4629 100644 --- a/src/Http/HttpClient.h +++ b/src/Http/HttpClient.h @@ -39,7 +39,7 @@ #include "HttpCookie.h" #include "HttpChunkedSplitter.h" #include "strCoding.h" - +#include "HttpBody.h" using namespace std; using namespace toolkit; @@ -64,145 +64,6 @@ public: } }; -class HttpBody{ -public: - typedef std::shared_ptr Ptr; - HttpBody(){} - virtual ~HttpBody(){} - //剩余数据大小 - virtual uint64_t remainSize() = 0; - virtual Buffer::Ptr readData() = 0; -}; - -class HttpStringBody : public HttpBody{ -public: - typedef std::shared_ptr Ptr; - HttpStringBody(const string &str){ - _str = str; - } - virtual ~HttpStringBody(){} - - uint64_t remainSize() override { - return _str.size(); - } - Buffer::Ptr readData() override { - auto ret = std::make_shared(_str); - _str.clear(); - return ret; - } -private: - mutable string _str; -}; - - -class HttpMultiFormBody : public HttpBody { -public: - typedef std::shared_ptr Ptr; - template - HttpMultiFormBody(const MapType &args,const string &filePath,const string &boundary,uint32_t sliceSize = 4 * 1024){ - _fp = fopen(filePath.data(),"rb"); - if(!_fp){ - throw std::invalid_argument(StrPrinter << "打开文件失败:" << filePath << " " << get_uv_errmsg()); - } - auto fileName = filePath; - auto pos = filePath.rfind('/'); - if(pos != string::npos){ - fileName = filePath.substr(pos + 1); - } - _bodyPrefix = multiFormBodyPrefix(args,boundary,fileName); - _bodySuffix = multiFormBodySuffix(boundary); - _totalSize = _bodyPrefix.size() + _bodySuffix.size() + fileSize(_fp); - _sliceSize = sliceSize; - } - virtual ~HttpMultiFormBody(){ - fclose(_fp); - } - - uint64_t remainSize() override { - return _totalSize - _offset; - } - - Buffer::Ptr readData() override{ - if(_bodyPrefix.size()){ - auto ret = std::make_shared(_bodyPrefix); - _offset += _bodyPrefix.size(); - _bodyPrefix.clear(); - return ret; - } - - if(0 == feof(_fp)){ - auto ret = std::make_shared(_sliceSize); - //读文件 - int size; - do{ - size = fread(ret->data(),1,_sliceSize,_fp); - }while(-1 == size && UV_EINTR == get_uv_error(false)); - - if(size == -1){ - _offset = _totalSize; - WarnL << "fread failed:" << get_uv_errmsg(); - return nullptr; - } - _offset += size; - ret->setSize(size); - return ret; - } - - if(_bodySuffix.size()){ - auto ret = std::make_shared(_bodySuffix); - _offset = _totalSize; - _bodySuffix.clear(); - return ret; - } - - return nullptr; - } - -public: - template - static string multiFormBodyPrefix(const MapType &args,const string &boundary,const string &fileName){ - string MPboundary = string("--") + boundary; - _StrPrinter body; - for(auto &pr : args){ - body << MPboundary << "\r\n"; - body << "Content-Disposition: form-data; name=\"" << pr.first << "\"\r\n\r\n"; - body << pr.second << "\r\n"; - } - body << MPboundary << "\r\n"; - body << "Content-Disposition: form-data; name=\"" << "file" << "\";filename=\"" << fileName << "\"\r\n"; - body << "Content-Type: application/octet-stream\r\n\r\n" ; - return body; - } - static string multiFormBodySuffix(const string &boundary){ - string MPboundary = string("--") + boundary; - string endMPboundary = MPboundary + "--"; - _StrPrinter body; - body << "\r\n" << endMPboundary; - return body; - } - - static uint64_t fileSize(FILE *fp) { - auto current = ftell(fp); - fseek(fp,0L,SEEK_END); /* 定位到文件末尾 */ - auto end = ftell(fp); /* 得到文件大小 */ - fseek(fp,current,SEEK_SET); - return end - current; - } - - static string multiFormContentType(const string &boundary){ - return StrPrinter << "multipart/form-data; boundary=" << boundary; - } -private: - FILE *_fp; - string _bodyPrefix; - string _bodySuffix; - uint64_t _offset = 0; - uint64_t _totalSize; - uint32_t _sliceSize; -}; - - - class HttpClient : public TcpClient , public HttpRequestSplitter { public: diff --git a/src/Http/HttpSession.cpp b/src/Http/HttpSession.cpp index e9ab7e81..ce45f379 100644 --- a/src/Http/HttpSession.cpp +++ b/src/Http/HttpSession.cpp @@ -44,6 +44,7 @@ #include "Util/base64.h" #include "Util/SHA1.h" #include "Rtmp/utils.h" +#include "HttpBody.h" using namespace toolkit; namespace mediakit { @@ -502,11 +503,6 @@ void HttpSession::Handle_Req_GET(int64_t &content_len) { return; } - //再看看是否为http-flv直播请求 - if(checkLiveFlvStream()){ - //若是,return! - return; - } //事件未被拦截,则认为是http下载请求 auto fullUrl = string(HTTP_SCHEMA) + "://" + _parser["Host"] + _parser.FullUrl(); @@ -556,6 +552,11 @@ void HttpSession::Handle_Req_GET(int64_t &content_len) { //访问的是文件 struct stat tFileStat; if (0 != stat(strFile.data(), &tFileStat)) { + //再看看是否为http-flv直播请求 + if(checkLiveFlvStream()){ + //若是,return! + return; + } //文件不存在 sendNotFound(bClose); throw SockException(bClose ? Err_shutdown : Err_success,"close connection after send 404 not found on file"); @@ -601,7 +602,6 @@ void HttpSession::Handle_Req_GET(int64_t &content_len) { } else { //分节下载 pcHttpResult = "206 Partial Content"; - fseek(pFilePtr.get(), iRangeStart, SEEK_SET); //分节下载返回Content-Range头 httpHeader.emplace("Content-Range",StrPrinter<<"bytes " << iRangeStart << "-" << iRangeEnd << "/" << tFileStat.st_size<< endl); } @@ -617,51 +617,34 @@ void HttpSession::Handle_Req_GET(int64_t &content_len) { throw SockException(bClose ? Err_shutdown : Err_success,"close connection after access file"); } //回复Content部分 - std::shared_ptr piLeft(new int64_t(iRangeEnd - iRangeStart + 1)); - GET_CONFIG(uint32_t,sendBufSize,Http::kSendBufSize); - + HttpBody::Ptr fileBody = std::make_shared(pFilePtr,iRangeStart,iRangeEnd - iRangeStart + 1); weak_ptr weakSelf = dynamic_pointer_cast(shared_from_this()); - auto onFlush = [pFilePtr,bClose,weakSelf,piLeft]() { - TimeTicker(); + + auto onFlush = [fileBody,bClose,weakSelf]() { auto strongSelf = weakSelf.lock(); - while(*piLeft && strongSelf){ + if(!strongSelf){ + //本对象已经销毁 + return false; + } + while(true){ //更新超时计时器 strongSelf->_ticker.resetTime(); - //从循环池获取一个内存片 - auto sendBuf = strongSelf->obtainBuffer(); - sendBuf->setCapacity(sendBufSize); - //本次需要读取文件字节数 - int64_t iReq = MIN(sendBufSize,*piLeft); - //读文件 - int iRead; - do{ - iRead = fread(sendBuf->data(), 1, iReq, pFilePtr.get()); - }while(-1 == iRead && UV_EINTR == get_uv_error(false)); - //文件剩余字节数 - *piLeft -= iRead; - - if (iRead < iReq || !*piLeft) { + //读取文件 + auto sendBuf = fileBody->readData(sendBufSize); + if (!sendBuf) { //文件读完 - if(iRead > 0) { - sendBuf->setSize(iRead); - strongSelf->send(sendBuf); - } - if(strongSelf->isSocketBusy()){ //套接字忙,我们等待触发下一次onFlush事件 //待所有数据flush到socket fd再移除onFlush事件监听 //标记文件读写完毕 - *piLeft = 0; return true; } - //文件全部flush到socket fd,可以直接关闭socket了 break; } //文件还未读完 - sendBuf->setSize(iRead); if(strongSelf->send(sendBuf) == -1) { //socket已经销毁,不再监听onFlush事件 return false; @@ -673,7 +656,7 @@ void HttpSession::Handle_Req_GET(int64_t &content_len) { //socket还可写,继续写socket } - if(bClose && strongSelf) { + if(bClose) { //最后一次flush事件,文件也发送完毕了,可以关闭socket了 strongSelf->shutdown(SockException(Err_shutdown,"read file eof")); }