在系统学习计算机网络理论知识后,单纯的理论记忆难以深化理解,工程实践才是巩固知识、提升能力的关键。因此,我选择实现一个云相册项目,将计算机网络中的Socket编程、网络协议设计、并发处理等核心知识点落地,切实加强自身的工程实践能力,实现理论与实操的深度结合。
一、项目背景与意义
本项目核心定位是"基于计算机网络的文件互传与管理系统",俗称"云相册",本质是通过 Socket 通信实现多客户端与服务器之间的文件交互,同时支持基础的文件管理操作。
通过本项目,将计算机网络中的核心理论(如TCP/UDP协议、Socket编程、并发处理、事件驱动、配置管理)转化为可运行的工程代码,解决"理论懂、不会用"的痛点,同时锻炼模块化编程、代码封装、问题排查等工程素养,为后续更复杂的网络项目打下基础。
二、项目需求分析
明确项目核心目标:实现多客户端与服务器之间的文件互传及基础文件管理,模拟简易云相册的核心功能。
核心功能需求
-
多客户端连接:支持多个客户端同时连接服务器,实现并发请求处理,避免单客户端独占资源
-
文件上传:客户端可将本地文件(图片、文本、视频等任意格式)上传至服务器,完成文件的远程存储。
-
文件下载:客户端可从服务器下载已存储的文件,实现文件的跨主机传输。
-
基础文件管理:支持与 bash 类似的文件操作,包括查看服务器端文件列表、删除服务器端文件、重命名服务器端文件,满足基本的文件管理需求。
隐性需求
-
稳定性:保证多客户端并发连接时,服务器不崩溃、文件传输不中断,数据传输准确无误。
-
可扩展性:项目框架设计模块化,便于后续新增功能(如文件分类、权限管理、断点续传等)。
-
可维护性:通过统一配置、规范封装,降低代码维护成本,便于后续排查问题、优化逻辑。
三、项目框架设计
本项目采用模块化设计思想,将不同功能拆分到独立文件中,降低耦合度,同时通过配置文件、公共头文件实现统一管理,整体框架清晰易懂,便于开发和维护。
核心文件说明
- makefile: 自动化编译与链接流程
- my.conf: 统一管理服务器配置(IP、端口、最大连接数等)
- public.h:公共头文件, 存放公共的宏定义、数据结构、函数声明,解决循环依赖问题
- socket.c: 封装所有与 Socket 操作相关的逻辑(创建、绑定、监听、accept 等)
- ser.c: 服务器主入口文件,负责程序启动、初始化和主流程控制
- work_thread.c: 封装工作线程 / 线程池相关的逻辑,处理客户端请求的业务逻辑
- epoll_manage.c: 封装 epoll 事件循环的核心逻辑(创建 epoll 实例、注册事件、事件分发等)
- cmd_process.c:封装和解析客户端传输的命令相关的逻辑
- 自定义通信协议:明确上传、下载、文件管理等操作的请求与响应格式
四、项目实现原则
在实现项目核心上传、下载、文件管理功能之前,需要先搭建一套稳定、可扩展、高并发的底层网络框架,整体包含 TCP 网络通信、epoll I/O 多路复用、工作线程处理、事件分发等核心机制。
开发原则
- 以主流程为起点逐步迭代开发,优先保证基础框架可用,不追求一次性完成全部代码;
- 遵循单一职责思想,一个函数仅负责一项功能,便于调试与复用;
- 采用模块化封装设计,将网络操作、epoll 管理、线程逻辑、业务命令处理进行解耦;
- 开发过程中按需补充功能与结构,根据实际编码中暴露的需求完善代码。
五、项目基础框架搭建
搭建服务端与客户端的基础网络通信框架,实现稳定的基础通信功能。
1、核心架构设计
项目采用 Reactor 模式 + 工作线程 模型,是 Linux 下高并发服务器的标准架构:
- 主线程(ser.c):负责监听、epoll 事件循环、事件分发
- I/O 处理模块(socket.c + epoll_manage.c):封装 Socket 初始化、客户端接入、epoll 事件管理
- 业务处理模块(work_thread.c):将客户端请求交给独立线程处理,不阻塞主线程
- 目标:主线程无阻塞,支持多客户端并发连接
2、模块与实现流程
服务端
my.conf
配置文件,统一存放:
- IP 地址
- 端口号
- 最大监听数
public.h
定义公共结构体,解决参数传递问题:
- NetInfo:绑定 IP、端口、监听队列长度
- WorkThreadInfo:绑定 fd、epfd、event,用于线程参数传递
epoll_manage.c
epoll 事件管理封装:
- EpollFdAdd():将文件描述符加入内核事件表
- EpollFdDel():将文件描述符从内核事件表删除
socket.c
网络基础操作封装:
- ReadConfig():读取配置文件信息
- 打开并读取 my.conf
- 将配置信息赋值给 NetInfo 结构体
- SocketInit():初始化 socket
- 调用 ReadConfig() 获取配置
- 执行 socket→bind→listen
- 返回监听套接字
- GetCLientLink():连接客户端
- 调用 accept 接收客户端连接
- 调用 EpollFdAdd() 将连接套接字加入加入 epoll 监听
work_thread.c
工作线程模块:
-
StartThread():创建工作线程处理客户端事件
-
创建 WorkThreadInfo 结构体,保存 fd、epfd、event
-
调用 pthread_create 创建线程,并将 info 作为参数传递给线程函数 WorkThread()
-
-
WorkThread():工作线程函数
-
强转参数,获取 fd、epfd、event
-
根据事件类型处理:
-
EPOLLRDHUP:客户端断开,调用 CloseOneClient()
-
EPOLLIN:有数据可读,调用 DealClientData()
-
-
-
CloseOneClient():关闭事件
-
关闭文件描述符
-
调用 EpollFdDel() 将对应文件描述符从 epoll 中移除
-
-
DealClientData():处理客户端数据
-
调用 recv 接收客户端消息
-
调用 send 回复客户端
-
ser.c(主程序)
- main():程序主入口
- 调用 SocketInit():获取监听套接字 sockfd
- 创建 epoll 实例,调用 EpollAdd():将 sockfd 加入监听
- 循环监听事件
- epoll_wait 等待事件
- 调用 DealReadyFd() 处理就绪事件
- DealReadyFd():处理就绪事件
- 遍历就绪事件列表
- 若为 sockfd,调用 GetCLientLink() 接收新连接
- 若为客户端 fd,调用 StartThread() 创建线程处理业务
- 遍历就绪事件列表
客户端 cli.c
- main()
- 解析服务端 IP、端口
- 调用 ConnectServer() 连接服务端
- 调用 ChatWIthServer() 和服务端通信
- ConnectServer():连接服务端
- 调用 socket、connect
- 返回套接字
- ChatWIthServer():和服务端通信
- 调用 send 发送消息
- 调用 recv 接收服务端回复
3、问题发现与解决
线程并发
问题现象
客户端连上服务端后,服务端随即立即显示断开连接,连接无法正常建立和维持
问题说明
在 StartThread 函数中,用于传递线程参数的 info 变量是栈区局部资源。当 pthread_create 将其他地址传递给工作线程 WorkThread 后,主线程可能在工作线程尚未读取该变量就退出 StartThread 函数,导致栈帧被销毁、info 变量被释放或覆盖
问题解决
采用信号量同步机制,确保工作线程在安全读取参数后,主线程再释放栈区资源
- 定义全局信号量 sem_t sem
- 程序初始阶段初始化信号量:在 main 函数中调用 SemInit(),将信号量初始值设为0
- 主线程同步等待:在 StartThread 函数中,创建线程后调用 sem_wait(&sem),阻塞主线程,直到工作线程完成参数读取
- 工作线程发送同步信号:在 WorkThread 中,成功读取 info 中的参数后,调用 sem_post(&sem) 通知主线程
多线程并发事件重复触发
问题现象
在 epoll 多线程模型中,单个客户端的断开事件被多个线程重复响应,导致服务端多次打印 断开连接 信息,甚至引发连接状态异常
- 客户端未发送数据直接断开,不会有问题:因为没有使用线程处理连接事件
- 客户端发送数据后再断开时,会触发大量重复日志(洪水现象),影响服务器稳定性

