FTP(File Transfer Protocol,文件传输协议) 是 TCP/IP 协议组中的协议之一。FTP协议包括两个组成部分,其一为FTP服务器,其二为FTP客户端。其中FTP服务器用来存储文件,用户可以使用FTP客户端通过FTP协议访问位于FTP服务器上的资源。在开发网站的时候,通常利用FTP协议把网页或程序传到Web服务器上。此外,由于FTP传输效率非常高,在网络上传输大的文件时,一般也采用该协议。
用C语言实现一个简单的FTP项目
本次FTP项目可以实现几个功能:
- 客户端和服务端之间可以实现上传文件和下载文件
- 文件日志的写入
- 可以切换文件目录
- 可以查看文件所含有的文件
- 可以使用md5对上传的文件和下载的文件进行校验
- 可以查看过去的输入的命令
- 实现用户登入
- 退出
日志
日志的主要作用是什么呢?是为了方便我们我们在运行程序时将一些所得到的的参数返回值数据写入我们的日志文件,当程序出错时,我么通过日志来更快速的找到出错的地方。
首先创建log.h和log.c文件存放实现日志功能的打开、写入、关闭函数
//log.h
#ifndef _LOG_H
#define _LOG_H
void log_creat(char *filename);
void log_destory();
void log_write(const char *format,...);
#endif
//log.c
#include <stdarg.h>
#include <time.h>
#include <string.h>
FILE *fp = NULL;
void log_creat(char *filename) { //打开日志文件
fp = fopen(filename, "a+"); //filename:文件名 “a+”:在文件尾追加文件内容
if (NULL == fp) {
perror("open my data failed");
}
fseek(fp, 0, SEEK_END);
}
void log_destroy() { //关闭日志文件
fclose(fp);
fp = NULL;
}
void log_write(const char *format, ...) { //写入日志内容
va_list asg;
va_start(asg, format);
vfprintf(fp, format, asg);
va_end(asg);
fflush(fp);
}
void log_time() { //获取系统的时间并写入日志便于记录
time_t t;
struct tm *t1;
char buf[128];
time(&t);
t1 = localtime(&t);
sprintf(buf, "--- %d-%d-%d %d:%d:%d----\n", t1->tm_year + 1900,
t1->tm_mon + 1, t1->tm_mday, t1->tm_hour, t1->tm_min, t1->tm_sec);
fwrite(buf, sizeof(char), strlen(buf), fp);
}
创建一个结构体,存放文件的内容、md5校验码等
#ifndef _MSG_H
#define _MSG_H
#define SERVERPORT 8080//端口号
//typedef enum FTP_CMD FTP_CMD
enum FTP_CMD{ //用枚举类型来存放相关功能的序号
FTP_CMD_LS=0,
FTP_CMD_GET=1,
FTP_CMD_PUT=2,
FTP_CMD_QUIT=3,
FTP_CMD_CD=4,
FTP_CMD_HISTORY=5,
FTP_CMD_CHECK=6,
FTP_CMD_ERROR,
};
struct Msg{ //有关于存放文件的结构体
enum FTP_CMD cmd; //功能标识符
char args[32];//命令
char md5[32];//md5校验码
long data_length;//文件长度
char data[5000];//文件内容
};
struct User //用户
{
enum FTP_CMD cmd;
char username[32]; //用户名
char password[10]; //密码
};
#endif
服务器的实现
在编写服务器的代码之前先回忆一下服务器与客户端建立连接的相关步骤:
- 建立套接字
- 为套接字添加信息(IP和端口号)
- 监听网络连接
- 接收网络连接
- 接收和发送
- 断开连接
//ftps.c
int main(int argc, char **argv) {
int s_fd;
int c_fd;
int n_bind;
int n_listen;
int ret;
char buf[128] = {0};
log_creat("server.txt"); //创建服务器的日志文件
log_time();//将系统时间写入日志总
// creat socket
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
struct Msg *msg_recv = (struct Msg *)malloc(sizeof(struct Msg));//接收客户端发送来的内容
struct Msg *msg_send = (struct Msg *)malloc(sizeof(struct Msg));//发送给客户端的内容
memset(&s_addr, 0, sizeof(struct sockaddr_in)); //清空服务器的地址
memset(&c_addr, 0, sizeof(struct sockaddr_in));//清空客户端的地址
s_fd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
if (-1 != s_fd) {
log_write("creat socket successfully,s_fd=%d\n", s_fd);//将套接字的返回参数写入日志文件
} else {
log_write("creat socket failed,s_fd=%d\n", s_fd);
exit(-1);
}
s_addr.sin_family = AF_INET; //添加IPV4协议
s_addr.sin_port = htons(SERVERPORT); //添加端口号,端口号为固定值
s_addr.sin_addr.s_addr = htonl(INADDR_ANY);//t添加客户端地址,使用INADDR_ANY表示为任何客户端都可以连接这个服务端
// 添加Ip和端口号
n_bind = bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
if (-1 != n_bind) {
log_write("bind successfully,n_bind=%d\n", n_bind);//将返回参数写入日志文件
} else {
log_write("bind failed,n_bind=%d\n", n_bind);
exit(-1);
}
// 服务器监听
n_listen = listen(s_fd, 10);
if (-1 != n_listen) {
log_write("creat socket successfully,n_listen=%d\n", n_listen);//将返回参数写入日志文件
} else {
log_write("listen failed,n_listen=%d\n", n_listen);
exit(-1);
}
int on = 1;//让端口号可以重复使用
setsockopt(n_listen, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int c_len = sizeof(struct sockaddr_in);
//连接客户端
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &c_len);
if (-1 == c_fd) {
log_write("accept failed,c_fd=%d\n", c_fd);//将返回参数写入日志文件
exit(-1);
}
printf("get connect:%s\n", inet_ntoa(c_addr.sin_addr));
//.....读写后面实现
}
客户端代码实现
//ftpc.c
int main(int argc, char **agrv) {
int c_fd;
int ret;
char md5[32];
log_creat("client.txt");
log_time();
//struct Msg
struct Msg *msg_send = (struct Msg *)malloc(sizeof(struct Msg));
struct Msg *msg_recv = (struct Msg *)malloc(sizeof(struct Msg));
//客户端地址
struct sockaddr_in c_addr;
memset(&c_addr, 0, sizeof(struct sockaddr_in));
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == c_fd) {
perror("client scoket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(SERVERPORT);
inet_aton(SEVERADDR, &c_addr.sin_addr);
ret = connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr));
if (-1 == ret) {
log_write("client connect failed\n");
exit(-1);
} else {
log_write("client connect successfully\n");
}
log_destroy();
//.....读写后面实现
}
第一功能:查看文件
我们在虚拟中输入ls这个命令的时候,会将该目录下的文件都显示出来,同样地,我们在FTP项目也这样实现,当我们输入ls这个命令的时候,客户端将命令发送给服务端,服务端对客户端发送过来的args命令进行处理,将处理后的结果重新发给客户端,然后在客户端上显示出来。
这时候我们要编写一个服务端和客户端可以一起使用的函数,比如分割字符串的函数
//utils.h
#ifndef _UTILS_H
#define _UTILS_H
int split_str(char *str,char *outstr);//分割命令字符串
#endif
//utils.c
#include "utils.h"
#include <string.h>
#include <stdio.h>
int split_str(char *str,char *outstr){
char *first;
int len;
first=strstr(str," ");//使用strstr对接收的命令进行分割,以空格为分界线
while(1){ //有时候我们输入的时候可能会多输空格,为了防止多输,我们要对分割后的第二字符进行判断是否不是空格
if(NULL==first){
return -1;
}
first++;
if(*first!=' ' || *first =='\0'){ //当检测下一个字符不是空格或者末尾时退出循环
break;
}
}
len=strlen(first); //计算分割后字符串的长度
memcpy(outstr,first,len-1);//因为分割后的字符串末尾带有‘\n’,我们要将换行给舍去
return 0;
}
编写服务器和客户端的接收和发送代码
popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh
-c来执行参数command的指令。参数type可使用“r”代表读取,“w”代表写入。依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。此外,所有使用文件指针(FILE*)操作的函数也都可以使用,除了fclose()以外
ftps.c
//服务端读写
g_running=1;
while (g_running) {
log_creat("server.txt");//打开日志
printf(">");
memset(msg_recv,0,sizeof(struct Msg));
ret = recv(c_fd, msg_recv, sizeof(struct Msg), 0);
if (-1 == ret) {
log_write("server receive from client failed,ret=%d\n", ret);
exit(-1);
}
memset(msg_send,0,sizeof(struct Msg));
handle_cmd(msg_recv, msg_send);//处理客户端发送过来的命令
//写入日志
log_write("get the cmd from client:%d\n", msg_recv->cmd);
log_write("%s\n", msg_send->data);
//将结果发送给客户端
ret = send(c_fd, msg_send, sizeof(struct Msg), 0);
if (-1 == ret) {
log_write("server send to client failed,ret=%d\n", ret);
exit(-1);
}
log_destroy();
}
//服务端处理函数
void handle_cmd(struct Msg *in_send, struct Msg *out_send) {
FILE *fp = NULL;
out_send->cmd = in_send->cmd;//保留客户端发送过来的功能标识符
switch (in_send->cmd) {
case FTP_CMD_LS:
fp = popen(in_send->args, "r");//利用popen实现ls命令
if (NULL != fp) {
fread(out_send->data, 1, sizeof(out_send->data), fp);//将数据从fp中读出,存在发送结构体的数据数组中
}else{
log_write("ls failed\n");
exit(-1);
}
pclose(fp);
break;
default:
break;
}
}
ftpc.c
//客户端读写
while (1) {
//用户输入命令
FILE *fp=NULL;
log_creat("client.txt");
handle_cmd(msg_send); //处理命令
//发送给服务器
ret = send(c_fd, msg_send, sizeof(struct Msg), 0);
log_write("send cmd to servr:%d\n", msg_send->cmd);
if (-1 == ret) {
log_write("client send to server failed\n,ret=%d", ret);
return -1;
}
//接收服务器发送的结果
ret = recv(c_fd, msg_recv, sizeof(struct Msg), 0);
if (-1 == ret) {
log_write("client receive from server failed\n,ret=%d", ret);
return -1;
}
//在客户端接收到服务器发送过来的数据,我们通过switch语句对cmd进行判断
switch(msg_recv->cmd){
case FTP_CMD_LS: //输出ls命令内容
printf("%s\n", msg_recv->data);
break;
}
//接收的结果写入日志
log_write("%s\n", msg_recv->data);
log_destroy();
}
//判断用户需要哪个功能
enum FTP_CMD get_cmd(char *buf) {
// printf("after:%s\n", str);
if (memcmp(buf,"ls",2) == 0) { //查看文件
return FTP_CMD_LS;
} else if (memcmp(buf,"get",3) == 0) {//下载文件
return FTP_CMD_GET;
} else if (memcmp(buf,"put",3) == 0) { //上传文件
return FTP_CMD_PUT;
} else if (memcmp(buf,"cd",2) == 0) { //切换文件路径
return FTP_CMD_CD;
}else if(memcmp(buf,"quit",4) == 0){ //退出
return FTP_CMD_QUIT;
}else if(memcmp(buf,"history",7) == 0){ //查看历史记录
return FTP_CMD_HISTORY;
}
else {
return FTP_CMD_ERROR;
}
}
//客户端处理函数
int handle_cmd(struct Msg *msg) { //处理cmd
char buf[32];
int ret; //标记符
FILE *fp;
enum FTP_CMD cmd;
memset(buf, 0, 32);
printf(">");
fgets(buf, 32, stdin);
strcpy(msg->args, buf);
cmd = get_cmd(buf);
msg->cmd = cmd;
if (FTP_CMD_ERROR == cmd) {
return -1;
}
return 0;
}
第二功能:上传文件和下载文件
上传文件:客户端打开文件,将文件内容读取出来,存放在发送结构体的数据数组中,服务端接收到客户端发送过来的数据,打开一个新的文件,将数据写入文件中
下载文件:与上次文件中客户端和服务端的操作相反。
在上次文件和下载文件中,我们也一起将md5来对我们的文件进行校验,如果校验码不相同,则说明上传文件或者下载文件失败
//编写得到文件ms5校验码的函数,添加到untls.c中,函数声明放入untls.h中
void get_md5(char *filename,char *outstr){
char buf[32];
char c_buf[32];
sprintf(buf,"md5sum %s",filename);//添加文件名构成命令,利用popen得到md5校验码
FILE *fp;
fp=popen(buf,"r");
if( NULL!=fp){
int ret=fread(c_buf,1,sizeof(c_buf),fp);
sscanf(c_buf,"%s",outstr);//sscanf函数是通过从字符串中读取想要的内容
}
pclose(fp);
}
//获取文件内数据的长度
long get_length(char *filename){
FILE *fp;
fp=fopen(filename,"r");
if( NULL!=fp){
fseek(fp,0,SEEK_END);
long length=ftell(fp);//ftell函数用来获取数据长度,返回值为long型
return length;
}else{
return -1;
}
fclose(fp);
}
**//上传文件**
//服务器处理函数中添加以下参数
FILE *fp = NULL;
int ret;
char filename[32];
char md5[32]; //md5校验码
long length;
//在处理函数的switch语句中添加上传的代码
case FTP_CMD_PUT:
memset(filename,0,32);
filename[0]='+'; //在新建文件前加上+表示新上传的文件
if(split_str(in_send->args,&filename[1])<0){
log_write("分割失败\n,没有找到%s",filename);
exit(-1);
}
printf("%s\n",filename);
fp=fopen(filename,"w"); //以只写的方式打开文件
if(NULL !=fp){
ret=fwrite(in_send->data,1,in_send->data_length,fp);
log_write("put write ret =%d\n",ret);
printf("客户端上传%s成功\n",filename);
}else{
printf("客户端上传%s失败\n",filename);
log_write("put write failed,ret=%d\n");
exit(-1);
}
fclose(fp);
get_md5(filename,md5);//获取新建文件的md5校验码
if(memcmp(md5,in_send->md5,32)!=0){//进行比对是否相同,不同则将文件移除
log_write("the %s is error,md5=%s\n",filename,md5);
remove(filename);
}else{
log_write("%s md5=%s",filename,md5);
}
break;
//在客户端处理函数中添加上传文件的代码
if(FTP_CMD_PUT==msg->cmd){ //上传文件
memset(filename,0,32);
if(split_str(msg->args,filename)<0){
log_write("分割失败\n,没有找到%s",filename);
return -1;
}
length=get_length(filename); //获取文件内数据的长度
if(length<0||length>5000){
log_write("get file length failed,length=%d\n",length);
exit(-1);
}
fp=fopen(filename,"r");
if(NULL != fp){
ret=fread(msg->data,1,length,fp);//以只读的方式打开文件
msg->data_length=length;
log_write("put read ret=%d\n",ret);
}else{
printf("%s文件上传失败\n",filename);
return -1;
}
fclose(fp);
get_md5(filename,msg->md5);//得到文件的md5校验码
log_write("%s md5=%s\n",filename,msg->md5);
return 0;
}
**//下载文件**
//在服务端的处理函数的switch语句中添加上传的代码
case FTP_CMD_GET:
if(split_str(in_send->args,filename)<0){
log_write("分割失败\n,没有找到%s",filename);
exit(-1);
}
length=get_length(filename);
if(length<0||length>5000){
log_write("get file length failed,length=%d\n",length);
exit(-1);
}
fp=fopen(filename,"r");//以只读的方式打开文件
if(NULL !=fp){
ret=fread(out_send->data,1,length,fp);
out_send->data_length=length;
log_write("read ret=%d\n",ret);
printf("客户端下载%s成功\n",filename);
}else{
printf("客户端下载失败\n");
log_write("open %s failed\n",filename);
exit(-1);
}
get_md5(filename,out_send->md5);
log_write("%s md5=%s\n",filename,out_send->md5);
fclose(fp);
break;
//在客户端接收到服务器发送过来的数据,我们通过switch语句添加下载的代码
case FTP_CMD_GET:
memset(filename,0,32);
filename[0]='_'; //下载文件添加个'_'表示下载的新文件
if(split_str(msg_send->args,&filename[1])<0){
log_write("分割失败\n,没有找到%s",filename);
return -1;
}
fp=fopen(filename,"w");//以只写的方式打开文件
if(NULL !=fp){
ret=fwrite(msg_recv->data,1,msg_recv->data_length,fp);
log_write("write ret=%d\n",ret);
printf("%s下载成功\n",filename);
}else{
printf("%s文件下载失败\n",filename);
return -1;
}
fclose(fp);
get_md5(filename,md5);
if(memcmp(md5,msg_recv->md5,32)!=0){ //md5校验文件是否一致
log_write("%s md5 is error,md5=%s\n",filename,md5);
remove(filename);
}else{
log_write("%s md5=%s\n",filename,md5);
}
break;
第三功能:切换文件目录
实现目录切换,我们需要使用chdir()函数
//服务端的处理函数添加以下参数
char road[32];//存放文件路径
//switch中添加切换目录的代码
case FTP_CMD_CD:
memset(road,0,32);
if(split_str(in_send->args,road)<0){
log_write("分割失败\n,没有找到%s",road);
exit(-1);
}
printf("客户端进入%s目录\n",road);
ret=chdir(road);
if(-1==ret){
log_write("cd the road failed,ret=%d\n",ret);
exit(-1);
}
break;
第四功能:退出
退出功能,设置一个全局变量g_running来判断是否要退出,当接收到客户端的退出命令时,g_running变成0,服务端发送完数据就退出程序,客户端接收到退出的指令,也退出程序。
//服务器的处理函数的switch语句添加退出代码
case FTP_CMD_QUIT:
printf("服务端关闭\n");
g_running=0;
break;
//客户端的接收后的switch语句添加退出代码
case FTP_CMD_QUIT:
printf("欢迎下次使用\n");
exit(-1);
break;
第五功能:查看历史命令
实现查看历史输入的命令,这边我们需要使用到链表,每输入一个指令,就把指令存入链表中,同样我们需要一个整合链表中的命令的函数,将所有的命令整合成一个字符串后输出
创建lisk.h和lisk.c文件
**//lisk.h**
#ifndef _LISK_H
#define _LISH_H
struct LNode {
char buf[32];
struct LNode *next;
};
void insert_LNode(struct LNode **head,char *buf);
void print_LNode(struct LNode *head);
void LNode_getbuf(struct LNode *head,char *outbuf);
#endif
**//lisk.c**
#include "lisk.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//添加节点
void insert_LNode(struct LNode **head,char *buf){
struct LNode *node=(struct LNode *)malloc(sizeof(struct LNode));
node->next=NULL;
strcpy(node->buf,buf);
node->next=*head;
*head=node;
}
//用于测试链表是否有插入成功
void print_LNode(struct LNode *head){
struct LNode *p=NULL;
p=head;
while(p!=NULL){
printf("---------------\n");
printf("%s",p->buf);
p=p->next;
}
}
//整合命令
void LNode_getbuf(struct LNode *head,char *outbuf){
struct LNode *p=NULL;
p=head;
char str[32];
while(p!=NULL){
sprintf(str,"----%s",p->buf);
strcat(outbuf,str);
p=p->next;
}
}
//在服务端的处理函数添加以下代码
static struct LNode *head=NULL;//将头结点设置为静态变量,让链表数据不会更改,只有退出了才会清空
insert_LNode(&head,in_send->args);
//switch中添加以下代码
case FTP_CMD_HISTORY:
LNode_getbuf(head,out_send->data);
break;
//客户端中添加查看历史命令功能的代码
case FTP_CMD_HISTORY:
printf("%s\n", msg_recv->data);
break
第六功能:用户登入
用户登入实现:客户端输入用户名和密码,服务端接收后从用户文件中读取用户名和密码进行对比,如果正确cmd等于FTP_CMD_CHECK,继续执行程序,如果不正确将cmd等于FTP_CMD_ERROR,退出程序
//ftpc.c的main中添加
struct User s_use;
printf("请输入用户名:");
scanf("%s",s_use.username);
printf("请输入密码:");
scanf("%s",s_use.password);
log_write("usr:%s pass:%s\n",s_use.username,s_use.password);
ret=send(c_fd,&s_use,sizeof(struct User),0); //发送用户名和密码给服务端
if(-1==ret){
log_write("user send failed,ret=%d\n",ret);
}else{
log_write("user send ret %d",ret);
}
ret=recv(c_fd,&s_use,sizeof(struct User),0);//接收服务器发送过来结果
if(-1==ret){
log_write("receive the check result failed\n");
}else{
log_write("receive the cmd=%d\n",s_use.cmd);
}
if(FTP_CMD_CHECK==s_use.cmd){
printf("登入成功\n");
}else {
printf("用户名或密码不正确\n");
exit(-1);
}
//ftps.c中main添加以下代码
//判断用户名和密码是否正确
struct User s_user;
char name[10];
char pass[10];
ret=recv(c_fd,&s_user,sizeof(struct User),0);//接收客户端发送过来的用户名和密码
if(-1==ret){
log_write("receive from the client's User failed\n");
}else{
FILE *fp1;
fp1=fopen("password.txt","r");
if(NULL != fp1){
fscanf(fp1,"%s %s ",name,pass);
fclose(fp1);
}else{
log_write("open the password.txt failed\n");
}
}
if(0==memcmp(s_user.username,name,strlen(name))&&0==memcmp(s_user.password,pass,strlen(pass))){ //用户名和密码进行比对
printf("登入成功\n");
s_user.cmd=FTP_CMD_CHECK;
}else{
s_user.cmd=FTP_CMD_ERROR;
}
ret=send(c_fd,&s_user,sizeof(struct User),0);
if(-1==ret){
log_write("send the check result failed\n");
}else{
log_write("send the cmd ofuser check, cmd=%d",s_user.cmd);
}
补充:ftps.c和ftpc.c中的头文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include "log.h"
#include "msg.h"
#include "lisk.h"
以上就是一个简单实现的FTP项目,通过这个项目编写可以更好掌握对文件的读写操作,还有Linux的网络编程,在做这个项目的过程,也学到一些没有用过的函数,比如日志的写入,sscanf函数,strstr函数,chdir函数等等,做这个项目的时候,要善于用git,每成功实现一个功能,都要将代码上传,以防我们下一次添加代码时候造成程序崩溃时,可以通过git来查看我们添加了什么代码,使用日志文件的写入可以更好地帮助我们调试程序,当我们不知道哪里出错,可以通过日志文件来看一些函数的参数,在这个项目中我们也有指针,当出现段错误时,要gdb来查找出错的地方,通过这个项目可以更好地锻炼我们的编程能力和调试代码的能力。
本文介绍了如何使用C语言实现一个简单的FTP项目,包括文件上传、下载、切换目录、查看文件、日志记录、用户登录等功能。项目涉及TCP/IP协议、文件操作、MD5校验及链表结构。通过该项目,可以提升Linux网络编程和调试技能。
1106

被折叠的 条评论
为什么被折叠?



