从零实现一个高性能 FTP 服务器(C++ / Linux)

发布时间:2026/5/22 3:55:19

从零实现一个高性能 FTP 服务器(C++ / Linux) 目录一、搭建 TCP 服务器骨架服务器代码测试二、支持多客户端并发三、线程模型核心思路为什么使用 detach输出为什么会错乱四、函数重构重构后的结构五、FTP 协议基础控制连接数据连接六、命令解析行缓冲区命令解析为什么要转大写七、PASV 被动模式为什么需要数据连接PASV 工作流程第一步第二步动态端口八、LIST 命令为什么 LIST 要 accept遍历目录安全问题九、文件下载RETR基础实现十、文件上传STOR为什么 recv 返回 0 表示结束十一、用户身份认证命令权限控制十二、路径系统设计十三、路径规范化 Bug修复后的思路normalize 的意义十四、sendfile 零拷贝优化sendfile优势1. 更少的 CPU 消耗2. 更少的数据拷贝3. 更少的上下文切换实测十五、断点续传RESTSession 状态下载续传上传续传测试结果十六、线程池优化十七、线程池模型condition_variable为什么任务队列需要 mutex十八、epoll 高并发模型epoll 的核心优势1. 事件驱动2. 红黑树管理 fd3. 回调机制十九、epoll 工作流程创建 epoll注册 fd等待事件非阻塞 IO二十、总结一、搭建 TCP 服务器骨架网络服务器基本步骤socketbindlistenacceptrecv/send先实现一个最小 TCP Server服务器代码intmain(){intserver_fd,client_fd;structsockaddr_inserver_addr,client_addr;socklen_t client_lensizeof(client_addr);charbuf[1024];server_fdsocket(AF_INET,SOCK_STREAM,0);if(server_fd-1){cerrsocket failedendl;return1;}intopt1;setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,opt,sizeof(opt));server_addr.sin_familyAF_INET;server_addr.sin_addr.s_addrINADDR_ANY;server_addr.sin_porthtons(2100);if(bind(server_fd,(structsockaddr*)server_addr,sizeof(server_addr))-1){cerrbind failedendl;close(server_fd);return1;}if(listen(server_fd,5)-1){cerrlisten failedendl;close(server_fd);return1;}coutlistening 2100...endl;client_fdaccept(server_fd,(structsockaddr*)client_addr,client_len);if(client_fd-1){cerraccept failedendl;close(server_fd);return1;}constchar*greeting220 ready\r\n;send(client_fd,greeting,strlen(greeting),0);while(true){memset(buf,0,sizeof(buf));intcountrecv(client_fd,buf,sizeof(buf)-1,0);if(count0){break;}buf[count]\0;cout[Received] : buf;}close(client_fd);close(server_fd);}测试服务端./server客户端telnet127.0.0.12100输入hello服务器即可收到数据二、支持多客户端并发当前服务器只能同时处理一个客户端因为accept-recv-recv-recv会一直阻塞。所以需要并发处理。最简单的方法主线程 accept每个客户端一个线程三、线程模型核心思路主线程 accept 新连接 工作线程 专门处理客户端代码threadthr(HandleClient,client_fd,client_ip,client_port);thr.detach();为什么使用 detachthr.detach();让线程独立运行。否则thr.join();会阻塞主线程。这样服务器就无法继续 accept 新客户端。输出为什么会错乱多个线程同时 couthelloabc123日志会交叉。因此需要mutex cout_mtx;配合lock_guardmutex保护输出。四、函数重构随着代码变多main 会越来越混乱。于是把逻辑拆分CreateServerSocketAcceptClientSendGreetingReceiveClientDataHandleClientRunServer这样整个结构会非常清晰。重构后的结构RunServer ├── CreateServerSocket ├── AcceptClient └── HandleClient ├── SendGreeting └── ReceiveClientData五、FTP 协议基础控制连接负责发送命令USER PASS LIST RETR STOR数据连接真正传输文件六、命令解析FTP 命令格式COMMAND arg\r\n例如USER alice\r\n因此需要解决 TCP 粘包按行解析提取命令和参数行缓冲区string line_buf;每次 recv 后line_buf.append(buf,count);然后寻找\r\n作为一条完整 FTP 命令。命令解析size_t spacecmd.find( );string opcmd.substr(0,space);string arg...例如USER alice解析后op USER arg alice为什么要转大写FTP 命令大小写不敏感。因此transform(op.begin(),op.end(),op.begin(),::toupper);统一转换。七、PASV 被动模式为什么需要数据连接FTP 协议规定控制连接发送命令数据连接传文件LIST / RETR / STOR 都必须走数据连接。PASV 工作流程第一步客户端发送PASV第二步服务器创建新的监听 socket随机绑定端口返回端口号例如227 Entering Passive Mode (127,0,0,1,19,136)其中port 19 * 256 136动态端口data_addr.sin_porthtons(0);端口设置为 0。让系统自动分配。再通过getsockname获取真实端口。八、LIST 命令LIST 的流程客户端PASV 服务器返回数据端口 客户端连接数据端口 客户端LIST 服务器accept 数据连接 服务器发送目录内容为什么 LIST 要 accept因为 PASV 时服务器只是 listen。真正的数据连接需要客户端主动 connect。因此 LIST 时必须accept(data_listen_fd,...)遍历目录DIR*diropendir(path.c_str());然后readdir(dir)获取目录项。安全问题if(path.find(..)!string::npos)防止目录穿越。否则客户端可能访问../../etc/passwd九、文件下载RETRFTP 下载流程PASV RETR filename服务器打开文件accept 数据连接send 文件内容基础实现while((nfread(buf,1,sizeof(buf),fp))0){send(data_client_fd,buf,n,0);}磁盘 - 用户态 - 内核态 - Socket会发生多次数据拷贝。后面会优化。十、文件上传STOR上传流程PASV STOR filename服务器recv-fwrite不断接收客户端数据。为什么 recv 返回 0 表示结束因为客户端关闭了数据连接。FTP 文件传输结束本质上就是关闭 data socket十一、用户身份认证FTP 标准流程USER xxx PASS xxx引入boollogged_in和string username命令权限控制在 LIST / RETR / STOR 前增加if(!logged_in)否则530 Please login with USER and PASS.十二、路径系统设计FTP 服务器需要维护当前工作目录例如/ /wo /wo/test因此需要实现CWDPWD路径规范化十三、路径规范化 Bug最开始出现的问题//wo导致LIST 失败原因是res/p;重复拼接了/。修复后的思路核心思想先拆路径再统一拼接例如/wo/test拆成[wo, test]最后统一生成。normalize 的意义它不仅解决//问题。还统一处理. ..所有路径逻辑统一入口处理。否则后面会出现大量边界 Bug。十四、sendfile 零拷贝优化最开始的下载流程磁盘 - 内核缓冲区 - 用户缓冲区 - Socket缓冲区存在用户态 / 内核态切换多次内存拷贝sendfileLinux 提供sendfile实现文件 - Socket直接在内核完成。代码sendfile(data_client_fd,file_fd,offset,remaining);优势1. 更少的 CPU 消耗2. 更少的数据拷贝3. 更少的上下文切换实测使用ddif/dev/zeroofbigfile.binbs1Mcount100生成 100MB 文件。测试下载lftp-p2100127.0.0.1-eget bigfile.bin; quit100MB 文件几乎瞬间完成。十五、断点续传RESTFTP 支持REST offset表示从 offset 开始继续传输Session 状态新增structClientSession{off_t restart_offset0;};每个客户端独立维护。下载续传核心off_t offsetsession.restart_offset;然后sendfile(...,offset,...)sendfile 会自动更新 offset。上传续传上传稍微复杂。需要fseek(fp,session.restart_offset,SEEK_SET);把文件指针移动到断点位置。测试结果中断上传后重新上传350 Restarting at xxx然后继续传输。最后 md5 校验一致。说明断点续传正确。十六、线程池优化前面的模型一个客户端 - 一个线程问题线程创建开销很大。高并发时线程数量暴涨上下文切换严重CPU 被调度拖死十七、线程池模型核心思想预先创建线程任务来了放入任务队列工作线程不断消费任务。condition_variable这里的关键cv.wait(lock,...)线程没有任务时睡眠避免空转浪费 CPU。为什么任务队列需要 mutex因为多个线程会同时pushpop必须加锁。十八、epoll 高并发模型epoll 的核心优势1. 事件驱动只返回真正活跃的 fd2. 红黑树管理 fd3. 回调机制效率极高。十九、epoll 工作流程创建 epollintepfdepoll_create1(0);注册 fdepoll_ctl(epfd,EPOLL_CTL_ADD,fd,ev);等待事件epoll_wait(epfd,events,max_events,-1);非阻塞 IO配合fcntl(fd,F_SETFL,O_NONBLOCK);真正实现单线程高并发二十、总结整个 FTP Server 的演进过程TCP Server ↓ 多线程并发 ↓ FTP 命令解析 ↓ PASV 数据连接 ↓ LIST / RETR / STOR ↓ 路径系统 ↓ 身份认证 ↓ sendfile 零拷贝 ↓ 断点续传 ↓ 线程池 ↓ epoll 高并发

相关新闻