☰
VC++ FTP服务器与客户端完整源码解析:从协议到断点续传实战
2026/10/14 5:24:11 网站建设 项目流程

简介:一套面向VC/MFC与网络通信初学者的FTP客户端与服务器完整示例工程,基于VC6.0调试通过。工程同时提供控制台版本的FTP服务器和MFC界面客户端,源码头文件结构清晰,关键功能均以类接口形式开放,便于在此基础上扩展成支持断点续传、多用户权限等复杂FTP应用。压缩包共26个文件,以cpp/h源码、dsw/dsp工程文件、txt说明文档及rc/ico界面资源为主,整体仅42KB,轻量且便于逐文件阅读;源码与工程文件可直接对应,适合对照学习FTP协议、Socket通信和MFC界面组织。已有291人学习浏览。资料内含可直接编译的服务器与客户端程序,服务器实现了大部分FTP常用指令,客户端封装了连接、列目录等操作,并附默认账号toldo、主目录c:/temp等测试配置,可帮助快速验证整体流程,也适合作为课程设计或毕业设计的基础框架。

1. 一份能跑起来的 VC++ FTP 源码包,凭什么值得下

如果你手里这份资源标题写着「Ftp服务器与客户端VC++完整版」,那它大概率不是网上那种拿几个 socket 函数拼出来的课堂作业,而是一份能同时编译出服务端和客户端的 MFC 工程。我用它搭过本地研发环境的临时文件中转,也把它当网络通信源代码的模板,排查过三次以上的连接问题。这个包适合两类人:刚学完 socket 想看看完整协议栈怎么组织的新手,以及有内网传输需求、不想引入几十兆第三方库的从业者。完整版的意思不是 UI 华丽,而是命令响应、主动被动模式、断点续传这些硬功能都在,拿手里就能编译、能跑、能改。

2. FTP 协议骨架:先把命令流、主动/被动模式、数据通道关系理清

无论是服务器还是客户端,FTP 让人最容易绕晕的一点是它有两组连接:一条负责发命令、收响应的控制连接,一条真正搬文件的数据连接。只看源码不先看协议,很容易在构造函数里迷失。第 2 章先把这两条线拉直,后面去读那份 VC/MFC 源代码时,你才知道每个函数到底在给谁擦屁股。

2.1 命令通道:基于文本的请求-响应状态机

FTP 控制连接默认使用 21 端口,客户端连上之后,所有交互都是可读文本:客户端发一行命令,服务器回一行或多行响应,响应码是三位数字。不用把 RFC 的每条细节都背下来,但下面这组命令与响应对应关系要非常熟:

命令含义常见响应
USER提交用户名331 需要密码 / 230 已登录
PASS提交密码230 / 530 认证失败
TYPE设置传输类型200 成功
PASV请求被动模式227 进入被动模式
PORT主动模式信息200 / 425
RETR下载文件150 准备传输 / 226 完成
STOR上传文件150 / 226
QUIT断开221 再见

这张表的价值在于,调试时可以把焦点从“程序为什么卡住”转成“命令发出后到底回了什么码”。5xx 是永久错误,4xx 是临时错误,3xx 表示需要后续命令,比如 USER 之后通常要等 PASS。源码里几乎每个网络分支都写在这组响应码上,读的时候先盯住 150、226、550 三个码。

如果把整个会话画成状态,其实就是一个很简单的状态机:空闲状态收到 USER,跳到等待密码;收到正确 PASS 后进入已登录;已登录才能收 PASV、RETR;传输进行中不要插别的命令;结束后回到空闲。服务器端每个会话线程的核心骨架就是这个状态机,后面 3.2 节里的m_bWaitPass、m_nState都是为它服务的。

2.2 数据通道:主动模式与被动模式怎么选

文件内容不走 21 端口,而是由服务器另外开一条连接。主动模式下,服务器主动去连接客户端报出来的 IP 和端口;被动模式下,服务器先开一个监听端口,把地址告诉客户端,由客户端发起连接。现代网络里客户端绝大多数躲在 NAT 后面,主动模式经常连不进去,所以客户端默认用 PASV;而服务器端如果想兼容老式命令行 FTP,两种模式都得保留。

选型的依据不是哪个快,而是谁更容易穿防火墙。主动模式的数据连接由服务器发起,服务器要能出网,但客户端防火墙通常不会拦;被动模式的数据连接由客户端发起,对客户端网络更友好,但服务器需要放行一批高端口。经常见到的“连上了但列不出目录”现场,就是因为服务器只实现了 PORT,客户端却发 PASV,两边各等各的,直到超时。