问题说明
epoll 默认采用 LT 水平触发模式,一当文件描述符 fd 上的事件未被处理或处理后未重置状态时,epoll 会持续触发该事件。在多线程环境下,这会导致同一个事件被多个线程捕获并响应,引发重复执行的问题。
问题解决
通过 EPOLLONESHOT 标志,控制一个文件描述符的事件在被触发一次后,自动从 epoll 事件表中移除,避免重复触发。处理流程如下:
- 注册事件时添加 EPOLLONESHOT 标志:在将客户端连接描述符 c 添加到 epoll 实例时,指定 EPOLLIN | EPOLLRDHUP | EPOLLONESHOT 事件
- 处理事件后重置监听:在 DealClientData() 处理完客户端数据后,通过 epoll_ctl 重置该 fd 的事件监听,使其可以再次触发
- 错误分支的特殊处理:若 recv 返回值 <= 0(表示连接断开或出错),必须先关闭连接,再避免调用 epoll_ctl,否则会因 fd 已关闭导致操作失败
注意事项
- EPOLLONESHOT 模式下,事件处理完成后必须重置监听,否则该 fd 将永远不会再次触发事件,导致后续请求无法处理
- 连接断开(EPOLLRDHUP)和数据错误场景,直接调用 CloseOneClient 关闭 fd 并从 epoll 中删除,无需再重置事件
- 确保 DealClientData 的返回值能正确区分 "处理成功" 和 "连接异常",避免错误分支下执行无效的 epoll_ctl 操作
优化效果
单个客户端的断开事件仅被一个线程响应一次,日志不再重复

