一、首先介绍一下这个项目要实现的功能
在客户端对服务端的操作:
1.获取服务器的文件(get
)
2.查看服务器的所有文件(ls
)
3.进入服务器的某个文件夹(cd)
4.上传文件到服务器(put
)
5.查看服务器当前在那个文件夹(pwd
)
6.退出 (quit
)
在客户端本地的功能实现:
1.查看客户端本地有哪些文件(lls
)
2.进入客户端的某个文件夹(lcd
)
3.查看客户端当前在那个文件夹下(lpwd
)
4.退出连接(quit
)
二、介绍一下项目开发的思路
服务端:
(1)先建立socket
通道
(2)创建子进程等待读取客户端发送来的指令信息
(3)接收到指令后,执行指令处理函数(cmd_handler
),执行函数找到对应指令执行对应的操作
(4)指令有LS,CD,PWD,GET,PUT,QUIT
:
LS
、PWD
:
调用popen
函数来输出内容,并将内容读取到字符流描述符fp
中,fp
中的内容用于发回到客户端中去
CD
:
由于CD
后要跟参数进入某某文件夹的,所以要用到字符串分割,分割出文件名后调用chdir
函数进入到某某文件夹
GET
:
由于GET
是客户端向服务器取文件,所以开始时要判断一下这个文件到底存不存在,接着调用字符串分割函数分割出函数名(记得在服务端跟客户端都要进行分割),然后在服务端打开文件,将文件的内容读取出来,读取出来的内容发回到客户端,读取完后在服务端关闭该文件
PUT
:
由于PUT
是上传文件到服务端的,所以同样要先进行字符串的分割(记得在服务端跟客户端都要进行分割),分割出要上传的文件的名字,然后在服务端同样分割出该文件名,并创建该文件,并把客户端发送过来的内容写入这个文件里去,写完后关闭该文件
QUIT
:这个比较简单,打印一句话说客户端已经退出连接即可
客户端:
(1)先建立socket
通道
(2)建立好通道后,获取输入的指令信息
(3)此时的指令要分为两部分,一部分是需要接收服务端返回指令执行后的结果的,一部分是不需要接收的,直接在客户端本地输出指令的相关信息
(4)客户端要封装两个函数,一个是用来对指令执行处理的(cmd_handler
),另一个(handler_sever_cmd
)是对接收到服务端返回对应的指令的结果进行处理的
(5)在客户端的指令有LS,LLS,PWD,LPWD,CD,LCD,GET,PUT,QUIT
:
LS
,PWD
,CD
:
这几个指令在客户端的实现比较简单,调用write
函数向服务端发送指令名字即可
LLS
:
调用system
函数,执行指令ls
即可
LPWD
:
调用函数system
,执行pwd
指令即可
LCD
:
由于LCD
后是要跟参数的,所以也要进行字符串的分割,分割出文件夹的名字后,调用chdir
函数进入某某目录
GET
:
这个指令在客户端只要发送指令相关的信息给服务端即可
PUT
:
调用函数分割出要上传的文件名,然后判断一下该文件存不存在,存在的话就打开该文件将文件的内容读取出来,将读取出来的内容发送到服务端去
QUIT
:
这个指令最简单,当客户端输入这个指令的时候说明要退出连接了,所以向服务端发送这个指令告诉一下服务端,然后调用close
关闭套接字,并调用exit
函数退出整个程序
三、总结积累一下项目中学到的东西
(1)分割字符串函数
函数原型:
char *strtok(char *str, const char *delim);
参数:
str
:将要被分割的字符串
delim
:为分割字符串中包含的所有字符(即分割符字符串)
返回值:
返回被分割出来的目的字符串
作用:分解字符串为一组字符串
描述:
strtok
()用来将字符串分割成一个个片段。参数str
指向欲分割的字符串,参数delim
则为分割字符串中包含的所有字符。当strtok
()在参数s的字符串中发现参数delim中包涵的分割字符时,则会将该字符改为\0
字符。在第一次调用时,strtok
()必需给予参数str
字符串,往后的调用则将参数str
设置成NULL
。每次调用成功则返回指向被分割出片段的指针。
str
开头开始的一个个被分割的串。当没有被分割的串时则返回NULL
。所有delim
中包含的字符都会被滤掉,并将被滤掉的地方设为一处分割的节点。
ftptest1
.c
#include <stdio.h>
#include <string.h>
char *getdir(char *cmd)
{
char *p;
p = strtok(cmd," ");
p = strtok(NULL," ");
// p = strtok(NULL," ");
return p;
}
int main()
{
char *dir;
char *pstr;
gets(pstr);
dir = getdir(pstr);
printf("the dir is:%s\n",dir);
return 0;
}
结果:
CLC@Embed_Learn:~/SECOND/SOCKET/FTP$ ./a.out
haha ju
the dir is:ju
由结果中可看出,为什么strtok
要执行两次呢,执行第二次是获取空格后的第二个字符串的意思,如果是一个strtok
函数的话,那么返回的就是空格前面的那个字符串
(2)判断文件存不存在的函数
函数原型:
int access(const char *pathname, int mode);
参数:
pathname
:需要检测的文件路径名
mode
:需要测试的操作模式
参数mode
的说明:
R_OK
测试读许可权
W_OK
测试写许可权
X_OK
测试执行许可权
F_OK
测试文件是否存在
返回值:
成功执行时,返回0
,失败返回-1
,errno
被设为以下的某个值
EINVAL
: 模式值无效
EACCES
: 文件或路径名中包含的目录不可访问
ELOOP
: 解释路径名过程中存在太多的符号连接
ENAMETOOLONG
:路径名太长
ENOENT
:路径名中的目录不存在或是无效的符号连接
ENOTDIR
: 路径名中当作目录的组件并非目录
EROFS
: 文件系统只读
EFAULT
: 路径名指向可访问的空间外
EIO
:输入输出错误
ENOMEM
: 不能获取足够的内核内存
ETXTBSY
:对程序写入出错
(3)进入某个文件夹的函数
函数原型:
int chdir(const char *path);
参数:
path
:所指代的工作目录(即将要进入的工作目录)
函数功能:用来将当前的工作目录改变成以参数path
所指的工作目录
返回值:
成功返回0
,失败返回-1
,errno
为错误代码
温馨提示:我们在linux终端执行cd ..
的时候是回到上一层目录,我们这里的chdir(..)
同样是可以执行回到上一层目录的
(4)清空缓冲流函数
函数原型:
int fflush(FILE *stream);
函数功能:
清除一个流,即清除文件缓冲区,即当文件以写的方式打开时,将缓冲区的内容写入到文件中去,也就是说对于ANSIC
规定的是缓冲文件系统,函数fflush
用于将缓冲区中的内容输出到文件中去
切记两种特殊的情况:
fflush(stdin)
用于刷新标准输入缓冲区(即键盘),把输入缓冲区中的内容丢弃
fflush(stdout)
用于刷新标准输出缓冲区(即屏幕),把输出缓冲区里的东西立即打印到标准输出设备(即屏幕)上
例子详解:
fflush用于清空缓冲流,虽然一般感觉不到,但是默认printf是缓冲输出的。 fflush(stdout),使stdout清空,就会立刻输出所有在缓冲区的内容。 fflush(stdout)这个例子可能不太明显,但对stdin很明显。 如下语句: int a,c; scanf("%d",&a); c=getchar(); 输入: 12(回车) 那么 a=12 ,c= '\n' 而: int a,c; scanf("%d",&a); fflush(stdin); c=getchar(); 输入: 12(回车) 那么a=12, c暂时未得到输入值,还需要再输入c,因为getchar也是缓冲输入,'\n'本还在缓冲区,但是被清空了。
另外fflush不能作用于重定向输入流。fflush(stdin)刷新标准输入缓冲区,把输入缓冲区里的东西丢弃
fflush(stdout)刷新标准输出缓冲区,把输出缓冲区里的东西打印到标准输出设备上
fflush(stdout);这句起什么作用???——fflush()的作用是:如果圆括号里是已写打开的文件的指针,则将输出缓冲区的内容写入该指针指向的文件,否则清除输出缓冲区。这里的stdout是系统定义的标准输出文件指针,默认情况下指屏幕,那就是把缓冲区的内容写到屏幕上。可是从代码中看不出缓冲区会有什么内容,所以它实际上没有起什么作用
四、总结一下做项目过程中遇到的问题:
问题一:
在客户端的PUT
操作时,在分割字符串的时候,另外定义一个字符数组或字符指针把msg.cmd
里的内容(即输入的put
和将要上传的文件名)拷贝出来再对新的字符数组或字符指针的内容进行字符串的分割,因为直接对输入的msg.cmd
进行字符串的分割的话,strtok
会破坏其完整性,从而导致发送到服务端的msg.cmd
的内容不完整
问题二:
在服务端的CD
操作时,用函数chdir
来实现进入到某个工作目录,而不是用system("cd xxx")
,因为用system
的话会重新打开一个新的终端来进入目标目录,而服务端当前的路径却没有发生改变
问题三:
在服务端向客户端或者是客户端向服务端发送内容时,一定要发送的是整个结构体,而不是单独发送结构体中的某一项,发送结构体中的某一项的话会导致发送的东西不全或甚至是没有发送到东西,因为无论在服务端或客户端接收信息的时候都是读取整个结构体的,所以发送单个结构体的变量的话就会出现上述的情况
问题四:
在进行对输入字符串进行检测的时候,即函数get_cmd_type
,lcd
的检测一定要放在cd
的前面,因为cd
是lcd
中的一个子字符串,所以如果cd
的检测是放在lcd
的前面的话,那么无论是输入的是cd
还是lcd
,都会是返回cd
问题五:
无论是一个字符指针或者是字符数组还是结构体中的字符数组,字符串都是会产生覆盖的,例如指针dir = "hello"
的时候,如果之后修改指针dir = "nihao"
,则会将之前的内容hello
覆盖掉,数组跟指针都会产生覆盖
五、直接上代码
客户端的代码:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <linux/in.h>
#include <stdlib.h>
#include "ftpconfig.h"
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
/*
struct sockaddr_in {
__kernel_sa_family_t sin_family;
__be16 sin_port;
struct in_addr sin_addr;
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
*/
int get_cmd_type(char *cmd)
{
if(strstr(cmd,"lcd") != NULL) return LCD;
if(!strcmp(cmd,"ls")) return LS;
if(!strcmp(cmd