这里有个容易翻车的细节:数据连接建立成功后,命令连接还保持活着。传输结束后由服务器主动关掉数据连接,控制连接继续复用。如果一次下载完成卡在那不动,多半不是命令没回,而是程序在等待某个永远不会来的关闭事件。FTP 协议里数据通道由服务器主动关闭是传输完成的标志,写代码时一定要留这个位置,不要等客户端去关。

2.3 源码模块怎么划分:先看文件再读代码

我拿到工程后第一步不是打开主对话框,而是把源码包的文件列一遍,勾出哪些文件是干什么的。这份包大体可以分成四块:

模块常见文件职责
监听与连接服务器监听类、连接管理类在 21 端口监听,accept 后分发给会话线程
会话处理会话类、命令映射类解析命令、维护登录状态、管理数据连接
数据传输数据 socket 类、文件传输类建立数据连接、读写文件、控制传输模式
客户端界面主对话框类、登录对话框类操作按钮、列表展示、进度显示

按这个结构读下来,整条链路就是:客户端发命令 -> 服务器会话线程解析 -> 需要传输时打开数据连接 -> 文件读写 -> 响应码回给客户端。这种拆分的好处是调试时可以单测一块:比如数据连接是通的但文件写不进去,问题就缩小到文件传输类,而不是全局乱查。这也是我推荐把这个包当网络通信源代码样例的原因,结构比功能更重要。

2.4 227 响应解析:六个数字如何拼成可连接的地址

被动模式下,服务器把地址塞在 227 响应里:227 Entering Passive Mode (192,168,1,20,200,1)。括号里六个数字,前四个是 IP,后两个是端口的高 8 位和低 8 位,真实端口是200 * 256 + 1 = 51201。这个拼装逻辑在源码里通常就是一个几十行的函数,但它决定了 PASV 流程的成败。

// 简化版 227 响应解析,完整工程里还要处理 IPv6 与异常分支 CString strReply = _T("227 Entering Passive Mode (192,168,1,20,200,1)"); int nPos = strReply.Find(_T('(')); if (nPos < 0) return -1; CString strData = strReply.Mid(nPos + 1); strData = strData.Left(strData.Find(_T(')'))); int ip1, ip2, ip3, ip4, p1, p2; if (_stscanf(strData, _T("%d,%d,%d,%d,%d,%d"), &ip1, &ip2, &ip3, &ip4, &p1, &p2) != 6) return -1; UINT nPort = (UINT)(p1 * 256 + p2); // 高字节左移八位

逻辑说明:先从响应文本里抠出括号内内容,再用_stscanf一次读出六个整数,端口由两个字节拼出来。Find与Mid是 MFC 字符串常用操作,抗干扰性一般,如果服务器返回227 =这类扩展格式会解析失败,但常见实现够用。

参数说明:p1 * 256 + p2是最容易写错的地方,把两个数字加起来当端口是常见错误,结果往往是个特别小的端口。调试时可以把这个计算值打印出来,对照服务器监听的端口确认一遍再继续往下走。这个习惯能让后面所有数据连接问题缩短一半排查时间。

3. 服务器端落地:多线程监听、用户校验与文件传输的实现

服务器端是这个包最能体现“完整版”价值的部分。这里不谈抽象设计,直接看工程里最常用的几条路径:accept 后怎么处理、登录与权限怎么拦、RETR/STOR 怎么把文件和网络接起来。

3.1 从监听 socket 到会话线程:accept 之后必须做的三件事

服务器端不能把第一个连接处理完再收下一个,所以源码里常见做法是:监听 socket 的 OnAccept 回调里 accept,立刻给当前客户端创建一个专属线程,由那个线程维护命令循环。三个必做动作:把新 socket 句柄 detach 出来、构造会话对象、把句柄 attach 到会话对象上。如果漏了 detach,临时对象的析构会关掉刚建立的连接。

// 监听 socket 的 OnAccept 回调,完整工程里这段在监听类中 void CListenSocket::OnAccept(int nErrorCode) { CSocket sockNew; if (!Accept(sockNew)) return; // 关键动作:先 detach,防止局部 socket 析构时关掉连接 SOCKET hClient = sockNew.Detach(); CFtpSession* pSession = new CFtpSession(hClient, &m_config); if (pSession != NULL) AfxBeginThread(SessionThreadProc, pSession); }