六、文件基本管理功能
关键词:自定义协议、管道通信
功能说明
基于 Linux「一切皆文件」的特性,实现类 Bash 文件管理命令,即实现客户端对服务器端文件系统的基础管理操作,支持 cd 等内置命令,以及 ls、rm、mkdir 等通用文件操作,为文件上传 / 下载功能提供目录管理支持
- 客户端发送命令 → 服务端执行 → 结果回传客户端
- 采用管道通信实现命令执行结果采集
- 基于自定义协议保证数据传输完整可靠
核心实现原理
1、整体执行流程
- 客户端发送命令字符串
- 服务端调用 CmdAnalysis() 解析命令
- 调用 CmdProcess() 根据命令类型分发处理
- cd:内置命令,之间调用系统函数
- pull/push:自定义文件上传/下载功能
- 其他命令:通过 fork+pipe+exec 执行
- 执行结果按自定义协议发送给客户端
- 客户端接收并展示
2、为什么用 fork+exec+pipe
- 直接在服务端主进程执行命令会替换进程镜像,导致服务器崩溃
- 必须使用子进程执行命令,父进程负责采集结果因此采用父子进程分工
- 管道用于父子进程间单向通信:子进程写、父进程读
3、管道通信要点
- 关闭多余文件描述符:
- 子进程关闭读端,父进程关闭写端。
- 子进程通过 dup2 将 stdout/stderr 重定向到管道
- 循环读取管道:
- 循环读取管道,避免数据截断
- 子进程退出后必须关闭管道写端,否则父进程会阻塞
实现步骤
自定义协议设计
为保证数据传输完整,避免粘包、截断,设计如下协议:
1、普通命令
- 先发送 4 字节长度
- 再发生 命令执行结果内容
- 客户端根据协议先接收长度,再解释内容
2、错误协议
- 文件为空:file error1
- 文件打开失败:file error2
协议设计目的
先发送数据长度,是为了防止一次接收时数据无法被客户端全部接收,以及循环接收时,让客户端知晓什么时候接收完毕
其他可选方法
- 最后发生特殊标志告知客户端发送完毕
- 将客户端文件描述符设置为非阻塞模式,等待服务器发送数据
各个方案都有利有弊,采取后会出现新的问题需要解决
服务端
work_thread.c
- DealClientData()
- recv 接收客户信息
- 调用 CmdAnalysis() 解析客户端命令
- 调用 CmdProcess() 处理命令
cd_process.c
- CmdAnalysis():解析命令
- 使用 strtok_r(线程安全版) 对命令解析切割
- CmdProcess():处理命令
- 使用 strncmp 区分不同的命令,进行不同的处理
- cd命令:调用 CdCommand()
- pull、push:下一部分实现
- 其他命令:调用 OtherCommand()
- 使用 strncmp 区分不同的命令,进行不同的处理
- CdCommand():cd命令实现
- 调用 chdir 切换路径
- 给客户端发送对应消息
- 先发送长度
- 再发送内容
- OtherCommand()
- 创建无名管道
- fork() 创建子进程执行命令
- 子进程关闭管道读端
- 使用 dup2 将标准输出、标准错误重定向到管道写端
- 关闭管道写端
- 调用 execvp 执行指定命令
- 父进程处理结果
- 关闭管道写端
- 等待子进程执行结束
- 调用 PipeSaveToFile() 将管道内容保存到文件
- 关闭管道读端
- 调用 SendMsg() 给客户端发送消息
- PipeSaveToFile()
- 创建/打开一个临时文件 temp.txt
- 读取管道内容,并写入临时文件
- 关闭临时文件
- SendMsg()
- 打开临时文件 temp.txt
- 先向客户端发送文件大小
- 再发送文件内容
- 关闭临时文件
- 删除临时文件
路径设置
为防止错误操作原代码路径文件,在 main() 最开始通过 chdir 修改程序执行路径
客户端
cli.c
- ChatWithServer()
- 调用 fgets 获取用户输入
- 调用 strncmp 区分不同的命令,进行不同的处理(将命令划分为pull、push、其他命令)
- 其他命令
- 调用 RecvServerMsg() 接收服务端消息
- 先接收 4 字节数据长度
- 再循环接收数据,并进行打印
- 调用 RecvServerMsg() 接收服务端消息
七、下载和上传功能
功能说明
基于自定义协议,实现客户端与服务器之间的文件互传功能:
- pull 下载:客户端从服务器获取文件,支持文件大小校验与断点续传准备
- push 上传:客户端将本地文件发送至服务器,支持服务器路径安全控制
- 整个流程基于 "请求 - 确认 - 传输" 的三步式协议,避免粘包与数据截断问题,保障文件传输的完整性与可靠性。
实现步骤
1、实现 pull 下载功能
确认协议
在实现下载功能之前,需要约定一套完整的通信协议,规范客户端与服务器的交互流程
协议包括:客户端如何发送下载命令,服务端如何回复,客户端如何再次回复保证通信和双方接受能力的正常
- 客户端发送下载命令格式:pull 文件名
- 服务器回复:
- 正常回复:OK#文件大小(#用来分隔两部分内容)
- 错误回复:file error 1或file error 2(此处需要规定不同错误的原因)
- 客户端二次回复:
- 正常回复:OK
- 错误回复:file error 1或file error 2

