news 2026/5/22 3:55:08

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现一个高性能 FTP 服务器(C++ / Linux)

目录

  • 一、搭建 TCP 服务器骨架
    • 服务器代码
    • 测试
  • 二、支持多客户端并发
  • 三、线程模型
    • 核心思路
    • 为什么使用 detach
    • 输出为什么会错乱
  • 四、函数重构
    • 重构后的结构
  • 五、FTP 协议基础
    • 控制连接
    • 数据连接
  • 六、命令解析
    • 行缓冲区
    • 命令解析
    • 为什么要转大写
  • 七、PASV 被动模式
    • 为什么需要数据连接?
    • PASV 工作流程
      • 第一步
      • 第二步
    • 动态端口
  • 八、LIST 命令
    • 为什么 LIST 要 accept
    • 遍历目录
    • 安全问题
  • 九、文件下载(RETR)
    • 基础实现
  • 十、文件上传(STOR)
    • 为什么 recv 返回 0 表示结束?
  • 十一、用户身份认证
    • 命令权限控制
  • 十二、路径系统设计
  • 十三、路径规范化 Bug
    • 修复后的思路
    • normalize 的意义
  • 十四、sendfile 零拷贝优化
    • sendfile
    • 优势
      • 1. 更少的 CPU 消耗
      • 2. 更少的数据拷贝
      • 3. 更少的上下文切换
    • 实测
  • 十五、断点续传(REST)
    • Session 状态
    • 下载续传
    • 上传续传
    • 测试结果
  • 十六、线程池优化
  • 十七、线程池模型
    • condition_variable
    • 为什么任务队列需要 mutex?
  • 十八、epoll 高并发模型
    • epoll 的核心优势
      • 1. 事件驱动
      • 2. 红黑树管理 fd
      • 3. 回调机制
  • 十九、epoll 工作流程
    • 创建 epoll
    • 注册 fd
    • 等待事件
    • 非阻塞 IO
  • 二十、总结

一、搭建 TCP 服务器骨架

网络服务器,基本步骤:

  1. socket
  2. bind
  3. listen
  4. accept
  5. recv/send

先实现一个最小 TCP Server

服务器代码

