计算机网络实践项目 | 云相册(文件互传与管理系统)

在系统学习计算机网络理论知识后,单纯的理论记忆难以深化理解,工程实践才是巩固知识、提升能力的关键。因此,我选择实现一个云相册项目,将计算机网络中的Socket编程、网络协议设计、并发处理等核心知识点落地,切实加强自身的工程实践能力,实现理论与实操的深度结合。


一、项目背景与意义

本项目核心定位是"基于计算机网络的文件互传与管理系统",俗称"云相册",本质是通过 Socket 通信实现多客户端与服务器之间的文件交互,同时支持基础的文件管理操作。

通过本项目,将计算机网络中的核心理论(如TCP/UDP协议、Socket编程、并发处理、事件驱动、配置管理)转化为可运行的工程代码,解决"理论懂、不会用"的痛点,同时锻炼模块化编程、代码封装、问题排查等工程素养,为后续更复杂的网络项目打下基础。


二、项目需求分析

明确项目核心目标:实现多客户端与服务器之间的文件互传及基础文件管理,模拟简易云相册的核心功能。

核心功能需求

  • 多客户端连接:支持多个客户端同时连接服务器,实现并发请求处理,避免单客户端独占资源

  • 文件上传:客户端可将本地文件(图片、文本、视频等任意格式)上传至服务器,完成文件的远程存储。

  • 文件下载:客户端可从服务器下载已存储的文件,实现文件的跨主机传输。

  • 基础文件管理:支持与 bash 类似的文件操作,包括查看服务器端文件列表、删除服务器端文件、重命名服务器端文件,满足基本的文件管理需求。

隐性需求

  • 稳定性:保证多客户端并发连接时,服务器不崩溃、文件传输不中断,数据传输准确无误。

  • 可扩展性:项目框架设计模块化,便于后续新增功能(如文件分类、权限管理、断点续传等)。

  • 可维护性:通过统一配置、规范封装,降低代码维护成本,便于后续排查问题、优化逻辑。


三、项目框架设计

本项目采用模块化设计思想,将不同功能拆分到独立文件中,降低耦合度,同时通过配置文件、公共头文件实现统一管理,整体框架清晰易懂,便于开发和维护。

核心文件说明

  1. makefile: 自动化编译与链接流程
  2. my.conf: 统一管理服务器配置(IP、端口、最大连接数等)
  3. public.h:公共头文件, 存放公共的宏定义、数据结构、函数声明,解决循环依赖问题
  4. socket.c: 封装所有与 Socket 操作相关的逻辑(创建、绑定、监听、accept 等)
  5. ser.c: 服务器主入口文件,负责程序启动、初始化和主流程控制
  6. work_thread.c: 封装工作线程 / 线程池相关的逻辑,处理客户端请求的业务逻辑
  7. epoll_manage.c: 封装 epoll 事件循环的核心逻辑(创建 epoll 实例、注册事件、事件分发等)
  8. cmd_process.c:封装和解析客户端传输的命令相关的逻辑
  9. 自定义通信协议:明确上传、下载、文件管理等操作的请求与响应格式

四、项目实现原则

在实现项目核心上传、下载、文件管理功能之前,需要先搭建一套稳定、可扩展、高并发的底层网络框架,整体包含 TCP 网络通信、epoll I/O 多路复用、工作线程处理、事件分发等核心机制。

开发原则

  1. 以主流程为起点逐步迭代开发,优先保证基础框架可用,不追求一次性完成全部代码;
  2. 遵循单一职责思想,一个函数仅负责一项功能,便于调试与复用;
  3. 采用模块化封装设计,将网络操作、epoll 管理、线程逻辑、业务命令处理进行解耦;
  4. 开发过程中按需补充功能与结构,根据实际编码中暴露的需求完善代码

五、项目基础框架搭建

搭建服务端与客户端的基础网络通信框架,实现稳定的基础通信功能。

1、核心架构设计