思考
思考1:可以省略5、6步(客户端的二次确认)吗
不建议省略。如果客户端发送下载请求后反悔,没有确认步骤,服务器会直接发送数据,导致客户端被迫接收。因此,服务器发送 OK#size 后,需要客户端回复确认,再开始传输。
思考2:客户端何如何判断数据发送完成?
服务器从文件读取到末尾就知道传输结束,但客户端无法感知。因此,协议约定服务器先发送文件大小,客户端按大小接收,当接收字节数等于文件大小时,判定传输完成。
思考3:服务器回复 OK 时,能否不等客户端确认直接发数据?
不行,会产生粘包问题。OK#size 刚发送出去就发文件内容,客户端无法区分 size 和后续数据,必须通过客户端的 OK 回复来分割两部分数据。

代码实现思路
该功能需要客户端和服务器端配合实现:
服务端
- 检查文件合法性:
- 若命令无文件名,回复 file error1
- 若文件不存在 / 打开失败,回复 file error2
- 若回复错误,直接结束下载流程,无需等待客户端确认
- 等待客户端确认
- 若客户端回复 OK,继续后续传输
- 若回复错误,结束下载流程
- 发送文件内容
- 打开文件,读取数据并发送给客户端
- 传输完成后关闭文件
客户端
- 接收服务端的回复
- 若收到 file error1 / file error2,直接结束下载
- 若收到 OK#size,解析文件大小,准备接收
- 二次回复确认
- 检查本地文件是否存在,若存在可回复 "已存在" 结束流程
- 若不存在,回复 OK#,告知服务器可以开始传输
- 接收并写入文件
- 按文件大小循环接收数据,写入本地文件
- 接收字节数达到文件大小时,结束下载
2、实现 push 上传功能
push 是 pull 下载的逆实现:客户端将本地指定文件上传到服务端的执行路径中,核心流程与下载对称,同样基于自定义协议实现一问一答的交互
确认协议
在实现上传功能前,先约定一套完整的通信协议,规范客户端与服务端的交互流程:
- 客户端发送上传目录:push 文件名
- 服务端回复:
- 正常回复:OK(确定可以接收文件)
- 错误回答:file error 1 / file error 2(分别对应文件名无效、文件打开失败)
- 客户端二次回复:发送文件大小
- 服务器二次回复:OK(确认可以开始接收文件内容)
- 客户端发送文件内容,传输完成后结束流程
代码实现思路
客户端 cli.c
- PushFileToServer()
- 命令解析与文件检查
- 解析 push 文件名 命令,提取目标文件名
- 打开本地文件,若打开失败则直接退出
- 向服务端发送 push 文件名 命令
- 服务端回复处理
- 接收服务端回复,若为错误信息则关闭文件并退出
- 收到 OK 回复后,继续上传流程
- 文件大小同步
- 获取文件大小,发送给服务端
- 等待服务端回复 OK,确认可以发送文件内容
- 文件数据发送
- 循环读取文件内容,通过 send 发送给服务端
- 当已发送字节数等于文件大小时,结束发送,关闭文件
- 命令解析与文件检查
服务端 cmd_process.c
- GetFileFromClient()
- 文件创建与合法性校验
- 检查文件名是否有效
- 创建 / 打开目标文件(O_WRONLY | O_CREAT),若打开失败则回复错误信息
- 回复 OK,确认可以接收文件
- 文件大小接收与确认
- 接收客户端发送的文件大小
- 回复 OK,告知客户端可以开始发送文件内容
- 文件数据接收与写入
- 循环接收客户端发送的文件数据,写入目标文件
- 当已接收字节数等于文件大小时,结束接收,关闭文件
- 文件创建与合法性校验
关键设计要点
- 二次确认机制:服务端收到上传命令后,必须回复 OK,客户端才能发送文件内容,避免服务端未准备好就接收数据;
- 数据粘包避免:通过两次 send/recv 交互(文件大小确认),将 "大小同步" 和 "文件数据" 分离,避免粘包问题;
- 传输完成判定:客户端按文件大小循环发送,服务端按文件大小循环接收,确保文件完整传输。
3、问题发现与解决
a、客户端接收命令结果异常


