今天是周六,这周我们实现了基于TCP连接的FTP文件传输,在此重新梳理一遍以及编写过程中遇到的问题。
下面直接看代码(源码没有注释,这些是我现写的):
char.c代码:
#include <string.h>
#include "char.h"
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int split_char(char *in_str,char *out_str){ //定义一个分割字符串的函数
char *first;
char *nn;
first = strstr(in_str," "); //找出in_str中出现空格的位置
nn = strstr(in_str,"\n"); //找出\n出现的位置
while(1){
if(NULL == first || first == "\0"){ //当first等于空或\0时说明是空字符串
return -1;
}
first += 1; //first当前位置是个空格,我们要找有字的符号,所以直接跳到下一个字符
if(*first != ' '){ //*first,如果first的位置上不等于' ',说明找到了,直接break;
break;
}
}
strncpy(out_str,first,nn - first); //拷贝nn-first个字符串到out_str
out_str[nn-first]= '\0'; //在字符串末尾加个结束符
return 0;
}
long get_length(char *filename){ //获取字符串长度
FILE *fp;
fp = fopen(filename,"r");
if(fp != NULL){
fseek(fp,0,SEEK_END); //把光标移到末尾
if(fseek < 0){
Write("fseek failed");
return -1;
}
long length = ftell(fp); //用ftell获取长度赋值给length
//printf("file length %ld\n",length);
fclose(fp);
return length; //返回length的值
}
printf("anyway you failed\n");
return -1;
}
void get_md5(char *filename,char *md5sum){ //获取文件校验码md5,filename是文件名,md5sum存放校验码
char cmd[64];
sprintf(cmd,"md5sum %s",filename); //格式化一下filename,以md5sum %s的格式写到cmd
char md5[64];
FILE *fp;
fp = popen(cmd,"r"); //popen,以只读方式打开一个cmd指令并返回到fp
if(NULL != fp){
int ret = fread(md5,1,sizeof(md5),fp); //读取校验码到md5
Write("fread ret %d,md5 %s\n",ret,md5);
pclose(fp);
}
sscanf(md5,"%s",md5sum); //把校验码放到md5sum中
}
char.h代码:
#ifndef CHAR_H_
#define CHAR_H_
int split_char(char *in_str,char *out_str); //函数声明
long get_length(char *filename);
void get_md5(char *filename,char *md5sum);
#endif
log.c代码:
日志代码,将执行过程写入到日志文件
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
FILE* fd =NULL;
void Create(char *filename){ //定义一个函数用于打开文件
fd = fopen(filename,"a+");
}
void Destroy(){ //定义一个函数用于关闭文件
close(fd);
fd =NULL;
}
void Write(char* msg,...){ //定义一个函数用于写入数据
va_list vaList; //定义va_list型变量名valist
va_start(vaList, msg); //初始化变量valist
vfprintf(fd,msg,vaList); //将数据输出到流中
va_end(vaList); //结束获取
fflush(fd); //刷新
}
log.h代码:
#ifndef __LOG_H__
#define __LOG_H__
void Create(char *filename);
void Destroy();
void Write(char* format,...);
#endif
linklist.c代码:
#include <stdio.h>
#include "linklist.h"
#include <stdlib.h>
#include <string.h>
void Linklist_insert(struct Linklist **head,char *cmd){ //链表插入函数
struct Linklist *new = (struct Linklist *)malloc(sizeof(struct Linklist)); //创建新节点并申请空间
new->next =NULL; //初始化新节点的数据
strcpy(new->cmd,cmd);
new->next = *head; //使用头插法的链表
*head = new;
}
void Linklist_printf(struct Linklist *h){ //遍历输出链表
struct Linklist *p = h;
while(p != NULL){
printf("%s\n",p->cmd);
p = p->next;
}
}
void Linklist_getcmd(struct Linklist *head,char *out_buf){ //把命令存入链表
struct Linklist *p = head;
char buf[1024];
while(p != NULL){
//sprintf(buf,"\n%s\n",out_buf);
strcat(out_buf,p->cmd); //把命令存到out_buf中
//strcat(out_buf,"\n");
//printf("%s",buf);
p = p->next;
}
// printf("%s",buf);
}
linlkist.h代码:
#include <stdio.h>
//#include "linklist.c"
struct Linklist{
char cmd[32];
struct Linklist *next;
};
void Linklist_getcmd(struct Linklist *head,char *out_buf);
void Linklist_insert(struct Linklist **head,char *cmd);
void Linklist_printf(struct Linklist *h);
msg.h代码:
#ifndef _MSG_H_
#define _MSF_H_
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_ERROR,
FTP_CMD_AUTH = 5,
FTP_CMD_HIST = 6,
};
struct Auth{ //定义一个结构体,用于存放用户名密码
enum FTP_CMD cmd;
char username[32];
char password[32];
};
struct Msg{ //定义一个用于存放消息的结构体,存有命令,校验码,数据长度,数据内容
enum FTP_CMD cmd;
char args[32];
char md5[64];
int data_length;
char data[5000];
};
#endif
pws代码:
ray 111 //存储用户名密码数据
FTPserver.c代码:
#include <stdio.h>
#include "log.h"
#include <sys/socket.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include "msg.h"
#include "char.h"
int g_running;
void handle_cmd(struct Msg *in_cmd,struct Msg *out_cmd){ //处理客户端发过来的命令
FILE *fp = NULL;
int ret;
char md5[64];
char filename[32];
char dirname[32];
char buf[1024]="\n";
static struct Linklist *h = NULL;
Linklist_insert(&h,in_cmd->args); //拿到命令后插入一条命令记录到链表
out_cmd->cmd = in_cmd->cmd; //把接收到的命令放到要发送的命令中
switch(in_cmd->cmd){ //判断命令属于那条命令
case FTP_CMD_LS: //如果收到命令是ls
fp = popen(in_cmd->args,"r"); //用popen执行ls命令存放到fp
if(NULL != fp){
ret = fread(out_cmd->data,1,sizeof(out_cmd->data),fp); //读取fp中的数据到要发送命令的data中
Write("fread ret %d,eof %d,data:\n%s\n",ret,feof(fp),out_cmd->data); //Write,将数据写入日志
pclose(fp);
}
break;
case FTP_CMD_GET: //收到的命令是get
if(split_char(in_cmd->args,filename) < 0){ //调用分割字符串函数
out_cmd->cmd = FTP_CMD_ERROR;
Write("file not found\n");
return;
}
long length = get_length(filename); //调用获取文件长度函数存入到length中
if(length < 0 || length > sizeof(out_cmd->data)){ //如果获取的长度小于0或者大于原文件数据长度,说明不对
Write("get length failed,length %d\n",length);
return;
}
get_md5(filename,md5); //获取文件的校验码
strcpy(out_cmd->md5,md5); //将文件的校验码拷贝到out_cm->md5
Write("md5 %s\n",md5); //记录一下校验码
fp = fopen(filename,"r");
if(NULL != fp){
out_cmd->data_length = fread(out_cmd->data,1,length,fp); //读取fp的内容存放到out_cmd->data中并把读取的长度存放到out_cmd->data_length
Write("fread ret %d,eof %d,data:\n%s\n",ret,feof(fp),out_cmd->data);
//out_cmd->data_length = ret;
fclose(fp);
}else{
out_cmd->cmd = FTP_CMD_ERROR;
Write("file not found\n");
}
break;
case FTP_CMD_PUT: //如果接受到的命令是put
filename[0]='+'; //文件的第一个字符为+,用于区分原文件名
split_char(in_cmd->args,&filename[1]); //分割字符串,读取命令后面的文件名并从第二个字符开始存储
//printf("filename %s\n",filename);
fp = fopen(filename,"w"); //创建这个文件
if(NULL != fp){
ret = fwrite(in_cmd->data,1,in_cmd->data_length,fp); //写入接收到的命令中的数据到文件中,写入的长度为client端获取文件的实际长度
Write("fwrite ret %d,filename %s,data:\n%s\n",ret,filename,in_cmd->data); //记录到日志文件
}
fclose(fp);
get_md5(filename,md5); //获取文件校验码
if(0 != memcmp(md5,in_cmd->md5,32)){ //比较接收到的命令中的校验码是否和服务端获取的校验码一致,校验码长度一般为32,所以对比32位就好,
Write("server md5 %s\nclient md5 %s\n",md5,in_cmd->md5); //记录日志文件
remove(filename); //如果校验码不同,使用remove函数删除这个文件
}
break;
case FTP_CMD_QUIT: //如果接收到的命令为quit
g_running = 0; //把全局变量g_running置0,正常情况下g_runing值为1,接收到quit后置0
Write("quit\n");
break;
case FTP_CMD_CD: //如果接收到的命令为cd
if(split_char(in_cmd->args,dirname) < 0){ //分割字符串
Write("Wrong path!\n");
return;
}
int ret = chdir(dirname); //使用chdir函数切换目录
Write("chdir ret %d",ret);
break;
case FTP_CMD_HIST: //如果接收到的命令为hist
Linklist_getcmd(h,buf); //把命令存到buf中
//printf("buf %s",buf);
strcpy(out_cmd->data,buf); //再把buf拷贝到要发送给客户端指令的data中
break;
default: //如果不是以上任何一条命令,退出
break;
}
}
int main(int argc, char const *argv[])
{
int s_fd;
int c_fd;
int ret;
struct sockaddr_in s_addr; //socket地址函数
struct sockaddr_in c_addr;
//char readBuf[128];
struct Msg *send_msg = NULL; //定义一个类型为struct Msg*的结构体
struct Msg *recv_msg = NULL;
send_msg = (struct Msg *)malloc(sizeof(struct Msg)); //分配空间
recv_msg = (struct Msg *)malloc(sizeof(struct Msg));
Create("./FTPserver.txt"); //创建一个名为FTPserver.txt的服务端日志文件
//1 socket
s_fd = socket(AF_INET,SOCK_STREAM,0); //第一步,连接socket
if(s_fd == -1){
Write("socket failed\n");
exit(-1);
}
//2 bind //第二步,设置ip地址和端口号并捆绑
memset(&c_addr,0,sizeof(struct sockaddr_in)); //初始化地址
s_addr.sin_family = AF_INET; //设置IPv4
s_addr.sin_port = htons(8990); //设置端口号,将整型数据转换成网络字节序(TCP/IP网络可识别的数据格式)
inet_aton("192.168.177.132",&s_addr.sin_addr); //设置ip地址到s_addr.sin_addr中
ret = bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr)); //捆绑套接字socket
if(ret < 0){
Write("bind error %d\n",ret);
return -1;
}
//3 listen //监听
if(listen(s_fd,10)== -1){
Write("listen failed\n");
exit(-1);
}
//4 accept //监听到有用户接入,接受
int clen = sizeof(struct sockaddr);
c_fd =accept(s_fd,(struct sockaddr *)&c_addr,&clen); //接受接入,存入c_addr,存clen这么长
if(c_fd == -1){
Write("accept error\n");
exit(-1);
}
Write("Get connect:%s\n", inet_ntoa(c_addr.sin_addr)); //记录日志文件
struct Auth auth; //定义一个struct Auth结构体
ret = recv(c_fd,&auth,sizeof(struct Auth),0); //接收客户端发来的数据
Write("recd ret %d %s %s\n",ret,auth.username,auth.password);
struct Auth server;
FILE *fp = fopen("psw","r"); //打开psw文件,里面存放着用户名密码数据
if(fp != NULL ){
fscanf(fp,"%s %s",server.username,server.password); //从fp中以%s %s的格式读到后面的参数中
Write("server %s %s\n",server.username,server.password);//记录日志文件
fclose(fp);
}
if(0 != memcmp(auth.username,server.username,strlen(server.username)) ||
0 != memcmp(auth.password,server.password,strlen(server.password))){ //如果用户名密码和服务端的psw中内容不一样,把auth_cmd等于FTP_CMD_ERROR
auth.cmd = FTP_CMD_ERROR;
Write("auth falied\n");
}
ret =send(c_fd,&auth,sizeof(struct Auth),0); //把处理好的命令发到客户端
Write("send ret %d\n",ret);
if(FTP_CMD_ERROR == auth.cmd){
return -1;
}
//5 recv
//memset(readBuf,0,sizeof(readBuf));
g_running = 1; //设置全局变量g_running的值为1
while(g_running){ //当全局变量g_running为1时才运行,如果为0,整个程序运行完毕
memset(send_msg,0,sizeof(struct Msg)); //清空send_msg和recv_msg
memset(recv_msg,0,sizeof(struct Msg));
ret = recv(c_fd,recv_msg,sizeof(struct Msg),0); //接收客户端发过来的消息,如果客户端没有发送消息,recv阻塞,程序不会往下运行,直到客户端发送消息
if(ret == -1){
Write("recv error\n");
exit(-1);
}
handle_cmd(recv_msg,send_msg); //处理接收到的消息并把处理好的消息存放到要发送到客户端的消息中
//6 send
ret = send(c_fd,send_msg,sizeof(struct Msg),0); //处理完后发送给客户端
Write("send ret %d\n",ret);
}
Destroy(); //关闭日志文件
return 0;
}
FTPclient.c代码:
#include <stdio.h>
#include "log.h"
#include <sys/types.h>
#include <sys/socket.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include "msg.h"
#include "char.h"
enum FTP_CMD get_cmd(char *buf,struct Msg *msg) { //定义一个返回值为enum FTP_CMD的函数用于处理命令
if(0 == memcmp(buf,"ls",2)){ //如果输入的命令的前2个字符等于ls
strcpy(msg->args,buf); //把输入的命令拷贝到msg_args(里面存放的是命令),
return FTP_CMD_LS; //把ls指令返回到主函数
}
else if(0 == memcmp(buf,"get",3)){ //如果输入的命令前3个字符等于get
strcpy(msg->args,buf); //把输入的命令拷贝到msg->args
return FTP_CMD_GET; //把get命令返回到主函数
}
else if(0 == memcmp(buf,"put",3)){ //如果输入的命令前3个字符等于put
//strcpy(msg->args,buf);
char filename[32];
if(split_char(buf,filename) < 0){ //分割字符串
Write("file not found\n");
return FTP_CMD_ERROR;
}
long length = get_length(filename); //获取文件的长度并存入length
//length = 91;
if(length < 0 || length > sizeof(msg->data)){ //如果获取的长度小于0或者大于5000(msg->data的大小)则退出
Write("get length failed,length %d\n",length);
return;
}
char md5[64];
get_md5(filename,md5); //获取文件的校验码
strcpy(msg->md5,md5); //存入msg->md5
Write("md5 %s\n",md5);
FILE *fp;
fp =fopen(filename,"r"); //打开文件
if(fp != NULL){
msg->data_length = fread(msg->data,1,length,fp); //读取数据,读到msg->data,读一次,一次读length这么长
// strcpy(msg->md5,md5);
//msg->data_length = ret;
Write("fread ret %d,filename %s,data:\n%s\n",length,filename,msg->data);
}
fclose(fp);
return FTP_CMD_PUT; //把put命令返回到主函数
}
else if(0 == memcmp(buf,"quit",4)){ //如果输入的命令是quit
strcpy(msg->args,buf); //把输入的命令存入msg->args
return FTP_CMD_QUIT; //把quit命令返回到主函数
}
else if(0 == memcmp(buf,"cd",2)){ //如果输入的命令是cd
strcpy(msg->args,buf); //把输入的命令存入msg->args
return FTP_CMD_CD; //把cd命令返回到主函数
}
else if(0 == memcmp(buf,"hist",4)){ //如果输入的命令是hist
strcpy(msg->args,buf); //把输入的命令存入msg->args
return FTP_CMD_HIST; //把hist命令返回到主函数
}
return FTP_CMD_ERROR; //如果以上命令都不是,返回error
}
int main(int argc, char const *argv[]) {
int c_fd;
int ret;
struct sockaddr_in c_addr;
struct Msg *send_msg = NULL;
struct Msg *recv_msg = NULL;
send_msg = (struct Msg *)malloc(sizeof(struct Msg));
recv_msg = (struct Msg *)malloc(sizeof(struct Msg));
Create("./FTPclient.txt"); //创建客户端日志文件
// 1 socket
c_fd = socket(AF_INET, SOCK_STREAM, 0); //第一步socket
// 2 connect //第二步,连接
memset(&c_addr, 0, sizeof(struct sockaddr_in));
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(8990); //设置端口号
inet_aton("192.168.177.132", &c_addr.sin_addr); //设置ip地址
ret = connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr)); //开始连接
if (ret == -1) {
Write("Connect failed,ret %d\n", ret);
exit(-1);
}
Write("Connect success\n");
struct Auth auth; //定义一个结构体,存入键盘输入的账号和密码
printf("username:");
scanf("%s",auth.username);
printf("password:");
scanf("%s",auth.password);
ret = send(c_fd,&auth,sizeof(struct Auth),0); //发送消息给服务端判断账号密码是否正确
Write("send ret %d\n",ret);
ret = recv(c_fd,&auth,sizeof(struct Auth),0); //接收服务端发过来的已经处理好的消息
if(FTP_CMD_ERROR == auth.cmd){ //如果命令等于error说明账号密码不对
printf("wrong username or password\n");
return -1;
}
printf("you are in now.\n");
while (1) {
char buf[32];
enum FTP_CMD cmd; //定义一个enum FTP_CMD类型的结构体
memset(buf,0,sizeof(char));
memset(send_msg,0,sizeof(struct Msg));
memset(recv_msg,0,sizeof(struct Msg));
printf("input cmd:\n");
fgets(buf,32,stdin); //fgets,从指定的流 stream 读取一行并存在buf中
Write("%s\n", buf);
cmd = get_cmd(buf,send_msg); //进入get_cmd处理用户输入的命令
//cmd = FTP_CMD_LS;
switch (cmd) {
case FTP_CMD_LS:
break;
case FTP_CMD_GET:
break;
case FTP_CMD_PUT:
break;
case FTP_CMD_QUIT:
break;
case FTP_CMD_CD:
break;
case FTP_CMD_HIST:
break;
case FTP_CMD_ERROR://passing thought
default:
break;
}
send_msg->cmd = cmd; //处理完后把命令存入seng_msg->cmd
strcpy(send_msg->args,buf); //然后再把用户输入的字符存入args
// 3 send
ret = send(c_fd, send_msg, sizeof(struct Msg), 0); //发送命令给服务端
if (ret == -1) {
Write("send error\n");
exit(-1);
}
Write("send_msg->cmd %d\n",send_msg->cmd);
// 4 recv
ret = recv(c_fd, recv_msg, sizeof(struct Msg), 0); //接收服务端处理后发送过来的消息
if (ret == -1) {
Write("read error\n");
exit(-1);
}
Write("recv_msg->cmd %d\n",recv_msg->cmd);
if(FTP_CMD_LS == recv_msg->cmd){ //然后开始判断命令属于哪一个,如果命令为ls,直接在屏幕上输出服务端发过来的消息里的data
printf("%s\n",recv_msg->data);
}
else if(FTP_CMD_GET == recv_msg->cmd){ //如果命令是get
char filename[32];
filename[0]='_'; //为了区分原文件,下载的文件第一个字符设置成_
split_char(send_msg->args,&filename[1]); //分割字符串,获取文件名
//long length = get_length(filename);
//printf("filename %s\n",filename);
//printf("length %ld\n",length);
FILE *fp;
fp=fopen(filename,"w+"); //创建这个文件
if(fp != NULL){
fwrite(recv_msg->data,1,recv_msg->data_length,fp); //把服务端发过来的数据写入到新建的文件
Write("fwrite ret %d\n",ret);
Write("data:\n%s\n",recv_msg->data);
//ret = recv_msg->data_length;
//Write("data length %d",ret);
}
fclose(fp);
char md5[64];
get_md5(filename,md5); //然后获取文件的校验码,
if(0 != memcmp(md5,recv_msg->md5,32)){ //和服务端的校验码对比,如果一样说明正确,不一样删除这个新建的文件
Write("get file failed:\nserver md5 %s\nclient md5 %s",recv_msg->md5,md5);
remove(filename);
}
}
else if(FTP_CMD_QUIT == recv_msg->cmd){ //如果命令是quit
printf("ByeBye!\n"); //此时客户端已经处理完命令,服务端已经退出,客户端直接在屏幕上打印ByeBye
break;
}
else if(FTP_CMD_HIST == recv_msg->cmd){ //如果命令是hist
printf("%s",recv_msg->data); //在屏幕上打印data
}
Write("data:\n%s\n",recv_msg->data); //记录日志文件
}
Destroy(); //关闭日志文件
return 0;
}
我在编写过程中犯了一个多余的错误,我在put的时候,在客户端和服务端都调用了get_length函数,导致服务端在调用get_length的时候,函数内部是有获取到长度,但是返回值返回给主函数时变成了-1,最后发现服务端不需要调用get_length函数,客户端在处理put命令时已经调用过一次,已经获取到了文件的长度并存放到了data_length中,服务端只需要使用客户端发过来的这个长度就可以了。get也是犯了类似的错误,以上。