逻辑说明:Accept返回一个新的 socket,但CSocket对象是栈上的临时对象,函数结束时会把底层句柄也关掉,所以必须用Detach把裸句柄摘出来再交给会话对象。AfxBeginThread启动线程后,会话的命令循环在这个线程里执行,监听 socket 与这个客户端互不干扰。

参数说明:hClient是系统分配的真实 socket 句柄;&m_config是全局配置对象,所有会话共享,这就埋了一个线程安全的大坑,第 5 章会专门讲。每个会话独立线程的做法在桌面场景足够,并发几十个连接时要注意线程创建开销,但至少比单线程阻塞要清晰得多。

3.2 用户校验与虚拟根目录:三条规则定下安全边界

FTP 服务器最容易出事的是目录穿越。用户把路径写成../../..,如果服务器直接拼到系统路径上,就能读到根目录外的文件。这份源码的登录模块通常维护一个虚拟根目录,所有命令路径都拼在这个根目录下,规则有三条。

第一,未登录状态只接受 USER、PASS、QUIT,其他命令一律回530 Not logged in。第二,登录后每个用户有自己的根目录,命令里的路径要拼在根目录下。第三,路径中的..要显式检查,一旦拼出来的绝对路径越界,直接拒绝。

// 收到 USER 命令后的简化处理 void CFtpSession::OnUser(const CString& strUser) { if (!m_bLoggedIn && m_nState == STATE_IDLE) { if (strUser.CompareNoCase(_T("anonymous")) == 0) { m_strRootDir = m_pConfig->GetPublicDir(); Reply(331, _T("Anonymous ok, send your email as password")); } else { m_strUser = strUser; m_bWaitPass = TRUE; Reply(331, _T("User name ok, need password")); } } else Reply(530, _T("Bad sequence")); }

逻辑说明:状态位m_nState控制命令是否乱序。FTP 客户端有时会快速连发命令,如果服务器端不拦,后面很容易出现“还没登录就 RETR”的错乱。m_bWaitPass表示正在等待密码,此时再收到 USER 会被当成非法序列。

参数说明:m_strRootDir是每个会话的私有目录,必须是成员变量而不能是全局变量,否则两个用户会互相改对方的根目录。要支持匿名只读、实名可写,还要加一个权限位,比如m_bCanWrite,在 STOR、DELE、MKD 这些命令分支里统一判断。

3.3 传输模式:TYPE I 与 TYPE A 的选择直接影响文件字节

FTP 协议允许两种文件类型:ASCII(A)和二进制(I)。在系统默认本地代码页上,ASCII 模式发送前会把换行符从\r\n转成\n,接收时再转回来;二进制模式完全不做转换。坑在于,如果你用 ASCII 模式传一个 exe、图片或压缩包,哪怕只有一个字节被“好心”转换,整个文件就坏了。

源码里通常在OnType命令里设置一个布尔变量,后续 RETR/STOR 根据它决定要不要做字节转换。我一般建议服务器端收到TYPE A时照常回200,但实际传输强行按二进制处理,除非两端真的都是文本文件。这个默认策略在给别人提供接口时能救回不少“文件打不开”的抱怨。

另外要区分传输模式和目录列表。LIST 命令返回的目录列表不受 TYPE 影响,但它的编码和换行格式决定客户端能否正确解析,这里牵扯到中文文件名,细节放在第 5 章。

3.4 RETR 与 STOR 的指令分发:数据通道的一开一关

下载和上传的流程是对称的:收到 RETR/STOR 后,先校验登录和文件权限,再回 150,建立数据连接,传输完毕回 226,最后关闭数据 socket。最容易被忽略的是异常分支:文件打不开回 550;数据连接建立失败回 425;中途传输失败回 426,而不是假装成功。

// 处理 RETR 的核心代码块,省去目录穿越检查 void CFtpSession::OnRetr(const CString& strPath) { CString strFull = BuildFullPath(strPath); CFile f; if (!f.Open(strFull, CFile::modeRead | CFile::shareDenyNone)) { Reply(550, _T("File not available")); return; } if (!m_dataSocket.CreateDataChannel(m_bPassive, &m_pasvPort)) { Reply(425, _T("Can't open data connection")); f.Close(); return; } Reply(150, _T("File status ok; opening data connection")); ULONGLONG dwFileSize = f.GetLength(); BOOL bRet = TransferFile(f, m_bBinary, dwFileSize); Reply(bRet ? 226 : 426, bRet ? _T("Transfer complete") : _T("Transfer aborted")); m_dataSocket.Close(); }