现象
- 除 pwd 命令,其余命令测试结果都不合理
- 服务端终端没有日志信息
- cd切换目录后,pwd 显示路径非预期结果
原因
客户端 RecvServerMsg() 函数中,recv 调用参数错误:
- 错误写法:recv(sockfd, &total, 8, 0);(错误接收 8 字节,超出实际数据长度)
- 正确写法:recv(sockfd, &msgSize, 4, 0);(按协议约定接收 4 字节长度信息)
错误的 recv 调用导致后续数据读取错位,所有命令结果解析异常。
解决
修正 recv 参数,按协议接收 4 字节数据长度:
cpp
void RecvServerMsg(int sockfd) {
int msgSize;
int total = recv(sockfd, &msgSize, 4, 0);
if (total == 0) {
printf("server reply is empty\n");
return;
}
printf("total: %d\n", total);
printf("msgSize: %d\n", msgSize);
// 后续按msgSize接收数据
}
b、ls 命令无结果
现象
执行 ls 命令时,客户端无服务端回复, msgSize 为0

原因
服务端启动时已通过 chdir 将工作路径切换至 /home/wuya/qzj/project,该目录下无文件,ls 命令无输出。
解决
验证目录下文件,或在目录中添加测试文件,确保命令执行有输出。

c、pull 命令错误(文件打开失败)
现象
- 服务端尝试打开文件失败,输出 file error2,但客户端未收到错误信息
- 客户端执行 pull a.txt 后无回复,直接结束