项目采用 Reactor 模式 + 工作线程 模型,是 Linux 下高并发服务器的标准架构:

  1. 主线程(ser.c):负责监听、epoll 事件循环、事件分发
  2. I/O 处理模块(socket.c + epoll_manage.c):封装 Socket 初始化、客户端接入、epoll 事件管理
  3. 业务处理模块(work_thread.c):将客户端请求交给独立线程处理,不阻塞主线程
  4. 目标:主线程无阻塞,支持多客户端并发连接

2、模块与实现流程

服务端

my.conf

配置文件,统一存放:

  • IP 地址
  • 端口号
  • 最大监听数
public.h

定义公共结构体,解决参数传递问题:

  1. NetInfo:绑定 IP、端口、监听队列长度
  2. WorkThreadInfo:绑定 fd、epfd、event,用于线程参数传递
epoll_manage.c

epoll 事件管理封装:

  1. EpollFdAdd():将文件描述符加入内核事件表
  2. EpollFdDel():将文件描述符从内核事件表删除
socket.c

网络基础操作封装:

  1. ReadConfig():读取配置文件信息
    1. 打开并读取 my.conf
    2. 将配置信息赋值给 NetInfo 结构体
  2. SocketInit():初始化 socket
    1. 调用 ReadConfig() 获取配置
    2. 执行 socket→bind→listen
    3. 返回监听套接字
  3. GetCLientLink():连接客户端
    1. 调用 accept 接收客户端连接
    2. 调用 EpollFdAdd() 将连接套接字加入加入 epoll 监听
work_thread.c

工作线程模块:

  1. StartThread():创建工作线程处理客户端事件

    1. 创建 WorkThreadInfo 结构体,保存 fd、epfd、event

    2. 调用 pthread_create 创建线程,并将 info 作为参数传递给线程函数 WorkThread()

  2. WorkThread():工作线程函数

    1. 强转参数,获取 fd、epfd、event

    2. 根据事件类型处理:

      1. EPOLLRDHUP:客户端断开,调用 CloseOneClient()

      2. EPOLLIN:有数据可读,调用 DealClientData()

  3. CloseOneClient():关闭事件

    1. 关闭文件描述符

    2. 调用 EpollFdDel() 将对应文件描述符从 epoll 中移除

  4. DealClientData():处理客户端数据

    1. 调用 recv 接收客户端消息

    2. 调用 send 回复客户端

ser.c(主程序)
  1. main():程序主入口
    1. 调用 SocketInit():获取监听套接字 sockfd
    2. 创建 epoll 实例,调用 EpollAdd():将 sockfd 加入监听
    3. 循环监听事件
      1. epoll_wait 等待事件
      2. 调用 DealReadyFd() 处理就绪事件
  2. DealReadyFd():处理就绪事件
    1. 遍历就绪事件列表
      1. 若为 sockfd,调用 GetCLientLink() 接收新连接
      2. 若为客户端 fd,调用 StartThread() 创建线程处理业务

客户端 cli.c

  1. main()
    1. 解析服务端 IP、端口
    2. 调用 ConnectServer() 连接服务端
    3. 调用 ChatWIthServer() 和服务端通信
  2. ConnectServer():连接服务端
    1. 调用 socket、connect
    2. 返回套接字
  3. ChatWIthServer():和服务端通信
    1. 调用 send 发送消息
    2. 调用 recv 接收服务端回复

3、问题发现与解决

线程并发

问题现象

客户端连上服务端后,服务端随即立即显示断开连接,连接无法正常建立和维持

问题说明

在 StartThread 函数中,用于传递线程参数的 info 变量是栈区局部资源。当 pthread_create 将其他地址传递给工作线程 WorkThread 后,主线程可能在工作线程尚未读取该变量就退出 StartThread 函数,导致栈帧被销毁、info 变量被释放或覆盖

问题解决

采用信号量同步机制,确保工作线程在安全读取参数后,主线程再释放栈区资源

  1. 定义全局信号量 sem_t sem
  2. 程序初始阶段初始化信号量:在 main 函数中调用 SemInit(),将信号量初始值设为0
  3. 主线程同步等待:在 StartThread 函数中,创建线程后调用 sem_wait(&sem),阻塞主线程,直到工作线程完成参数读取
  4. 工作线程发送同步信号:在 WorkThread 中,成功读取 info 中的参数后,调用 sem_post(&sem) 通知主线程

