現(xiàn)FTP服務(wù)器:QFtpServer原理與客戶端聯(lián)通實(shí)戰(zhàn))
簡(jiǎn)介基于QT開發(fā)的QFtpServer完整源碼包面向需要實(shí)現(xiàn)FTP服務(wù)端與客戶端的Qt開發(fā)者也適合通過項(xiàng)目實(shí)戰(zhàn)學(xué)習(xí)TCP套接字、FTP協(xié)議與網(wǎng)絡(luò)安全配置的人群。項(xiàng)目利用QTCP與QFtp類構(gòu)建完整實(shí)現(xiàn)用戶認(rèn)證、命令處理、目錄瀏覽、文件讀寫、數(shù)據(jù)連接管理等機(jī)制并加入SSL/TLS加密支持便于開發(fā)者快速搭建具備權(quán)限控制的安全文件傳輸服務(wù)。壓縮包共45個(gè)文件以12個(gè)cpp與11個(gè)h源碼為主配合pro工程、ui界面、qrc資源、pem證書及png圖標(biāo)整體僅198KB目錄結(jié)構(gòu)清晰。目前已有353人學(xué)習(xí)下載。通過源碼可深入理解FTP控制連接與數(shù)據(jù)連接的分工、STOR/RETR等命令的處理流程、服務(wù)器認(rèn)證策略及客戶端上傳下載邏輯同時(shí)工程自帶圖形界面和命令行兩個(gè)入口適合二次開發(fā)與教學(xué)案例使用。1. 先用 QFtpServer 說清一件事QT 里寫 FTP 服務(wù)端難點(diǎn)根本不在協(xié)議一個(gè)反直覺的結(jié)論放在開頭QT 寫 FTP 服務(wù)端真正卡人的不是 FTP 命令解析、不是 RFC 959 那幾十條指令而是「被動(dòng)模式下數(shù)據(jù)連接怎么建」「路徑與文件信息用什么編碼列給客戶端」「斷線重連時(shí)控制連接與數(shù)據(jù)連接誰來回收」。QFtpServer-master.zip這類以QFtpServer為類名前綴的代碼包在 GitHub 和 coding.net 上能搜到多個(gè)變體它們大多基于QTcpServer派生、用QHashQString, UserInfo做虛擬賬戶表核心價(jià)值是把「控制連接」和「數(shù)據(jù)連接」的生命周期拆成了兩個(gè)獨(dú)立的 socket 流。對(duì)想在自己的 QT 桌面應(yīng)用里內(nèi)嵌一個(gè) FTP 服務(wù)端、而不是單獨(dú)搭 vsftpd 或 FileZilla Server 的開發(fā)者來說這套結(jié)構(gòu)比協(xié)議本身更值得抄。這個(gè)標(biāo)題里同時(shí)出現(xiàn)了「QT FTP服務(wù)器」「ftp服務(wù)器端」「qt ftp客戶端」說明你大概率的需求是客戶端和服務(wù)端都要在 QT 里自包含不做跨語言調(diào)用。那么這篇博文就按「原理 → 服務(wù)端代碼骨架 → 客戶端怎么連 → 防火墻/編碼/斷線這些坑」的順序來寫。所有代碼基于 Qt 5.15.2 的QTcpServer/QTcpSocket不依賴QFtp那個(gè)被移出 Qt5 的模塊因?yàn)槟莻€(gè)類只解決了客戶端問題而且已經(jīng) deprecated。2. 拆解 QFtpServer 的協(xié)議處理與連接模型2.1 FTP 服務(wù)端的「控制連接 數(shù)據(jù)連接」雙通道模型FTP 與 HTTP 最大的差別在于HTTP 一個(gè)連接完成一次請(qǐng)求-響應(yīng)而 FTP 里「登錄、敲命令」走 21 端口控制連接「拉列表、傳文件」走另一個(gè)數(shù)據(jù)端口。主動(dòng)模式PORT是服務(wù)器主動(dòng)連客戶端給的 IP:端口被動(dòng)模式PASV是服務(wù)器開一個(gè)臨時(shí)監(jiān)聽端口、把端口號(hào)告訴客戶端讓客戶端來連。QFtpServer 這類實(shí)現(xiàn)普遍走 PASV 優(yōu)先主要原因有兩點(diǎn)。第一客戶端在 NAT 后面時(shí)主動(dòng)模式常常失敗——服務(wù)器去連客戶端的私有 IP 根本不可達(dá)第二QT 的QTcpServer可以輕松地為每個(gè) PASV 會(huì)話動(dòng)態(tài)監(jiān)聽一個(gè)端口而不需要像 vsftpd 那樣維護(hù)pasv_min_port到pasv_max_port的范圍策略。代碼里的形態(tài)通常是這樣FtpServer繼承QTcpServer在incomingConnection(qintptr handle)里把新連接包裝成FtpConnection對(duì)象每個(gè)FtpConnection內(nèi)部維護(hù)一個(gè)QTcpSocket用于控制通道另有一根QTcpServer *dataServer只在 PASV 時(shí)創(chuàng)建。響應(yīng)227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)時(shí)p1*256p2必須等于dataServer-serverPort()。初學(xué)者最常見錯(cuò)誤是響應(yīng)端口寫錯(cuò)、客戶端連不上其次是 PASV 只開一個(gè)固定端口、多客戶端同時(shí)下載時(shí)互相踩。QFtpServer 的優(yōu)勢(shì)在于它是「每連接一個(gè) FtpConnection、每 PASV 一個(gè) dataServer」天然規(guī)避了并發(fā)踩端口問題。2.2 Qt5/Qt6 下 FTP 相關(guān)庫的變遷為什么服務(wù)端代碼比客戶端更值得自己寫Qt 5 移除了QFtp官方推薦用QNetworkAccessManager的ftp://URL 支持但那個(gè)實(shí)現(xiàn)到 5.15 已經(jīng)基本不維護(hù)。Qt 6.4 之后QNetworkAccessManager的 FTP 也進(jìn)入停滯狀態(tài)社區(qū)普遍轉(zhuǎn)向libcurl或自己解析。這意味著如果你要寫「qt ftp客戶端」依賴官方模塊越來越不靠譜而QFtpServer這種基于QTcpServer的服務(wù)端反而沒有任何官方替代品顯得更「剛需」。從維護(hù)角度說FTP 服務(wù)端比客戶端簡(jiǎn)單幾十倍。客戶端要正確處理 TLS/SSL、代理、斷點(diǎn)續(xù)傳、目錄緩存這些邊界服務(wù)端只需要線性地處理命令、按用戶權(quán)限返回目錄列表、轉(zhuǎn)發(fā)文件字節(jié)流。QFtpServer 的代碼量通常在 2000 到 4000 行之間最復(fù)雜的部分不是協(xié)議而是路徑解析——要防御../越權(quán)要處理 Windows 盤符和 Linux 絕對(duì)路徑的區(qū)別。所以我的建議是不要去找那種「全功能帶界面」的服務(wù)端項(xiàng)目而是找QFtpServer-master.zip這種精簡(jiǎn)包把FtpConnection、FtpUserManager兩個(gè)類摳出來融進(jìn)你的應(yīng)用。下面第三、四章給一個(gè)可直接落地的骨架。3. 自己實(shí)現(xiàn)一個(gè)可嵌入的 QFtpServer 核心3.1 工程結(jié)構(gòu)與賬號(hào)校驗(yàn)的基礎(chǔ)代碼假設(shè)你的工程叫QFtpServer目錄結(jié)構(gòu)建議如下QFtpServer/ ├── QFtpServer.pro ├── src/ │ ├── ftpserver.h / ftpserver.cpp # QTcpServer 派生 │ ├── ftpconnection.h / ftpconnection.cpp │ ├── ftpuser.h / ftpuser.cpp │ └── main.cpp # 僅測(cè)試用ftpuser.h里定義一個(gè)最小結(jié)構(gòu)#ifndef FTPUSER_H #define FTPUSER_H #include QString #include QHash class FtpUser { public: FtpUser() : uid(0), canRead(false), canWrite(false) {} QString username; QString password; QString rootPath; // 虛擬根目錄實(shí)際路徑鎖定在此 int uid; bool canRead; bool canWrite; }; typedef QHashQString, FtpUser UserTable; #endif // FTPUSER_H.pro文件注意QT network如果你需要 TLS 再加QT network之外的模塊但 QFtpServer 原始版本用的純明文內(nèi)網(wǎng)用完全足夠。代碼邏輯說明UserTable用QHash的原因是通過用戶名查賬戶為 O(1)FTP 客戶端每次登錄都先發(fā)USER再發(fā)PASS兩條命令用哈希表避免遍歷。3.2 命令分發(fā)主循環(huán)從 USER/PASS 到 CWD/PWDFtpConnection類核心是一個(gè)狀態(tài)機(jī)。控制 socket 的readyRead信號(hào)觸發(fā)后handleCommand()讀取一行以\r\n結(jié)尾按命令字分發(fā)void FtpConnection::handleCommand() { while (m_socket-canReadLine()) { QByteArray line m_socket-readLine().trimmed(); // 去掉 \r\n int spaceIdx line.indexOf( ); QByteArray cmd line.left(spaceIdx).toUpper(); QByteArray param (spaceIdx -1) ? QByteArray() : line.mid(spaceIdx 1); if (cmd USER) { m_pendingUser QString::fromUtf8(param); m_socket-write(331 Password required.\r\n); } else if (cmd PASS) { handlePass(param); } else if (cmd SYST) { m_socket-write(215 UNIX Type: L8\r\n); } else if (cmd PWD) { m_socket-write(257 \ m_currentDir.toUtf8() \ is current directory.\r\n); } else if (cmd CWD) { handleCwd(param); } else if (cmd PASV) { handlePasv(); } else if (cmd LIST) { handleList(param); } else if (cmd RETR) { handleRetr(param); } else if (cmd STOR) { handleStor(param); } else if (cmd QUIT) { m_socket-write(221 Bye.\r\n); m_socket-disconnectFromHost(); } else { m_socket-write(502 Command not implemented.\r\n); } } }參數(shù)說明cmd USER時(shí)還不能立刻回復(fù)因?yàn)?QFtpServer 的認(rèn)證策略是 USER 和 PASS 分離需要在m_pendingUser里暫存用戶名等 PASS 來了再一起校驗(yàn)。PWD命令要返回257而不是200這是客戶端解析「當(dāng)前目錄字符串」的約定很多自研服務(wù)端寫錯(cuò)導(dǎo)致 Windows 資源管理器連不上。SYST返回215 UNIX Type: L8是為了讓客戶端知道列表格式是 UNIX 風(fēng)格方便ls -l解析。3.3 路徑穿越防護(hù)與虛擬目錄映射QFtpServer 這類內(nèi)嵌服務(wù)端最容易出現(xiàn)的安全漏洞就是路徑穿越。下面這段代碼是常見的防御寫法bool FtpConnection::changeDir(const QString newDir) { QString combined; if (newDir.startsWith(/)) { combined m_user.rootPath newDir; } else { combined m_currentDir newDir; } QDir dir(combined); if (!dir.exists()) { m_socket-write(550 Directory not found.\r\n); return false; } QString canonical dir.canonicalPath(); QString root QDir(m_user.rootPath).canonicalPath(); if (!canonical.startsWith(root)) { m_socket-write(550 Access denied.\r\n); return false; } m_currentDir combined; m_socket-write(250 Directory changed.\r\n); return true; }這里最關(guān)鍵的是canonicalPath()。調(diào)用后目錄中的..和.都會(huì)被解析成真實(shí)絕對(duì)路徑再與rootPath做前綴匹配可以擋住絕大多數(shù)穿越嘗試。需要注意 Windows 路徑的大小寫不敏感前綴比較時(shí)要把canonical和root都.toLower()否則 D 盤用戶訪問 d:\ftp 和 D:\FTP 會(huì)被判定為越權(quán)。LIST命令實(shí)現(xiàn)也很典型記得數(shù)據(jù)通道只傳目錄文本控制通道回狀態(tài)碼void FtpConnection::handleList(const QByteArray param) { if (!m_dataSocket || !m_dataSocket-isOpen()) { m_socket-write(425 No data connection.\r\n); return; } QDir dir(m_currentDir); QFileInfoList entries; if (param.trimmed().isEmpty() || param.trimmed() -a || param.trimmed() -l) { entries dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); } else { QDir sub(m_currentDir / param.trimmed()); entries sub.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); } QByteArray listing; for (const QFileInfo fi : entries) { QString perms fi.isDir() ? drwxr-xr-x : -rw-r--r--; listing QString(%1 1 owner group %2 Jan 01 00:00 %3\r\n) .arg(perms) .arg(fi.size()) .arg(fi.fileName()) .toUtf8(); } m_dataSocket-write(listing); m_dataSocket-disconnectFromHost(); m_socket-write(226 Transfer complete.\r\n); }這段代碼故意把時(shí)間戳寫死是為了避免QFileInfo::lastModified()與 FTP 客戶端的時(shí)區(qū)解析打架。如果你希望真實(shí)顯示修改時(shí)間用fi.lastModified().toString(MMM dd HH:mm)替代硬編碼。注意列表行必須以\r\n結(jié)尾如果只寫\nWindows 自帶的 FTP 客戶端會(huì)顯示成一行長(zhǎng)串。4. 把客戶端接進(jìn)來QFtpServer 與 QT 客戶端的聯(lián)通實(shí)戰(zhàn)4.1 QNetworkAccessManager 的 ftp:// 用法及其邊界既然標(biāo)題里有「qt ftp客戶端」那至少得有一個(gè)跑通的連接示例。最常見的做法是用QNetworkAccessManager的get()拉一個(gè)文件#include QNetworkAccessManager #include QNetworkReply #include QUrl #include QFile void downloadWithQnam(QNetworkAccessManager *manager, const QString url) { QUrl u(url); // 形如 ftp://user:pass127.0.0.1:21/file.txt QNetworkRequest request(u); QNetworkReply *reply manager-get(request); QFile *out new QFile(reply); // 用 reply 做父對(duì)象自動(dòng)釋放 out-setFileName(downloaded_from_ftp.txt); if (!out-open(QIODevice::WriteOnly)) { qWarning() cannot open output file; return; } QObject::connect(reply, QNetworkReply::readyRead, []() { out-write(reply-readAll()); }); QObject::connect(reply, QNetworkReply::finished, []() { out-close(); reply-deleteLater(); qInfo() download finished, bytes out-size(); }); }用 QNAM 的好處是 API 很簡(jiǎn)潔ftp://user:passhost這種 URL 會(huì)自動(dòng)完成認(rèn)證。但它在QFtpServer上有一個(gè)兼容問題QFtpServer 的LIST默認(rèn)返回 UNIX 格式QNAM 只做字節(jié)流傳輸不解析目錄列表所以目錄列舉還是得自己發(fā)LIST命令解析。另外QNetworkAccessManager不支持?jǐn)帱c(diǎn)續(xù)傳下載中斷只能從頭再來這是它和QFtp模塊的一個(gè)重大差距。4.2 手工實(shí)現(xiàn)一個(gè)最小 FTP 客戶端控制器如果 QNAM 不能滿足你的需求——比如要顯示傳輸進(jìn)度、要支持?jǐn)帱c(diǎn)續(xù)傳、要控制被動(dòng)模式開關(guān)——那就得換用QTcpSocket自己寫。下面是一個(gè)最小客戶端連接邏輯void connectToFtp(QTcpSocket *ctrlSocket, const QString host, quint16 port, const QString user, const QString pass) { ctrlSocket-connectToHost(host, port); QObject::connect(ctrlSocket, QTcpSocket::readyRead, []() { while (ctrlSocket-canReadLine()) { QByteArray line ctrlSocket-readLine().trimmed(); qInfo() S: line; if (line.startsWith(220)) { ctrlSocket-write(USER user.toUtf8() \r\n); } else if (line.startsWith(331)) { ctrlSocket-write(PASS pass.toUtf8() \r\n); } else if (line.startsWith(230)) { ctrlSocket-write(PASV\r\n); } else if (line.startsWith(227)) { // 解析 (h1,h2,h3,h4,p1,p2) parsePasvResponse(line); } } }); }這個(gè)狀態(tài)機(jī)把服務(wù)端的回碼映射到本地的下一步動(dòng)作。220歡迎、331要密碼、230登錄成功、227被動(dòng)模式參數(shù)。注意ctrlSocket-write()后面要 append\r\n這是 FTP 協(xié)議的行終結(jié)符很多初學(xué)客戶端的人只寫\n導(dǎo)致服務(wù)端讀不到命令、表現(xiàn)為「連接超時(shí)」。4.3 Windows 服務(wù)器防火墻與 FTP 被動(dòng)端口配置QFtpServer 跑在 Windows 上時(shí)被問最多的問題是「ftp 無法與服務(wù)器建立連接」。排查順序是先確認(rèn)21端口入站放行再確認(rèn)被動(dòng)端口范圍放行。假如 QFtpServer 的pasv端口范圍是50000-50100需要兩條命令管理員權(quán)限netsh advfirewall firewall add rule nameQFtpServer_TCP21 dirin actionallow protocolTCP localport21 netsh advfirewall firewall add rule nameQFtpServer_PASV dirin actionallow protocolTCP localport50000-50100提示如果只放行 21 端口被動(dòng)模式列表和傳輸全部連接失敗表現(xiàn)為客戶端能登錄但LIST命令425錯(cuò)誤。如果你的場(chǎng)景是局域網(wǎng)測(cè)試可以臨時(shí)關(guān)閉防火墻確認(rèn)問題是不是它引起但正式環(huán)境別這么做。正確辦法是在QFtpServer里把pasv_min_port和pasv_max_port做成可配置項(xiàng)Windows 防火墻規(guī)則與之一致。有些教程讓你「關(guān)閉防火墻」那是對(duì)未知端口范圍的一種偷懶手段不是解決方案。5. 性能、編碼與異常處理的三個(gè)深水區(qū)5.1 RETR/STOR 傳輸循環(huán)read 與 write 的緩沖大小選擇傳文件是 FTP 服務(wù)端最核心的數(shù)據(jù)操作。初版實(shí)現(xiàn)很容易寫成一次性readAll()再write()這對(duì)超過 1GB 的文件是災(zāi)難。常規(guī)做法是循環(huán) 64KB 分塊void sendFileToClient(const QString filePath) { QFile f(filePath); if (!f.open(QIODevice::ReadOnly)) { m_socket-write(550 Failed to open file.\r\n); return; } m_socket-write(150 Sending file.\r\n); QByteArray buffer; buffer.resize(64 * 1024); qint64 bytesRead; while ((bytesRead f.read(buffer.data(), buffer.size())) 0) { m_dataSocket-write(buffer.constData(), bytesRead); m_dataSocket-waitForBytesWritten(3000); } f.close(); m_socket-write(226 Transfer complete.\r\n); }這里waitForBytesWritten(3000)的作用是背壓——如果客戶端接收慢服務(wù)端不會(huì)把整個(gè)文件塞進(jìn) socket 緩沖區(qū)導(dǎo)致內(nèi)存暴漲。64KB 是吞吐率和內(nèi)存占用之間的常用折中實(shí)測(cè)千兆局域網(wǎng)下可以達(dá)到 110MB/s 左右的線速。注意m_socket-write(150)必須放在數(shù)據(jù)傳輸開始前、m_socket-write(226)必須等數(shù)據(jù)全部寫完后再發(fā)順序顛倒客戶端會(huì)報(bào)「?jìng)鬏敵瑫r(shí)」。5.2 中文文件名與編碼UTF-8 與本地編碼的兼容策略QFtpServer 的一個(gè)大坑是中文文件名。控制連接默認(rèn)用 UTF-8 解析命令但 Windows 自帶的 FTP 客戶端ftp.exe發(fā)送的路徑名可能是本地 ANSI 編碼GBK。這會(huì)導(dǎo)致「中文目錄打不開」「中文文件名下載出來亂碼」兩個(gè)癥狀。常見解決策略是在handleCwd、handleRetr、handleStor里統(tǒng)一先按 UTF-8 解碼如果失敗再按QTextCodec::codecForName(GBK)解碼QString decodePath(const QByteArray raw) { QTextCodec *utf8 QTextCodec::codecForName(UTF-8); QTextCodec *gbk QTextCodec::codecForName(GBK); QTextCodec::ConverterState state; QString s utf8-toUnicode(raw.constData(), raw.size(), state); if (state.invalidChars 0) { return gbk-toUnicode(raw); } return s; }參數(shù)說明ConverterState::invalidChars記錄無效字符個(gè)數(shù)非零說明原始字節(jié)串不是合法 UTF-8再回退 GBK。在 Windows 上用這招基本能覆蓋所有中文場(chǎng)景Linux 客戶端一般發(fā) UTF-8macOS 的 Finder 也是 UTF-8所以回退邏輯只在 Windows 平臺(tái)被觸發(fā)。5.3 QT 崩潰與槽函數(shù)返回值之間的隱性關(guān)聯(lián)QFtpServer運(yùn)行中崩潰有相當(dāng)比例不是 FTP 業(yè)務(wù)代碼的錯(cuò)而是信號(hào)槽跨線程使用姿勢(shì)不對(duì)。FTP 的數(shù)據(jù)傳輸是有阻塞語義的如果你把sendFileToClient放在 GUI 線程里直接調(diào)用waitForBytesWritten界面會(huì)假死如果你用QtConcurrent跑了數(shù)據(jù)線程卻直接訪問FtpConnection里的QString成員會(huì)有極小概率觸發(fā)崩潰。一個(gè)穩(wěn)妥的線程方案是FtpConnection只存活于 QT 的事件循環(huán)線程RETR/STOR命令只往m_dataSocket里寫數(shù)據(jù)但不wait用bytesWritten信號(hào)驅(qū)動(dòng)下一塊數(shù)據(jù)發(fā)送。這樣不阻塞 GUI也沒有跨線程訪問問題void FtpConnection::startSending() { m_fileOffset 0; connect(m_dataSocket, QTcpSocket::bytesWritten, this, [](qint64) { if (m_fileOffset m_file-size()) { QByteArray chunk m_file-read(qMinqint64(64 * 1024, m_file-size() - m_fileOffset)); m_fileOffset chunk.size(); m_dataSocket-write(chunk); } else { m_socket-write(226 Transfer complete.\r\n); m_dataSocket-disconnectFromHost(); } }); }這段代碼避免了waitForBytesWritten的阻塞式自旋也讓文件句柄m_file的生命周期與數(shù)據(jù) socket 解耦。QT 槽函數(shù) 返回值那個(gè)熱詞在這里的對(duì)應(yīng)點(diǎn)就是槽函數(shù)里不要返回?cái)?shù)據(jù)用成員變量或者信號(hào)傳遞狀態(tài)否則在跨線程BlockingQueuedConnection下會(huì)直接觸發(fā)崩潰。6. QFtpServer 上線前的驗(yàn)證清單與性能壓測(cè)技巧QFtpServer 這類自研服務(wù)端最大的問題是沒有內(nèi)置測(cè)試工具。我的做法是用lftp做壓力驗(yàn)證——它比 Windows 自帶客戶端嚴(yán)格得多能暴露服務(wù)端在命令時(shí)序上的不合規(guī)。lftp -u testuser:testpass 192.168.1.10 -e set ftp:passive-mode on; mirror --parallel5 /local/download /remote/upload; quitmirror會(huì)并發(fā)生成多個(gè)數(shù)據(jù)傳輸流程考驗(yàn) PASV 端口管理是否泄漏如果QFtpServer的 PASV socket 在文件傳完后沒有銷毀lftp 第二次連接就會(huì)收到不一致的端口或者500錯(cuò)誤。沒有 lftp 時(shí)也可以手工驗(yàn)證三個(gè)最基本路徑主動(dòng)模式下載、被動(dòng)模式下載、被動(dòng)模式上傳。之所以強(qiáng)調(diào)「被動(dòng)模式」優(yōu)先是因?yàn)楝F(xiàn)在絕大多數(shù)客戶端默認(rèn) PASV而不少Q(mào)FtpServer實(shí)現(xiàn)為了省事只做了 PORT 主動(dòng)模式導(dǎo)致客戶端連接列表失敗。QFtpServer-master 原版在這塊做得比較好因?yàn)樗?PASV 請(qǐng)求都新建臨時(shí)QTcpServer用完即釋放不會(huì)和下一個(gè)客戶端沖突。最后給一個(gè)可以拿去復(fù)盤實(shí)際使用效果的全局開關(guān)設(shè)計(jì)供各位把QFtpServer集成進(jìn)自己產(chǎn)品前做個(gè)能力對(duì)照功能點(diǎn)最低可接受推薦配置最大并發(fā)連接逐個(gè) accept 即可不設(shè)限按系統(tǒng)文件句柄數(shù)-100 限制超出回421數(shù)據(jù)連接空閑超時(shí)無要求60 秒無數(shù)據(jù)讀寫在RETR/STOR中強(qiáng)制斷開同一用戶多會(huì)話允許但各自獨(dú)立用uid做互斥鎖避免同一文件并發(fā)寫速度限制無令牌桶按每 100ms 累積配額控制write速度常見做法是壓測(cè)時(shí)把上述配置中「并發(fā)限制」調(diào)到 100 左右「速度限制」關(guān)閉然后連發(fā)STOR一百萬字節(jié)的小文件看內(nèi)存是否會(huì)隨連接數(shù)線性上漲。如果每個(gè)連接泄漏 64KB 的QByteArray緩沖區(qū)連續(xù)跑 10 分鐘就能看到QProcess內(nèi)存明顯膨脹。用這個(gè)檢查方式七八成 FPTServer 自身的內(nèi)存泄漏問題都能在半小時(shí)內(nèi)暴露出來而不必等到線上崩潰。本文還有配套的精品資源點(diǎn)擊獲取