原因
- 客户端 strncmp 匹配错误:客户端判断服务端回复时,strncmp 长度设置错误,导致无法识别 FILE_ERR1/FILE_ERR2 错误信息
- 路径问题:服务端工作路径 /home/wuya/qzj/project 下无 a.txt 文件,文件打开失败
解决
-
修正客户端错误信息匹配逻辑:
*cppif(strncmp(buff, "FILE_ERR1", sizeof("FILE_ERR1")) == 0 || strncmp(buff, "FILE_ERR2", sizeof("FILE_ERR2")) == 0) { printf("server reply: %s\n", buff); return; } -
确保服务端工作路径下存在目标文件,或调整文件路径。
d、push 命令执行时服务端回复 file error1
现象
客户端输入 push a.txt 时,服务端回复 file error1,文件上传失败
排除过程
- 通过打印调试发现,客户端发送给服务端的消息只有 push,丢失了后面的文件名
- 进一步定位发现:客户端在发送前调用 strtok_r 切割 userMsg 以获取文件名,该操作修改了原始 userMsg 字符串,导致后续 send 发送的内容被截断,只发送了 push
解决
在对 userMsg 进行 strtok_r 切割前,先保存一份原始字符串的副本,发送时使用未被修改的副本,确保完整命令被发送到服务端
结果
push 命令可正常发送完整命令,服务端成功接收文件名,文件上传流程正常


e、文件存在但服务端提示打开失败(file error2)
现象
服务端工作路径 /home/wuya/qzj/project 下文件 a.txt 存在,但执行 pull 命令时,服务端提示 file error2,文件打开失败

排除过程
- 使用 perror("open failed") 打印错误信息,显示 No such file or directory。
- 路径分析发现:
- push 命令:客户端从当前执行路径找文件,上传到服务端的 /home/wuya/qzj/project 路径。
- pull 命令:服务端从 /home/wuya/qzj/project 路径找文件,下载到客户端的当前执行路径。
- 本次问题根源是对程序执行路径混淆,误以为 push 会将文件上传到客户端路径,实际文件已上传到服务端工作路径,客户端执行 pull 时因路径理解错误导致文件找不到
解决
明确服务端工作路径与客户端执行路径的差异,在 pull 命令中使用文件的完整路径,确保服务端能正确找到文件
结果
文件可正常打开,pull 命令能成功从服务端下载文件到客户端

f、ls 命令输出异常,包含文件内容且文件消失
现象
执行 ls 命令时,输出不仅包含文件列表,还混入了文件内容;执行 ls 后,服务端工作路径下的文件消失


排除过程
- 定位 OtherCommand() 函数中的 PipeSaveToFile(),发现该函数将管道命令的输出固定写入 a.txt 文件,而不是临时文件。
- 后续 SendMsg() 读取并发送该文件内容后,会执行 remove("./a.txt") 删除文件,导致 ls 命令输出文件内容,且真实文件被误删
解决
将命令执行结果的临时存储文件改为 tmp.txt,避免与用户上传 / 下载的文件重名,传输完成后删除 tmp.txt,不影响用户文件
结果
ls 命令仅输出文件列表,无额外内容,用户文件不再被误删

g、push 命令传输大文件时服务端输出乱码
现象
客户端上传 test.jpg 时,服务端打印大量乱码数据,文件传输失败
服务端

客户端

排除过程
定位到服务端接收文件大小的代码:atoi(buff + 3),代码假设客户端发送的文件大小前带有固定前缀,但客户端直接发送的是纯数字的文件大小,导致解析出的文件大小错误,后续数据接收错位
解决
修改服务端接收逻辑,直接对 recv 到的 buff 调用 atoi 解析文件大小,去掉多余的 +3 偏移
结果
大文件传输正常,服务端可正确解析文件大小,文件接收完整,无乱码问题