多线程并发事件重复触发

问题现象

在 epoll 多线程模型中,单个客户端的断开事件被多个线程重复响应,导致服务端多次打印 断开连接 信息,甚至引发连接状态异常

  • 客户端未发送数据直接断开,不会有问题:因为没有使用线程处理连接事件
  • 客户端发送数据后再断开时,会触发大量重复日志(洪水现象),影响服务器稳定性

问题说明

epoll 默认采用 LT 水平触发模式,一当文件描述符 fd 上的事件未被处理或处理后未重置状态时,epoll 会持续触发该事件。在多线程环境下,这会导致同一个事件被多个线程捕获并响应,引发重复执行的问题。

问题解决

通过 EPOLLONESHOT 标志,控制一个文件描述符的事件在被触发一次后,自动从 epoll 事件表中移除,避免重复触发。处理流程如下:

  1. 注册事件时添加 EPOLLONESHOT 标志:在将客户端连接描述符 c 添加到 epoll 实例时,指定 EPOLLIN | EPOLLRDHUP | EPOLLONESHOT 事件
  2. 处理事件后重置监听:在 DealClientData() 处理完客户端数据后,通过 epoll_ctl 重置该 fd 的事件监听,使其可以再次触发
  3. 错误分支的特殊处理:若 recv 返回值 <= 0(表示连接断开或出错),必须先关闭连接,再避免调用 epoll_ctl,否则会因 fd 已关闭导致操作失败

注意事项

  1. EPOLLONESHOT 模式下,事件处理完成后必须重置监听,否则该 fd 将永远不会再次触发事件,导致后续请求无法处理
  2. 连接断开(EPOLLRDHUP)和数据错误场景,直接调用 CloseOneClient 关闭 fd 并从 epoll 中删除,无需再重置事件
  3. 确保 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、普通命令

  1. 先发送 4 字节长度
  2. 再发生 命令执行结果内容
  3. 客户端根据协议先接收长度,再解释内容

2、错误协议

  1. 文件为空:file error1
  2. 文件打开失败:file error2

协议设计目的

先发送数据长度,是为了防止一次接收时数据无法被客户端全部接收,以及循环接收时,让客户端知晓什么时候接收完毕

其他可选方法

  • 最后发生特殊标志告知客户端发送完毕
  • 将客户端文件描述符设置为非阻塞模式,等待服务器发送数据

各个方案都有利有弊,采取后会出现新的问题需要解决

服务端

work_thread.c
  1. DealClientData()
    1. recv 接收客户信息
    2. 调用 CmdAnalysis() 解析客户端命令
    3. 调用 CmdProcess() 处理命令
cd_process.c
  1. CmdAnalysis():解析命令
    1. 使用 strtok_r(线程安全版) 对命令解析切割
  2. CmdProcess():处理命令
    1. 使用 strncmp 区分不同的命令,进行不同的处理
      1. cd命令:调用 CdCommand()
      2. pull、push:下一部分实现
      3. 其他命令:调用 OtherCommand()
  3. CdCommand():cd命令实现
    1. 调用 chdir 切换路径
    2. 给客户端发送对应消息
      1. 先发送长度
      2. 再发送内容
  4. OtherCommand()
    1. 创建无名管道
    2. fork() 创建子进程执行命令
      1. 子进程关闭管道读端
      2. 使用 dup2 将标准输出、标准错误重定向到管道写端
      3. 关闭管道写端
      4. 调用 execvp 执行指定命令
    3. 父进程处理结果
      1. 关闭管道写端
      2. 等待子进程执行结束
      3. 调用 PipeSaveToFile() 将管道内容保存到文件
      4. 关闭管道读端
      5. 调用 SendMsg() 给客户端发送消息
  5. PipeSaveToFile()
    1. 创建/打开一个临时文件 temp.txt
    2. 读取管道内容,并写入临时文件
    3. 关闭临时文件
  6. SendMsg()
    1. 打开临时文件 temp.txt
    2. 先向客户端发送文件大小
    3. 再发送文件内容
    4. 关闭临时文件
    5. 删除临时文件
