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功能实现:
- 传输开始前,客户端进入准备状态并告知服务器
- 客户端打开路径对应文件,若打开失败,返回异常信息并告知服务器中断传输
,否则告知服务端输入流打开成功,令服务端打开输出流
- 服务端若打开输出流失败,需告知客户端中断传输,否则告知客户端输入流打开
- 双方写流和读流都开启后,客户端告知服务端数据各字段长度(包括文件名,文件大小),并按各字段长度开始传输
- 服务端逐字节接收,若接收过程中出现异常,需告知客户端传输失败并中断传输
- 告知的数据长度传递完毕后两段即正常完成传输
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 服务器后端,用于承载服务端主要功能,接收客户端远程指令
四.系统运行和测试
服务端和客户端总界面
客户端调用ls, cd功能
客户端调用put功能
客户端调用get功能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eICzBV7-1658071604152)(https://s2.loli.net/2022/07/17/eh43vOWwtuLZVED.jpg)]
五.实现时遇到的困难和坑点记录
直接使用字符串传指令(用宏定义各种状态):
导致各种问题,包括字符不定长增大编程难度,野字符导致比较失效,最后决定用指令码取代字符串指令,在头文件定义了各类指令宏,后来文件系统出现的各种状态也使用了这种定义方式。
用memset初始化输出数组:
实现ls指令时,发现ls根目录后,cd后更下一级目录再ls,ls会把当前目录的文件和根目录的文件一起输出去,原因是函数退出后并不一定会将内存都销毁,有些内存会被操作系统留下来供以后复用,最后通过memset在每次运行ls时将输出数组初始化解决了这个问题。
字符串传输中出现字符串尾部莫名出现/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网络编程》