h、临时文件 tmp.txt 残留,影响后续命令执行
现象
ls 命令输出中频繁出现 tmp.txt,影响文件列表展示

排除过程
部分命令执行异常退出时,tmp.txt 文件未被删除,残留于服务端工作路径中
解决
在 PipeSaveToFile() 和 SendMsg() 中增加异常处理,确保无论命令执行是否成功,都能关闭文件并删除 tmp.txt
结果
ls 命令输出中不再出现残留的 tmp.txt,文件列表干净准确
八、功能扩展
1、断点续传
关键词:自定义协议扩展、偏移量同步、断点恢复
功能说明
文件传输过程中(上传 / 下载)可能因网络波动、程序中断等原因导致传输终止,此时文件仅完成部分传输。若重新从头传输会造成效率浪费,因此本项目扩展实现了断点续传功能,支持从上次中断的位置继续传输,大幅提升大文件传输的效率和稳定性。
核心协议设计
协议扩展逻辑

核心实现思路
- 客户端本地检查目标文件,计算已下载字节数(downsize),并将其随 OK 消息发送给服务端
- 服务端解析偏移量,通过 lseek 将文件指针定位到指定位置,从该位置开始发送数据
- 客户端以追加模式打开文件,通过 lseek 将文件指针定位到已下载位置,继续写入数据,实现断点恢复
完整实现流程
服务端
- 合法性检验:判断文件名是否有效、文件是否存在,不存在则恢复错误信息
- 基础信息回复:获取文件大小,向客户端发送 OK#文件总大小
- 客户端反馈处理:接收客户端的 OK#偏移量 或 file already exists 消息
- 收到 "文件已存在",直接关闭文件并结束传输
- 若收到偏移量,通过 lseek 将文件指针定位到指定位置
- 断点续传数据发送:从偏移位置开始,循环读取文件并发送数据,直至文件发送完毕
客户端
- 服务端响应解析:接收服务端的 OK#文件总大小 或错误信息,错误则直接退出
- 本地文件检查:尝试打开目标文件,若存在则通过 stat 获取文件大小作为 downsize
- 文件完整性判断:若 downsize 与服务端返回的文件总大小相等,说明文件已完整存在,向服务端发送 file already exists 消息并退出
- 断点续传准备:以追加模式打开文件,通过 lseek 将文件指针定位到 downsize 位置,向服务端发送 OK#downsize 消息
- 断点数据接收:循环接收服务端数据并写入文件,同时通过 printf + fflush 实时打印下载进度,直至接收完毕
关键细节与问题修复
问题 1:客户端文件已存在时,服务端一直阻塞等待
- 现象:客户端发现文件已完整存在后直接退出,未向服务端发送反馈,导致服务端 recv 一直阻塞
- 原因:服务端 recv 未收到客户端消息,会一直等待,直到客户端断开连接才会返回错误
- 修复:客户端文件已存在时,必须向服务端发送 file already exists 消息,服务端收到后主动关闭文件并结束传输
问题 2:传输进度打印不更新、卡死
- 现象:进度条显示不变,或添加 sleep(1) 后程序反应缓慢
- 原因:printf("\r...") 不会自动刷新标准输出缓冲区,进度信息被缓存未及时显示
- 修复:在进度打印后添加 fflush(stdout),强制刷新缓冲区,确保进度实时更新
其他关键细节
- 服务端发送数据时,必须从 lseek 后的偏移位置读取文件,避免重复发送已传输数据
- 客户端接收数据时,需限制单次 recv 的最大字节数,避免读取到后续无关数据
- 循环接收数据时,需以「已接收字节数 ≥ 剩余待接收字节数」作为退出条件,确保文件完整接收
运行效果


优化方向
当前实现基于文件大小判断断点位置,更严谨的方案可通过 MD5 文件校验 验证文件完整性:客户端本地文件计算 MD5,与服务端文件的 MD5 对比,确保断点位置的准确性,避免文件损坏导致的传输错误