路径设置

为防止错误操作原代码路径文件,在 main() 最开始通过 chdir 修改程序执行路径

客户端

cli.c
  1. ChatWithServer()
    1. 调用 fgets 获取用户输入
    2. 调用 strncmp 区分不同的命令,进行不同的处理(将命令划分为pull、push、其他命令)
    3. 其他命令
      1. 调用 RecvServerMsg() 接收服务端消息
        1. 先接收 4 字节数据长度
        2. 再循环接收数据,并进行打印

七、下载和上传功能

功能说明

基于自定义协议,实现客户端与服务器之间的文件互传功能:

  • pull 下载:客户端从服务器获取文件,支持文件大小校验与断点续传准备
  • push 上传:客户端将本地文件发送至服务器,支持服务器路径安全控制
  • 整个流程基于 "请求 - 确认 - 传输" 的三步式协议,避免粘包与数据截断问题,保障文件传输的完整性与可靠性。

实现步骤

1、实现 pull 下载功能

确认协议

在实现下载功能之前,需要约定一套完整的通信协议,规范客户端与服务器的交互流程

协议包括:客户端如何发送下载命令,服务端如何回复,客户端如何再次回复保证通信和双方接受能力的正常

  1. 客户端发送下载命令格式:pull 文件名
  2. 服务器回复:
    1. 正常回复:OK#文件大小(#用来分隔两部分内容)
    2. 错误回复:file error 1或file error 2(此处需要规定不同错误的原因)
  3. 客户端二次回复:
    1. 正常回复:OK
    2. 错误回复:file error 1或file error 2
思考

思考1:可以省略5、6步(客户端的二次确认)吗

不建议省略。如果客户端发送下载请求后反悔,没有确认步骤,服务器会直接发送数据,导致客户端被迫接收。因此,服务器发送 OK#size 后,需要客户端回复确认,再开始传输。

思考2:客户端何如何判断数据发送完成?

服务器从文件读取到末尾就知道传输结束,但客户端无法感知。因此,协议约定服务器先发送文件大小,客户端按大小接收,当接收字节数等于文件大小时,判定传输完成。

思考3:服务器回复 OK 时,能否不等客户端确认直接发数据?

不行,会产生粘包问题。OK#size 刚发送出去就发文件内容,客户端无法区分 size 和后续数据,必须通过客户端的 OK 回复来分割两部分数据。

代码实现思路

该功能需要客户端和服务器端配合实现:

服务端
  1. 检查文件合法性:
    1. 若命令无文件名,回复 file error1
    2. 若文件不存在 / 打开失败,回复 file error2
    3. 若回复错误,直接结束下载流程,无需等待客户端确认
  2. 等待客户端确认
    1. 若客户端回复 OK,继续后续传输
    2. 若回复错误,结束下载流程
  3. 发送文件内容
    1. 打开文件,读取数据并发送给客户端
    2. 传输完成后关闭文件
客户端
  1. 接收服务端的回复
    1. 若收到 file error1 / file error2,直接结束下载
    2. 若收到 OK#size,解析文件大小,准备接收
  2. 二次回复确认
    1. 检查本地文件是否存在,若存在可回复 "已存在" 结束流程
    2. 若不存在,回复 OK#,告知服务器可以开始传输
  3. 接收并写入文件
    1. 按文件大小循环接收数据,写入本地文件
    2. 接收字节数达到文件大小时,结束下载

2、实现 push 上传功能

push 是 pull 下载的逆实现:客户端将本地指定文件上传到服务端的执行路径中,核心流程与下载对称,同样基于自定义协议实现一问一答的交互

确认协议

在实现上传功能前,先约定一套完整的通信协议,规范客户端与服务端的交互流程:

  1. 客户端发送上传目录:push 文件名
  2. 服务端回复:
    1. 正常回复:OK(确定可以接收文件)
    2. 错误回答:file error 1 / file error 2(分别对应文件名无效、文件打开失败)
  3. 客户端二次回复:发送文件大小
  4. 服务器二次回复:OK(确认可以开始接收文件内容)
  5. 客户端发送文件内容,传输完成后结束流程