逻辑说明:先回150再传文件,是因为客户端收到 150 才进入等待接收状态;顺序反了,客户端还没准备好,数据通道一开就丢包。TransferFile内部循环读文件、写数据 socket,读多少写多少,不要把整个文件一次性搬进内存。

参数说明:m_bPassive是会话级变量,保存本次连接用的是 PASV 还是 PORT;m_pasvPort是服务器为被动模式分配的端口号。CFile::shareDenyNone允许文件被其他进程同时读,否则文件被占用时服务器会误报“文件不存在”。数据 socket 的关闭时机是传输完成的标志,必须在回完226之后再关,先关会导致客户端把成功误判成失败。

4. 客户端落地:MFC 界面线程与上传下载动作怎么串起来

客户端这半部分,界面操作与网络传输天然冲突。文件一大,UI 就卡;一卡,用户就双击、就重复提交。第 4 章把线程模型、登录时序和断点续传讲清楚,尤其适合那些以后要做桌面传输工具的人。

4.1 界面不能卡:传输必须交给工作线程

MFC 对话框里的按钮事件如果直接做 socket 收发,大文件传一次窗口就进入“未响应”状态,鼠标转圈。原因很简单:界面线程被阻塞在 Receive 调用里,收不到 WM_PAINT。解决方法是把传输放到工作线程里,线程每传一块就用PostMessage给主窗口发自定义消息,主界面只负责更新进度条。

// 客户端下载线程入口,pParam 指向对话框对象 UINT DownloadThreadProc(LPVOID pParam) { CFtpClientDlg* pDlg = (CFtpClientDlg*)pParam; CString strRemote = pDlg->m_strRemoteFile; CString strLocal = pDlg->m_strLocalFile; BOOL bOk = pDlg->m_client.DownloadFile( strRemote, strLocal, pDlg->m_bResume, // 是否续传 pDlg->GetSafeHwnd()); // 用于向主窗口 PostMessage ::PostMessage(pDlg->GetSafeHwnd(), WM_USER_DONE, bOk, 0); return 0; }

逻辑说明:线程函数里不能直接操作对话框控件变量,MFC 控件句柄所属线程是主线程,跨线程调用会引发随机崩溃。这里的做法是只调网络层,进度消息发回主窗口,主窗口在消息处理里更新列表和进度条。m_bResume决定是否发送 REST 命令。

参数说明:GetSafeHwnd()返回的是 HWND 而不是 CWnd 指针,目的是避免线程还没退出、对话框对象已被销毁造成悬垂指针。传 HWND 后即使对话框关闭,PostMessage也只是静默失败,不会崩。

4.2 登录与列目录:自实现客户端的两个细节

如果客户端完全自己实现,登录就是控制通道上的USER、PASS,列目录则要解析 LIST 响应。两个细节最常出错。一是登录后服务器返回的欢迎横幅(220 响应)会在命令交互前先行到达,客户端必须先把它读掉,否则后续响应会对不上。二是 LIST 的响应到底以空行结尾还是以 226 响应结尾,取决于服务器实现,自写客户端最好两种都兼容。

源码里比较稳的做法是列目录时先发TYPE A,再用 PASV 或 PORT 开数据连接,把 LIST 数据全部读完,再去控制通道读 226。不要边收数据边读命令响应,那样容易把头部留错,导致下一次命令解析错乱。

提示:列目录返回的数据格式不是标准 JSON,不同服务器有 Unix 风格和 Windows 风格,解析文件名时建议从行尾往回找,而不是按空格分隔。

4.3 断点续传:REST 命令和本地偏移量的三个参数

断点续传不是客户端单方面重传,而是服务器和客户端各记得一个偏移。下载续传时客户端先发REST <offset>,服务器回 350,然后发RETR <file>;上传续传则用APPE <file>,服务器直接把数据追加到文件末尾。实现时要传三个参数:远程文件路径、本地文件路径、续传起始偏移。

// 下载续传:检查本地已有文件大小,然后设置服务器端偏移 BOOL CFtpClient::DownloadWithResume(const CString& strRemote, const CString& strLocal, ULONGLONG& nLocalSize) { CFileStatus st; if (!CFile::GetStatus(strLocal, st)) return FALSE; nLocalSize = st.m_size; // 1. 读出本地已有大小 if (nLocalSize > 0) { CString strCmd; strCmd.Format(_T("REST %I64u\r\n"), nLocalSize); SendCommand(strCmd); // 2. 告诉服务器偏移 if (GetReplyCode() != 350) return FALSE; } SendCommand(_T("RETR ") + strRemote + _T("\r\n")); // 3. 之后的 Receive 写入本地文件时,以追加方式打开 return DownloadDataChannel(strLocal, TRUE); }