intmain(){intserver_fd,client_fd;structsockaddr_inserver_addr,client_addr;socklen_t client_len=sizeof(client_addr);charbuf[1024];server_fd=socket(AF_INET,SOCK_STREAM,0);if(server_fd==-1){cerr<<"socket failed"<<endl;return1;}intopt=1;setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));server_addr.sin_family=AF_INET;server_addr.sin_addr.s_addr=INADDR_ANY;server_addr.sin_port=htons(2100);if(bind(server_fd,(structsockaddr*)&server_addr,sizeof(server_addr))==-1){cerr<<"bind failed"<<endl;close(server_fd);return1;}if(listen(server_fd,5)==-1){cerr<<"listen failed"<<endl;close(server_fd);return1;}cout<<"listening "<<2100<<"..."<<endl;client_fd=accept(server_fd,(structsockaddr*)&client_addr,&client_len);if(client_fd==-1){cerr<<"accept failed"<<endl;close(server_fd);return1;}constchar*greeting="220 ready\r\n";send(client_fd,greeting,strlen(greeting),0);while(true){memset(buf,0,sizeof(buf));intcount=recv(client_fd,buf,sizeof(buf)-1,0);if(count<=0){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();

为什么使用 detach

thr.detach();

让线程独立运行。

否则:

thr.join();

会阻塞主线程。

这样服务器就无法继续 accept 新客户端。


输出为什么会错乱

多个线程同时 cout:

helloabc123

日志会交叉。

因此需要:

mutex cout_mtx;

配合:

lock_guard<mutex>

保护输出。


四、函数重构

随着代码变多,main 会越来越混乱。

于是把逻辑拆分:

  • CreateServerSocket
  • AcceptClient
  • SendGreeting
  • ReceiveClientData
  • HandleClient
  • RunServer

这样整个结构会非常清晰。

重构后的结构

RunServer ├── CreateServerSocket ├── AcceptClient └── HandleClient ├── SendGreeting └── ReceiveClientData

五、FTP 协议基础

控制连接

负责发送命令:

USER PASS LIST RETR STOR

数据连接

真正传输文件


六、命令解析

FTP 命令格式:

COMMAND arg\r\n

例如:

USER alice\r\n

因此需要:

  1. 解决 TCP 粘包
  2. 按行解析
  3. 提取命令和参数

行缓冲区

string line_buf;

每次 recv 后:

line_buf.append(buf,count);

然后寻找:

\r\n

作为一条完整 FTP 命令。


命令解析

size_t space=cmd.find(' ');string op=cmd.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_port=htons(0);

端口设置为 0。

让系统自动分配。

再通过:

getsockname

获取真实端口。


八、LIST 命令

LIST 的流程:

客户端:PASV 服务器:返回数据端口 客户端:连接数据端口 客户端:LIST 服务器:accept 数据连接 服务器:发送目录内容

为什么 LIST 要 accept

因为 PASV 时:

服务器只是 listen。

真正的数据连接:

需要客户端主动 connect。

因此 LIST 时必须:

accept(data_listen_fd,...)

遍历目录

DIR*dir=opendir(path.c_str());

然后:

readdir(dir)

获取目录项。


安全问题

if(path.find("..")!=string::npos)

防止目录穿越。

否则客户端可能访问:

../../etc/passwd

九、文件下载(RETR)

FTP 下载流程:

PASV RETR filename

服务器:

  1. 打开文件
  2. accept 数据连接
  3. send 文件内容

基础实现

while((n=fread(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

因此需要实现:

  • CWD
  • PWD
  • 路径规范化

十三、路径规范化 Bug

最开始出现的问题:

//wo

导致:

LIST 失败

原因是:

res+="/"+p;

重复拼接了/


修复后的思路

核心思想:

  1. 先拆路径
  2. 再统一拼接

例如:

/wo/test

拆成:

[wo, test]

最后统一生成。


normalize 的意义

它不仅解决:

//

问题。

还统一处理:

. ..

所有路径逻辑统一入口处理。

否则后面会出现大量边界 Bug。


十四、sendfile 零拷贝优化

最开始的下载流程:

磁盘 -> 内核缓冲区 -> 用户缓冲区 -> Socket缓冲区

存在:

  • 用户态 / 内核态切换
  • 多次内存拷贝

sendfile

Linux 提供:

sendfile

实现:

文件 -> Socket

直接在内核完成。

代码:

sendfile(data_client_fd,file_fd,&offset,remaining);

优势

1. 更少的 CPU 消耗

2. 更少的数据拷贝

3. 更少的上下文切换


实测

使用:

ddif=/dev/zeroof=bigfile.binbs=1Mcount=100

生成 100MB 文件。

测试下载:

lftp-p2100127.0.0.1-e"get bigfile.bin; quit"

100MB 文件几乎瞬间完成。


十五、断点续传(REST)

FTP 支持:

REST offset

表示:

从 offset 开始继续传输

Session 状态

新增:

structClientSession{off_t restart_offset=0;};

每个客户端独立维护。


下载续传

核心:

off_t offset=session.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?

因为:

多个线程会同时:

  • push
  • pop

必须加锁。


十八、epoll 高并发模型

epoll 的核心优势

1. 事件驱动

只返回:

真正活跃的 fd

2. 红黑树管理 fd

3. 回调机制

效率极高。


十九、epoll 工作流程

创建 epoll

intepfd=epoll_create1(0);

注册 fd

epoll_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 高并发
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/22 3:55:07

达梦数据库-统计信息收集-记录

达梦数据库-统计信息收集-记录总结 1统计信息收集 统计信息主要是描述数据库中表和索引的大小及数据分布状况等信息。比如&#xff1a;表的行数、块数、平均每行的大小、索引的高度、叶子节点数以及索引字段的行数等。统计信息对于CBO&#xff08;基于代价的优化器&#xff0…

作者头像 李华
网站建设 2026/5/22 3:52:17

LangChain 是什么?从零开始学会 LangChain 的工程实践指南

LangChain 是什么&#xff1f;从零开始学会 LangChain 的工程实践指南 1. 文章背景&#xff1a;为什么这个主题重要 在大模型应用开发中&#xff0c;很多人第一次接触 LangChain&#xff0c;是因为想快速做一个“基于大模型的应用”&#xff1a;例如知识库问答、RAG 检索增强生…

作者头像 李华
网站建设 2026/5/22 3:48:59

VCG Mesh平滑整形

文章目录 一、简介 二、实现代码 三、实现效果 参考资料 一、简介 这里使用拉普拉斯算子来优化Mesh,之前我们写过一篇关于极小曲面的文章,它就是将拉普拉斯算子尽可能都靠近0,以这种目标实现对极小曲面的求解。这里的Mesh平滑整形也是同样的到了,只不过我们并不要求曲率处处…

作者头像 李华
网站建设 2026/5/22 3:48:01

学Simulink——多路输出反激式开关电源(SMPS)交叉调整率改善仿真

目录 手把手教你学Simulink——多路输出反激式开关电源(SMPS)交叉调整率改善仿真 摘要 Abstract 1. 引言 1.1 研究背景 1.2 交叉调整率定义 2. 交叉调整率产生机理 2.1 电路结构 2.2 主要原因 3. Simulink 主电路建模 3.1 参数设置 3.2 关键模块 4. 传统单反馈控…

作者头像 李华
网站建设 2026/5/22 3:42:00

AMDGPU SVM Set Attr 流程分析:XNACK ON vs OFF

AMD工程师的更新频率很快啊,看提交版本应该是先支持XNACK off,然后再支持XACK on, 最后两者合一。下面是lore的最新版本链接: RFC XNACK on/off 统一版 RFC migration v4 版 XNACK-on 本文基于上述版本,进行了设计上的分析,及时跟踪SVM的最新进展。 1. 概述 AMDGPU SVM(…

作者头像 李华
网站建设 2026/5/22 3:39:36

鸿蒙备考题库页面构建:错题本、小组榜单与备考提示模块详解

鸿蒙备考题库页面构建&#xff1a;错题本、小组榜单与备考提示模块详解 前言 在 HarmonyOS 6.0 应用开发中&#xff0c;教育类应用的错题管理、学习排行榜和系统提示是提升用户粘性的关键功能模块。本文将以“备考题库”应用中的“错题本”高频错题列表、“小组榜单”学习排名和…

作者头像 李华