代码实现思路

客户端 cli.c
  1. PushFileToServer()
    1. 命令解析与文件检查
      1. 解析 push 文件名 命令,提取目标文件名
      2. 打开本地文件,若打开失败则直接退出
      3. 向服务端发送 push 文件名 命令
    2. 服务端回复处理
      1. 接收服务端回复,若为错误信息则关闭文件并退出
      2. 收到 OK 回复后,继续上传流程
    3. 文件大小同步
      1. 获取文件大小,发送给服务端
      2. 等待服务端回复 OK,确认可以发送文件内容
    4. 文件数据发送
      1. 循环读取文件内容,通过 send 发送给服务端
      2. 当已发送字节数等于文件大小时,结束发送,关闭文件
服务端 cmd_process.c
  1. GetFileFromClient()
    1. 文件创建与合法性校验
      1. 检查文件名是否有效
      2. 创建 / 打开目标文件(O_WRONLY | O_CREAT),若打开失败则回复错误信息
      3. 回复 OK,确认可以接收文件
    2. 文件大小接收与确认
      1. 接收客户端发送的文件大小
      2. 回复 OK,告知客户端可以开始发送文件内容
    3. 文件数据接收与写入
      1. 循环接收客户端发送的文件数据,写入目标文件
      2. 当已接收字节数等于文件大小时,结束接收,关闭文件

关键设计要点

  1. 二次确认机制:服务端收到上传命令后,必须回复 OK,客户端才能发送文件内容,避免服务端未准备好就接收数据;
  2. 数据粘包避免:通过两次 send/recv 交互(文件大小确认),将 "大小同步" 和 "文件数据" 分离,避免粘包问题;
  3. 传输完成判定:客户端按文件大小循环发送,服务端按文件大小循环接收,确保文件完整传输。

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 后无回复,直接结束

原因

  1. 客户端 strncmp 匹配错误:客户端判断服务端回复时,strncmp 长度设置错误,导致无法识别 FILE_ERR1/FILE_ERR2 错误信息
  2. 路径问题:服务端工作路径 /home/wuya/qzj/project 下无 a.txt 文件,文件打开失败

解决

  1. 修正客户端错误信息匹配逻辑:
    *

    cpp 复制代码
    if(strncmp(buff, "FILE_ERR1", sizeof("FILE_ERR1")) == 0 || 
       strncmp(buff, "FILE_ERR2", sizeof("FILE_ERR2")) == 0) {
        printf("server reply: %s\n", buff);
        return;
    }
  2. 确保服务端工作路径下存在目标文件,或调整文件路径。

d、push 命令执行时服务端回复 file error1

现象

客户端输入 push a.txt 时,服务端回复 file error1,文件上传失败

排除过程

  1. 通过打印调试发现,客户端发送给服务端的消息只有 push,丢失了后面的文件名
  2. 进一步定位发现:客户端在发送前调用 strtok_r 切割 userMsg 以获取文件名,该操作修改了原始 userMsg 字符串,导致后续 send 发送的内容被截断,只发送了 push

解决

在对 userMsg 进行 strtok_r 切割前,先保存一份原始字符串的副本,发送时使用未被修改的副本,确保完整命令被发送到服务端

结果

push 命令可正常发送完整命令,服务端成功接收文件名,文件上传流程正常

e、文件存在但服务端提示打开失败(file error2)

现象

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

排除过程

  1. 使用 perror("open failed") 打印错误信息,显示 No such file or directory。
  2. 路径分析发现:
    1. push 命令:客户端从当前执行路径找文件,上传到服务端的 /home/wuya/qzj/project 路径。
    2. pull 命令:服务端从 /home/wuya/qzj/project 路径找文件,下载到客户端的当前执行路径。
  3. 本次问题根源是对程序执行路径混淆,误以为 push 会将文件上传到客户端路径,实际文件已上传到服务端工作路径,客户端执行 pull 时因路径理解错误导致文件找不到

解决

明确服务端工作路径与客户端执行路径的差异,在 pull 命令中使用文件的完整路径,确保服务端能正确找到文件