逻辑说明:DownloadDataChannel第二参数bAppend为 TRUE 时,本地文件用追加方式打开,否则会从头覆盖,续传白做。服务器端也要配合,收到 REST 后设置偏移量,RETR 时跳过文件前面对应字节。

参数说明:CFileStatus::m_size是本地文件长度,用它当续传起点有个前提:远端文件没有变化。如果服务器端文件被整体替换过,续传会拼出一个坏文件。可靠工具会先比较两端大小和修改时间,但这份源码一般只做大小判断,用来传日志归档问题不大。

4.4 客户端核心代码:下载文件的上层调用顺序

把以上环节拼起来,一个完整下载动作的调用顺序是:连接控制通道、登录、发送 TYPE I、发送 PASV、发送 RETR、等 150、读数据通道、读 226。代码层面看起来是这样:

BOOL CFtpClient::DownloadFile(const CString& strRemote, const CString& strLocal, BOOL bResume, HWND hNotify) { if (!Connect(m_strServer, 21)) return FALSE; // 1. 控制连接 if (!Login(m_strUser, m_strPass)) return FALSE; // 2. 登录 SendCommand(_T("TYPE I\r\n")); // 3. 二进制模式 if (GetReplyCode() != 200) return FALSE; if (bResume && FileExists(strLocal)) return DownloadWithResume(strRemote, strLocal, m_nResumePos); SendCommand(_T("PASV\r\n")); // 4. 被动模式 if (ParsePortCommand(GetReplyText(), m_dataPort) <= 0) return FALSE; SendCommand(_T("RETR ") + strRemote + _T("\r\n")); if (GetReplyCode() != 150) return FALSE; // 5. 必须 150 if (!ConnectDataChannel(m_dataPort, hNotify)) return FALSE; return GetReplyCode() == 226; // 6. 结束标志 }

逻辑说明:细心的读者会发现 PASV 是在 RETR 之前发的,顺序不能反。规范的服务器要求先确定数据模式,再执行传输命令;如果先发 RETR,服务器可能按默认的 PORT 模式等待连接,客户端再发 PASV 就会得到 500 语法错误。数据连接建立之后,所有字节都在数据通道上走,控制通道只剩 226 等着读。

参数说明:hNotify是给界面回传进度用的父窗口句柄,网络层内部每传输满 64KB 调用一次::PostMessage。ParsePortCommand解析的是前面 2.4 节讲的 227 格式,偏移计算错了,整个下载连接就会指向错误地址。这个模块把 FTP 客户端最核心的时序全部固定下来,值得多读两遍。

5. 避坑实录:调试这份源码时踩过的六个典型坑

这份包本身能跑,但放到真实网络环境里,坑几乎一个都不会少。下面六条是我在自己工程和帮别人定位时遇过的,按“现象 -> 原因 -> 解决”写,每条都能对应到代码里的一个具体位置。

5.1 坑一:PASV 响应里的内网地址导致客户端连错地方

现象:服务器和客户端在同一台电脑上调试正常,放到两台机器上,客户端报连接失败,抓包发现它连的是127.0.0.1的端口。原因:源码在构造 227 响应时取了本机回环地址,或者取的是多网卡中的第一块网卡地址,这个地址既不是客户端可达的地址,也不是数据 socket 实际监听的地址。解决:227 响应里的 IP 应取数据 socket 本地绑定的地址,用getsockname拿到的才是真正监听的那个 IP,不要用gethostname再解析,后者在多网卡机器上极易翻车。

5.2 坑二:ASCII 模式传输二进制文件,文件字节被“修正”

现象:客户端传一个 zip,目标端文件能打开但解压报错,对比原始文件发现多处0x0A被替换成0x0D 0x0A。原因:客户端默认 TYPE A,服务器也按 ASCII 处理,系统在发送时把换行符展开成 CRLF。对二进制文件而言,任何原始字节0x0A都会被误转,文件长度都变了。解决:客户端在 RETR/STOR 前强制发TYPE I,服务器端OnType里也要维护独立标志,不要用默认值。我后来的习惯是服务器端除目录列表外一律按二进制传输,客户端想传文本就自己处理换行。

5.3 坑三:中文文件名列出乱码且无法下载

现象:客户端列表显示成问号,RETR 时服务器回 550。原因:客户端用系统本地代码页发送命令,服务器却按另一套编码解码;LIST 返回的中文文件名用的又是服务器本地代码页,两边对不上。解决:统一走 UTF-8。客户端发送命令前把文件名从本地编码转成 UTF-8,服务器端收到OPTS UTF8 ON后启用 UTF-8 路径解析,命令解析前先把字节转成宽字符。这份源码如果初始写于较早年代,多半没有这个开关,改造时客户端和服务器两个类要同时改,只改一边等于白改。

5.4 坑四:多个会话共享配置对象不加锁,偶发崩溃

现象:两个客户端同时上传,服务器日志偶发出现两行内容混在一起,过一会儿进程直接退出。原因:配置和日志对象是全局单例,多个会话线程同时写同一个 CString 成员,没有临界区保护。CString 的引用计数不是原子的,两个线程同时赋值会发生内存释放两次。解决:日志写入统一走静态方法,方法内部用CCriticalSection的 Lock 和 Unlock 包住;配置对象要么加锁,要么每个会话创建一份副本。这个坑是网络服务器最常见的崩溃来源,现象随机,排查起来特别费时间,我在一个项目里翻车两小时才定位。

5.5 坑五:CSocket 默认超时设置,长任务被悄悄掐断

现象:下载一个几百兆文件,传到一半连接断开,服务器端日志显示连接已关闭,客户端却还在傻等。原因:CSocket 内部有阻塞超时,在无数据阶段超过设定时间就自动断开,源码里如果没重新设置,长时间传输就会被中断。解决:在网络层初始化时把收发超时调大,比如SetSocketOption(SO_RCVTIMEO)和SO_SNDTIMEO设成 60 到 300 秒,不要设 0,0 表示永不超时,连接异常时线程再也收不回来。大文件传输还可以在循环里记录“最后收到字节的时间”,超过阈值主动报错退出,至少能稳定抛异常而不是挂死。

5.6 坑六:被动模式下高端口被系统防火墙拦截

现象:客户端连接控制端口正常,USER 和 PASS 都通过,但 PASV 之后数据连接一直建立不起来。原因:系统防火墙只放行了 21 端口,被动模式分配的高端口被拦。虽然数据连接是由客户端发起的,但服务器端的入站策略没放行这些随机端口。解决:把服务器被动端口限制在一个固定范围内,比如 50000-50050,然后在防火墙里放行这个段。改造后不仅安全,调试时也能照着端口段去抓包,不用再翻日志猜。

6. 进阶:把这份源码改造成可配置的 FTP 服务与断点续传工具箱

到这一步,源码能跑、坑也清楚,剩下的就是把它用到自己的场景里。我通常做两件改造:把账号和目录下沉到 ini 配置,把被动端口范围固定下来。

6.1 账号、根目录和被动端口写进 ini

直接在代码里改账号密码,每次部署都要重新编译,非常痛苦。把可变化的部分抽到配置里,服务器启动时读一次,每个会话创建时按用户名取配置。一个最小配置长这样:

[Global] ListenPort=21 PassivePortRange=50000-50050 MaxConnections=10 [User:admin] RootDir=D:\FtpRoot Writable=1 [User:anonymous] RootDir=D:\FtpRoot\public Writable=0

服务器启动时用GetPrivateProfileString读ListenPort,升迁为账号后读取对应用户的RootDir和Writable,会话对象构造时传进去。这样新加账号不用动代码,只改 ini 重启服务就生效。被动端口范围也从 ini 读,生成数据端口时在范围内按顺序取,避免随机端口每次不同导致防火墙规则失效。

6.2 上传断点续传:APPE 比 REST+STOR 更稳

下载续传用 REST 已经讲过。上传续传我一般用APPE而不是REST + STOR,因为不是所有服务器都支持 REST 后再 STOR 的偏移,而 APPE 是协议里专门做追加的命令。客户端发APPE name.txt,服务器直接把数据写到文件末尾,本地端只需要把文件指针定位到已发送位置,逐块读出来发过去即可。若要在服务器端实现,就是收到 APPE 时用追加方式打开文件,其余流程与 STOR 完全一致,工作量和风险都小很多。

从那以后,我拿到任何网络通信源代码,都会强制走一遍三查:命令时序是否与服务器状态机一致、数据连接是否由正确的一方关闭、字符串编码是否统一。这次拆包也一样,先把这三条过了再谈功能改造。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询