FTP文件传输协议
何为文件传输协议
解:文件传输时用于在网络上进行文件传输的一套标准协议,它在OSI模型的第七层上工作。简而言之,即用于连接服务端与客户端。
基本模型
Socket服务器和客户端的开发步骤
1.创建套接字
2.为套接字添加信息IP地址和端口号
3.监听网络连接
4.监听到有客户端接入-〉接收一个连接
5.数据交互
6.关闭套接字,断开连接
写一个日志函数
log.h
#ifndef LOG_H
#define LOG_H
void log_create(const char *filename); //创建前缀
void log_destroy(); //销毁/关闭文件
void log_write(const char *filename,...); //写入文件
#endif
log.c
#include <stdio.h>
#include <stdarg.h>
#include "log.h"
FILE *g_log = NULL; //定义一个全局变量
void log_create(const char *filename){ //创建前缀
g_log = fopen(filename,"a+");
if(g_log==NULL){
printf("fopen %s failed\n",filename);
}
}
void log_destroy(){ //关闭文件
fclose(g_log);
g_log = NULL; //防止变成野指针
}
void log_write(const char *format,...){ //写入文件,以下命令可通过查找va_list 找到其具体用法
va_list args; //定义va_list变量
va_start(args,format); //创建
vfprintf(g_log,format,args); //输出字符
va_end(args); //关闭
fflush(g_log); //强制写入文件
}
注:fflush()函数不能忘,否则有可能导致写入失败
编写一个测试单元
log_unisttest.c
#include <stdio.h>
#include <string.h>
#include "log.h"
int main(){
log_create(1.txt);
log_write("hello\n");
log_write("world\n");
log_destroy();
return 0;
}
命令结构体函数
msg.h
#ifndef MSG_H
#define MSG_H
#define SERVER_PORT 8888
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_AUTH=5, //验证用户名密码
FTP_CMD_HIST=6, //历史纪录
FTP_CMD_ERROR, //无效命令
};
struct Msg{
FTP_CMD cmd; //命令
char args[32]; //命令行
char md5[64]; //md5校验值
int data_length; //实际长度
char data[5000];
};
struct AUTH{
enum FTP_CMD cmd;
char username[12];
char password[12];
};
#endif
分割字符串及获取文件的实际长度函数
utils.h
#ifndef UTILS_H
#define UTILS_H
int split_string(char *in_str,char *out_str); //分割字符串,以空格为分隔符 in_str 待分割的字符串 out_str 去掉命令后的第一个参数
long get_length(char *filename); //获取文件的实际长度
void get_md5(char *filename,char *md5sum); //获取文件的md5
#endif
utils.c
#include "utils.h"
#include <stdio.h>
#include <string.h>
int split_string(char *in_str, char *out_str) {
char *one;
char *_i;
one = strstr(in_str, " "); //从in_str查找第一个空格
_i = strstr(in_str, "\n"); //找到\n
while (1) {
if (one == NULL || *one == '\0') { //假设没有找到空格
return -1;
}
one += 1; //移动到下一个空格
if (one != " ") { //判断当前字符是否为空格
break;
}
}
strncpy(out_str, one, _i - one); //不拷贝\n
out_str[_i - one] = '\0'; //设置文件结尾'\0'
return 0;
}
long get_length(char *filename) {
FILE *fp = fopen(filename, "r");
if (fp != NULL) {
int ret = fseek(fp, 0, SEEK_END); //移动到文件末尾,计算长度
if(ret<0){
log_write("fseek\n");
return -1;
}
long length = ftell(fp); //获取长度
//fseek(fp, 0, SEEK_SET);
fclose(fp);
return length;
}
return -1;
}
void get_md5(char *filename,char *md5sum){
char cmd[64]; //转换成命令md5sum 1.txt
sprintf(cmd,"md5sum %s",filename);
char md5[64]; //popen调用md5sum 获取文件的md5
FILE *p = popen(cmd,"r");
if(p!=NULL){
int ret = fread(md5,1,sizeof(md5),p);
log_write("fread:%d,md5:%s",ret,md5);
pclose(p);
}
sscanf(md5,"%s",md5sum); //分割md5的值
return ;
}
/*
int main(){
char *in_str;
char *out_str;
char *filename;
split_string(in_str,out_str);
get_length(filename);
return 0;
}*/
链表
链表头文件
Linklist.h
#ifndef LINKLIST_H
#define LINKLIST_H
struct Linklist{
char cmd[64]; //history历史纪录
struct Linklist *next; //next piont下一个节点
};
void Linklist_insert(struct Linklist **head,char *cmd); //采用头插法插入节点
void Linklist_print(struct Linklist *head);
void Linklist_get_cmd(struct Linklist *head,char *obuf); //获取所有的命令
#endif
链表
Linklist.c
#include<stdio.h>
#include<stdlib.h>
#include "Linklist.h"
#include<string.h>
void Linklist_insert(struct Linklist **head,char *cmd){
struct Linklist *node = (struct Linklist*)malloc(sizeof(struct Linklist)); //申请节点并初始化
node->next=NULL;
strcpy(node->cmd,cmd);
node->next=*head;
*head=node;
}
void Linklist_print(struct Linklist *head){
struct Linklist *p=head;
while(p!=NULL){
printf("%s\n",p->cmd);
p=p->next;
}
}
void Linklist_get_cmd(struct Linklist *head,char *obuf){
char buf[32];
struct Linklist *p=head;
while(p!=NULL){
sprintf(buf,"%s\n",p->cmd); //在命令后面\n
strcat(obuf,buf); 拷贝命令
p=p->next; //指向下一个节点
}
}
链表的测试单元
#include<stdio.h>
#include"Linklist.h"
int main(int argc,char **argv){
struct Linklist *h=NULL;
Linklist_insert(&h,"ls");
Linklist_insert(&h,"cd dir");
Linklist_insert(&h,"get 1.txt");
Linklist_print(h);
char buf[64]={0};
Linklist_get_cmd(h,buf);
printf("%s\n",buf);
return 0;
}
设置你的密码
passwd
abc 123
服务端
写一个server服务端
server.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include "msg.h"
#include "log.h"
#include "utils.h"
#include "Linklist.h"
int g_running;
void handle_cmd_1(struct Msg *in_cmd, struct Msg *out_cmd) {
FILE *fd = NULL;
int ret;
char filename[32];
static struct Linklist *hist = NULL; //局部静态变量,其值一直存在
log_write("cmd: %d,args: %s\n", in_cmd->cmd, in_cmd->args); //in_cmd从网络读取数据,全部写入日志,用于调试
out_cmd->cmd = in_cmd->cmd; //返回的命令
Linklist_insert(&hist,in_cmd->args); //保存的历史命令
if (FTP_CMD_LS == in_cmd->cmd) { //判断in_cmd的命令类型
fd = popen(in_cmd->args, "r");
if (NULL != fd) {
ret = fread(out_cmd->data, 1, sizeof(out_cmd->data), fd);
log_write("fread:%d,eof:%d,args:%s\n", ret, feof(fd),
out_cmd->data);
pclose(fd);
}
} else if (FTP_CMD_GET == in_cmd->cmd) {
if (split_string(in_cmd->args, filename) < 0) { //分割字符
out_cmd->cmd = FTP_CMD_ERROR;
log_write("filename not found\n");
return;
}
fd = fopen(filename, "r");
if (fd != NULL) {
ret = fread(out_cmd->data, 1, out_cmd->data_length, fd);
log_write("fread:%d,eof:%d,args:%s\n", ret, feof(fd),
out_cmd->data);
pclose(fd);
}
} else if (FTP_CMD_PUT == in_cmd->cmd) {
filename[0] = '+'; //获取文件名,并以+开头
split_string(in_cmd->args, &filename[1]);
fd = fopen(filename, "w"); //把文件内容写入文件中
if (fd != NULL) {
ret = fwrite(in_cmd->data, 1, in_cmd->data_length, fd);
log_write("fwrite:%d,filename:%s,data_length:%d\n", ret, filename,
in_cmd->data_length);
fclose(fd);
}
char md5[64]; //对比客户端的md5
get_md5(filename, md5);
if (memcmp(md5, in_cmd->md5, 32) != 0) { //若md5不同,则删掉服务端的文件
remove(filename);
log_write("client:%s,server:%s\n", in_cmd->md5, md5);
}
} else if (FTP_CMD_QUIT == in_cmd->cmd) {
log_write("quit\n");
g_running = 0;
} else if (FTP_CMD_CD == in_cmd->cmd) {
char dirname[32];
if (split_string(in_cmd->args, dirname) < 0) { //获取文件夹的名字
return;
}
ret = chdir(dirname); //切换目录
log_write("chdir:%d", ret);
}else if(FTP_CMD_HIST==in_cmd->cmd){
Linklist_get_cmd(hist,out_cmd->data);
}
}
int main(int argc, char **argv) {
int listenfd;
int ret;
int sock;
struct Msg *msg_send = NULL;
struct Msg *msg_recv = NULL;
msg_send = (struct Msg *)malloc(sizeof(struct Msg));
msg_recv = (struct Msg *)malloc(sizeof(struct Msg));
log_create("server.txt");
listenfd = socket(AF_INET, SOCK_STREAM, 0); //socket
if (listenfd < 0) {
log_write("socket failed,ret:%d\n", listenfd);
return -1;
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = SERVER_PORT;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1; //允许地址重用
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
ret = bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if (ret < 0) {
log_write("bind failed,ret:%d\n", ret);
return -1;
}
ret = listen(listenfd, 0); //监听模式
if (ret < 0) {
log_write("listen failed,ret:%d\n", ret);
return -1;
}
sock = accept(listenfd, NULL, 0); //返回已经完成3次握手的socket
if (ret < 0) {
log_write("accept failed,ret:%d\n", ret);
return -1;
}
log_write("hello client,connect.\n"); //成功建立TCP连接
struct AUTH auth; //读取用户名和密码
ret = recv(sock, &auth, sizeof(struct AUTH), 0);
log_write("username:%s,password:%s\n", auth.username, auth.password);
struct AUTH server; //获取本地用户名和密码
FILE *p = fopen("passwd", "r");
if (p != NULL) {
fscanf(p, "%s %s", server.username, server.password);
fclose(p);
log_write("%s %s\n",server.username,server.password);
}
if (memcmp(auth.username, server.username, strlen(server.username)) != 0 ||
memcmp(auth.password, server.password, strlen(server.password)) != 0) { //校验用户名密码
auth.cmd = FTP_CMD_ERROR;
log_write("failed\n");
}
ret = send(sock, &auth, sizeof(struct AUTH), 0); //发送校验结果
log_write("send:%d\n", ret);
if (auth.cmd == FTP_CMD_ERROR) {
return -1;
}
g_running = 1; //运行服务端
while (g_running) {
ret = recv(sock, msg_recv, sizeof(struct Msg), 0); //接收到客户端命令
log_write("recv:%d\n", ret);
handle_cmd_1(msg_recv, msg_send); //处理客户端命令
ret = send(sock, msg_send, sizeof(struct Msg), 0); //发送处理结果给客户端
log_write("send:%d\n", ret);
}
log_destroy();
return 0;
}
客户端
一个客户端
client.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "log.h"
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include "msg.h"
#include "utils.h"
int g_running;
enum FTP_CMD get_cmd(char *buf,struct Msg *msg) { //把用户输入的字符串转为FTP_CMD
char *ptr_args;
if(0==memcmp(buf,"ls",2)){
return FTP_CMD_LS;
}
return FTP_CMD_ERROR;
}
int handle_usr_1(struct Msg *msg_send) {
char buf[32];
enum FTP_CMD cmd;
fgets(buf, 32, stdin); //读取命令
log_write("%s\n", buf); //打印调试信息
//检测命令
if (memcmp(buf, "ls", 2) == 0) {
cmd = FTP_CMD_LS;
} else if (memcmp(buf, "get", 3) == 0) {
cmd = FTP_CMD_GET;
} else if (memcmp(buf, "put", 3) == 0) {
cmd = FTP_CMD_PUT;
} else if(memcmp(buf,"quit",4)==0){
cmd = FTP_CMD_QUIT;
g_running=0;
}else if(memcmp(buf,"cd",2)==0){
cmd = FTP_CMD_CD;
}else if(memcmp(buf,"hist",4)==0){
cmd = FTP_CMD_HIST;
}
else {
cmd = FTP_CMD_ERROR;
}
if (cmd == FTP_CMD_ERROR) { //无效命令,返回失败
return -1;
}
msg_send->cmd = cmd; //初始化msg_send
strcpy(msg_send->args, buf);
return 0;
}
int main(int argc, char **argv) {
int sock;
int ret;
struct Msg *msg_send = NULL;
struct Msg *msg_recv = NULL;
msg_send = (struct Msg *)malloc(sizeof(struct Msg));
msg_recv = (struct Msg *)malloc(sizeof(struct Msg));
log_create("client.txt");
struct sockaddr_in serveraddr;
sock = socket(AF_INET, SOCK_STREAM, 0); //建立socket
memset(&serveraddr, 0, sizeof(serveraddr)); //初始化服务端地址
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = SERVER_PORT;
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ret = connect(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); //连接
if (ret < 0) { //连接失败则退出程序
log_write("connect failed,ret:%d\n", ret);
return -1;
}
struct AUTH auth; //输入用户名密码
printf("username:\n");
scanf("%s",auth.username);
printf("password:\n",auth.password);
scanf("%s",auth.password);
auth.cmd=FTP_CMD_AUTH;
ret = send(sock,&auth,sizeof(struct AUTH),0); //发送给服务端验证
log_write("auth.send:%d\n",ret);
ret = recv(sock,&auth,sizeof(struct AUTH),0); //判断用户名密码是否正确
log_write("auth.recv:%d\n",ret);
if(auth.cmd==FTP_CMD_ERROR){
printf("error\n");
return -1;
}
printf("good job!\n");
g_running=1;
while (g_running) {
// memset(msg_send,sizeof(msg_send));
char filename[28];
FILE *p;
if (handle_usr_1(msg_send) < 0) { //等待用户输入
continue;
}
ret = send(sock, msg_send, sizeof(struct Msg), 0); //发送
log_write("send:%d\n", ret);
ret = recv(sock, msg_recv, sizeof(struct Msg), 0); //接收
log_write("recv:%d\n", ret);
log_write("data:%s\n", msg_recv->data);
if (FTP_CMD_HIST==msg_recv->cmd||FTP_CMD_LS == msg_recv->cmd) {
printf("%s\n", msg_recv->data);
} else if (FTP_CMD_GET == msg_recv->cmd) {
filename[0] = '_';
split_string(msg_send->args, &filename[1]);
p = fopen(filename, "w");
if (p != NULL) {
ret = fwrite(msg_recv->data, 1,msg_recv->data_length, p);
log_write("fwrite:%d", ret);
fclose(p);
}
} else if (FTP_CMD_PUT == msg_recv->cmd) {
get_md5(filename,msg_send->md5);
if (split_string(msg_send->args, filename) < 0) {
msg_recv->cmd = FTP_CMD_ERROR;
log_write("filename miss\n");
return -1;
}
p = fopen(filename, "r");
if (p != NULL) {
msg_send->data_length = fread(msg_send->data, 1, sizeof(msg_send->data), p);
log_write("fread:%d\n",msg_send->data_length);
pclose(p);
}
}else if(FTP_CMD_QUIT==msg_recv->cmd){
printf("see you next time!\n");
break;
}
}
log_destroy();
return 0;
}
注意区别
fopen与popen
fopen 函数是打开一个文件,在当前目录下打开文件并对其进行操作。
popen函数通过创建一个管道,调用fork()产生一个子进程,执行一个shell以运行命令来开启一个进程。
fgets
char *fgets(char *s,int size,FILE *stream);
fgets函数用于从文件指针中读取一行,其功能是从stream流中读取size个字符存储到字符指针变量s所指向的内存空间。
ftell
ftell函数用于得到文件位置指针当前相对于文件首的偏移字节数。
使用fseek函数后再调用ftell函数就能够非常容易地确定文件的当前位置。
fscanf
从一个流中执行格式化输入,fscanf遇到空格和换行时结束。
sscanf
取指定长度的字符串
体验
存在问题:编写过程容易将客户端与服务端混淆,不够细心,常常因为一些细节引起段错误,导致程序运行不出来。对于一些函数的理解不够,以致于编写困难。
收获:终于get一个既能git用法,恩!虽然懂得其用法,但有时意识不够到位,需加强。每添加一个功能莫忘先测试在添加,以免出现不可逆转的错误!!!