结果

文件可正常打开,pull 命令能成功从服务端下载文件到客户端

f、ls 命令输出异常,包含文件内容且文件消失

现象

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

排除过程

  1. 定位 OtherCommand() 函数中的 PipeSaveToFile(),发现该函数将管道命令的输出固定写入 a.txt 文件,而不是临时文件。
  2. 后续 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 将文件指针定位到已下载位置,继续写入数据,实现断点恢复

完整实现流程

服务端
  1. 合法性检验:判断文件名是否有效、文件是否存在,不存在则恢复错误信息
  2. 基础信息回复:获取文件大小,向客户端发送 OK#文件总大小
  3. 客户端反馈处理:接收客户端的 OK#偏移量 或 file already exists 消息
    1. 收到 "文件已存在",直接关闭文件并结束传输
    2. 若收到偏移量,通过 lseek 将文件指针定位到指定位置
  4. 断点续传数据发送:从偏移位置开始,循环读取文件并发送数据,直至文件发送完毕
客户端
  1. 服务端响应解析:接收服务端的 OK#文件总大小 或错误信息,错误则直接退出
  2. 本地文件检查:尝试打开目标文件,若存在则通过 stat 获取文件大小作为 downsize
  3. 文件完整性判断:若 downsize 与服务端返回的文件总大小相等,说明文件已完整存在,向服务端发送 file already exists 消息并退出
  4. 断点续传准备:以追加模式打开文件,通过 lseek 将文件指针定位到 downsize 位置,向服务端发送 OK#downsize 消息
  5. 断点数据接收:循环接收服务端数据并写入文件,同时通过 printf + fflush 实时打印下载进度,直至接收完毕

关键细节与问题修复

问题 1:客户端文件已存在时,服务端一直阻塞等待

  • 现象:客户端发现文件已完整存在后直接退出,未向服务端发送反馈,导致服务端 recv 一直阻塞
  • 原因:服务端 recv 未收到客户端消息,会一直等待,直到客户端断开连接才会返回错误
  • 修复:客户端文件已存在时,必须向服务端发送 file already exists 消息,服务端收到后主动关闭文件并结束传输

问题 2:传输进度打印不更新、卡死

  • 现象:进度条显示不变,或添加 sleep(1) 后程序反应缓慢
  • 原因:printf("\r...") 不会自动刷新标准输出缓冲区,进度信息被缓存未及时显示
  • 修复:在进度打印后添加 fflush(stdout),强制刷新缓冲区,确保进度实时更新

其他关键细节

  • 服务端发送数据时,必须从 lseek 后的偏移位置读取文件,避免重复发送已传输数据
  • 客户端接收数据时,需限制单次 recv 的最大字节数,避免读取到后续无关数据
  • 循环接收数据时,需以「已接收字节数 ≥ 剩余待接收字节数」作为退出条件,确保文件完整接收

运行效果

优化方向

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

相关推荐
用户805533698031 小时前
嵌入式Linux驱动开发——设备树语法与编译工具——读懂这张"藏宝图"
linux·嵌入式
原来是猿2 小时前
网络计算器:理解序列化与反序列化(下)
linux·开发语言·网络·网络协议·json·php
木木_王2 小时前
嵌入式学习 | STM32裸板驱动开发(Day01)入门学习笔记(超详细完整版|点灯实验 + 库函数代码 + 原理全解)
linux·驱动开发·笔记·stm32·学习
勤自省2 小时前
ROS2从入门到“重启解决”:21讲8~12章踩坑血泪史与核心总结
linux·开发语言·ubuntu·ssh·ros
原来是猿2 小时前
Linux守护进程(Daemon)完全指南:从原理到实战
linux·运维·服务器·网络·php
阡陌..2 小时前
如何使用samba为Linux设置一个局域网共享盘
linux·运维·服务器
霞姐聊IT3 小时前
三大并发技术—进程、线程和协程
linux·运维·网络·操作系统
南境十里·墨染春水3 小时前
linux学习进展 网络编程——HTTPS (补充)
linux·网络·学习
t5y223 小时前
【Linux】学习小计
linux