2022-7-17 FTP客户端项目实现 - 总结

一.简介

FTP客户端(Linux C++, https://gitee.com/tracker647/linftp)

项目描述:该项目模拟FTP客户端与服务端的通信流程,具有shell,网络,文件系统等模块,实现了对服务端的ls,cd,put和get功能,未来考虑添加登录验证,多线程和mkdir和rmdir等更多指令支持。

个人收获:通过该项目对于Linux的文件和网络相关API更为熟悉,了解了网络协议的基本实现方式,同时加深了对TCP传输特点和网络传输原理的理解。

二.项目设计与实现

计划实现功能

指令功能实现情况
ls显示ftp服务器当前目录文件,默认为根目录
cd切换远程目录
help指令一览
open连接指定IP
mkdir创建远程目录
rmdir删除远程目录
get下载文件
put上传文件
quit退出系统
status显示客户端状态
close断开连接

尚未考虑指令的其他功能特性:

​ 登录验证(为方便调试就没做)

​ 多客户端支持(目前试过多进程-> 借此了解vscode调试多进程的方法,以后尝试多线程)

​ 方向键弹出历史指令(一度写了个历史指令队列,但Linux C并没有各按键中断的处理库,除了Ctrl+C,这部分有一个nurse库实现了,但是懒得再引入外部库,放弃了)

功能实现思路

ls功能实现:
  • 客户端发送ls信号等待服务端响应

  • 如服务端正常响应,则客户端进入数据接收状态

  • 服务端将当前路径传入文件系统的对应函数,该函数根据当前路径返回当前目录的所有文件名的字符串

  • 将当前目录信息,文件名集合整合到服务端写缓冲上,发送给客户端

  • 客户端读取后显示服务端当前目录的信息

cd功能实现:
  • 接收 ”./” , “…/”, 相对路径三种输出

  • 对于 “./” 直接返回,让客户端显示当前目录

  • 对于 “…/” 若已在根目录,告知客户端后返回,否则获取上一级目录的路径并将服务端当前目录改为上一级目录,并告知客户端

  • 对于相对路径,与当前路径组合后通过open检查其路径有效性,若路径有效,则将当前路径拷贝为当前路径与相对路径组合后的新路径,并告知客户端

put功能实现:
  • 传输开始前,客户端进入准备状态并告知服务器

- 客户端打开路径对应文件,若打开失败,返回异常信息并告知服务器中断传输

,否则告知服务端输入流打开成功,令服务端打开输出流

- 服务端若打开输出流失败,需告知客户端中断传输,否则告知客户端输入流打开

- 双方写流和读流都开启后,客户端告知服务端数据各字段长度(包括文件名,文件大小),并按各字段长度开始传输

- 服务端逐字节接收,若接收过程中出现异常,需告知客户端传输失败并中断传输

- 告知的数据长度传递完毕后两段即正常完成传输

image-20220717221701664
get功能实现:

- 客户端向服务端传送路径,询问服务端路径有效性,若路径有效,则服务端通知客户端可正常打开输入流,否则告知路径非法,中断传输。

- 客户端打开输入流若成功告知服务端开始数据传输,否则告知打开失败,中断传输。

- 服务端告知客户端数据各字段长度(包括文件名,文件大小),并按各字段长度开始传输

- 客户端逐字节接收,若接收过程中出现异常,需告知服务端传输失败并中断传输

- 告知的数据长度传递完毕后两段即正常完成传输

三.项目总体布局(2022717)

图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wkRVXgKU-1658071604140)(https://s2.loli.net/2022/07/17/b4RT6mklJAVD75t.png)]

目录结构
linftp/

├── ftp_client

├── ftp_server

├── makefile

├── obj/

│ ├── client_shell.o

│ ├── error.o

│ ├── filesys.o

│ ├── ftp_client.o

│ ├── ftp_server.o

│ ├── network.o

│ └── server_backend.o

├── serverRootDir/

└── src/

├── client_shell.c

├── client_shell.h

├── cmd_code.h

├── filesys.c

├── filesys.h

├── ftp_client.c

├── ftp_server.c

├── network.c

├── network.h

├── server_backend.c

└── server_backend.h
主要文件介绍

serverRootDir/ ftp服务器文件根目录

ftp_client 客户端前端,主要指令功能由client_shell实现

cmd_code shell用于shell指令的宏定义

client_shell 客户端上指令的具体实现,包括本地指令和连接服务端后才能用的指令

network 负责联网部分功能的封装和实现,包括客户端连接和服务端socket初始化

filesys 服务端的文件系统,宏定义和功能实现

ftp_server 服务端前端,主要功能由server_backend实现

server_backend 服务器后端,用于承载服务端主要功能,接收客户端远程指令

四.系统运行和测试

服务端和客户端总界面

img

客户端调用ls, cd功能

img

img

img

客户端调用put功能

img

img

img

客户端调用get功能

img

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eICzBV7-1658071604152)(https://s2.loli.net/2022/07/17/eh43vOWwtuLZVED.jpg)]

img

五.实现时遇到的困难和坑点记录

直接使用字符串传指令(用宏定义各种状态):

导致各种问题,包括字符不定长增大编程难度,野字符导致比较失效,最后决定用指令码取代字符串指令,在头文件定义了各类指令宏,后来文件系统出现的各种状态也使用了这种定义方式。

用memset初始化输出数组:

实现ls指令时,发现ls根目录后,cd后更下一级目录再ls,ls会把当前目录的文件和根目录的文件一起输出去,原因是函数退出后并不一定会将内存都销毁,有些内存会被操作系统留下来供以后复用,最后通过memset在每次运行ls时将输出数组初始化解决了这个问题。

知识点:free 释放内存,会归还给操作系统吗?

字符串传输中出现字符串尾部莫名出现/377/177问题:

定义好字符数组后用memset初始化一下。

write的写溢出和read的读溢出:

回声测试时直接让双方以定义的缓存宏为单位通信数据,结果带来了很多问题,具体举例如下:

	char *mes1 = "Hello World"; 
    char *mes2 = "SINCE WHEN YOU ARE THE ONE IN CONTROL?" ;
    char *mes3 = "LOOK A DEER";
    int filefd = open("sockfd",O_RDWR, O_TRUNC);
    write(filefd,mes1,500);

以上的write不仅会读入mes1,还将把mes2和mes3以及各种垃圾字符共500字读入文件中。

而以下代码

//客户端
int datalen = statbuf.st_size;
write(servfd,filename,strlen(filename)); //5
write(servfd,filedata,datalen);	//50

//服务端
#define READ_BUFFER_SIZE 500
read(servfd,filename,READ_BUFFER_SIZE)

将导致服务端把文件名和文件数据全部读入filename数组中,导致后继的逻辑异常。

以上原因全是因为TCP的传输方式是无边界的,意味着数据的读写不受范围限制,一次write不一定对应一个read,可能分几次read走(比如传入的字符串发生了拆包),而N个属性各符一个write共N次输出也可能一次就被read走(造成粘包问题)。

因此网络通信中对每种信息,最好都通知对方自己将发送多少数据的字节(比如上次文件,客户端依次发送文件名长,文件名,数据长,最后数据传输),保证read和write能够一一对应,避免拆包和粘包,才能确保网络程序的正常运行。

另一个方法是在每种信息之间都加入自定义的分界符,不过我没试过。

open的mode位引发的权限问题:

实现put和get时发现一个怪现象,凡是接收端对应路径已经存在同名文件就会导致异常,由于之前是在云服务器编程,全程root,并没有发现这个问题,后来切换到虚拟机编程时才发现是自己的权限问题,文件浏览器一看新建的文件都是加叉加锁的,最后定位到open函数上,一查,发现mode位对权限的计算方式是mode & ~umask,而不是像chmod那样直接修改, 根据参考修改了mode位后,问题解决。

多进程服务端的调试问题:

之前尝试过用多进程的方式实现服务端,原理是有新连接时父进程将另外fork出一个子进程用于会话,而调试器默认只能追踪父进程,无法查知子进程的执行情况,解决方式预先将gdb设置指令为set follow-fork-mode child (在vscode上则是设置launch.json的对应配置的setupCommands属性,详见项目源码),这样调试器将在fork时自动切换到子进程。

六.个人收获心得

  • 不要自信自己在无提前git的情况就能升级函数代码,一旦写崩了有你好受的,我就因此自信自己能把lcd写出来去修改成型的cd函数,结果导致自己花了一整晚才把cd的功能重新恢复。

  • 接第一条,升级代码更好的方法是出芽生殖,比如要修改xxx_fun函数,应该另写个个xxx_fun2去测试,有多种思路想测试还可以写xxx_fun3, xxx_fun4。。。这样万一写崩了直接改回原本的函数就行。

  • 别想着一开始就把代码写的最好,动不动就想要封装或者简化,在有限的知识下的实践带来的产品往往是漏洞百出,再简化的功能,随着代码量的增多也会变成一坨屎,你会感觉注释反而更加重要。此外不管你是什么代码,工作N年后回头去看都会觉得是一坨垃圾。

  • 在自作聪明地为自己已知的文件异常定义状态宏时不如请教下errno。

  • 写put和get有感:不要放过流程出现的每一个可能异常,打上日志!

  • printf日志可能提供比GDB还出其不意的效果。

  • 直接用纸笔记录自己项目的实现情况更直观也更助于思考,我在实现这个项目时常把思路手写到纸质本上,时常就能看到,电子化笔记应该用于以后总结。

七.附录

开发环境

系统:Ubuntu 20.04 LTS

IDE:vscode

参考

https://blog.csdn.net/qq_24889575/article/details/81566164

《UNIX环境编程》

《UNIX网络编程》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值