目录
一 FTP指什么?
1.1FTP服务器概念:
FTP是用来在两台计算机之间传输文件,是Internet中应用非常广泛的服务之一,它可根据实际需设置各用户的使用权限,同时还具有跨平台的特性,即在UNIX、Linux和Windows等操作系统中都可实现FTP客户端和服务器,相互之间可跨平台进行文件的传输。
FTP(File Transfer Protocol)即文件传输协议,是一种基于TCP的协议,采用客户/服务器模式,通过FTP协议,用户可以在FTP服务器中进行文件的上传或下载等操作,虽然现在通过HTTP协议下载的站点有很多,但是由于FTP协议可以很好地控制用户数量和宽带的分配,快速方便地上传、下载文件,因此FTP已成为网络中文件上传和下载的首选服务器,同时,它也是一个应用程序,用户可以通过它把自己的计算机与世界各地所有运行FTP协议的服务器相连,访问服务器上的大量程序和信息
1.2 FTP协议
FTP服务的功能是实现完整文件的异地传输,特点如下:
(一)FTP使用两个平行连接:控制连接和数据连接。控制连接在两主机间传送控制命令,如用户身份、口令、改变目录命令等。数据连接只用于传送数据
(二)在一个会话期间,FTP服务器必须维持用户状态,也就是说,和某一个用户的控制连接不能断开。另外,当用户在目录树中活动时,服务器必须追踪用户的当前目录,这样,FTP就限制了并发用户数量
(三)FTP支持文件沿任意方向传输。当用户与一远程计算机建立连接后,用户可以获得一个远程文件也可以将一本地文件传输至远程机器
二 功能介绍及开发的思路
2.1.server服务端
步骤1:socket网络编程,主要函数socke,bind,listen,access
1 socket 函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数说明:
- domain指明所使用的协议族,通常为PF_INET,表示TCP/IP协议;
- type指定socket的类型,基本上有三种:数据流套接字、数据报套接字、原始套接字
- protocol通常赋值"0"。
- 成功,返回socket文件描述符;失败,返回-1,并设置errno
两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。socket数据结构中包含这五种信息。
2 bind函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能说明:
将套接字和指定的端口相连。成功返回0,否则,返回-1,并置errno.
参数说明:
- sock_fd是调用socket函数返回值, 和open函数类似,一个操作句柄
- my_addr是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针;
struct sockaddr_in结构类型是用来保存socket信息的: struct sockaddr_in { short int sin_family; //网络协议的方式 unsigned short int sin_port; //端口号 struct in_addr sin_addr; //ip地址 unsigned char sin_zero[8]; };
- addrlen为sockaddr的长度。
3 connect函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能说明:
客户端发送服务请求。成功返回0,否则返回-1,并置errno。
参数说明:
- sock_fd 是socket函数返回的socket描述符;
- serv_addr是包含远端主机IP地址和端口号的指针;
- addrlen是结构sockaddr_in的长度。
4 listen函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能说明:
等待指定的端口的出现客户端连接。调用成功返回0,否则,返回-1,并置errno.
参数说明:
- sock_fd 是socket()函数返回值;
- backlog指定在请求队列中允许的最大请求数
5 accecpt函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能说明:
用于接受客户端的服务请求,成功返回新的套接字描述符,失败返回-1,并置errno。
参数说明:
- sock_fd是被监听的socket描述符
- addr通常是一个指向sockaddr_in变量的指针
- addrlen是结构sockaddr_in的长度,要注意是指针类型
2.3 指令介绍(自己实现的几个小功能)
#define LS 0 //查看服务单前目录下的文件
#define PWD 1 //获取服务器当前目录
#define CD 2 //操作服务器的目录
#define GET 3 //获取文件
#define PUT 4 //发送文件
#define LLS 5 //查看客户端当前目录
#define LCD 6 //操作客户端的目录
2.4socket 知识补充
2.4.1 socket中TCP的三次握手建立连接:
(1)tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
(2)示意图:
(3)步骤:
1.当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;
2.服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;
3.客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
(4)总结:
客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。
2.4.2 socket中TCP的四次挥手释放连接:
(1)示意图:
图2:四次挥手
(2)过程:
- 1.某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
- 2.另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
- 3.一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
- 4.接收到这个FIN的源发送端TCP对它进行确认。
【注意】中断连接端可以是Client端,也可以是Server端。
假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!
2.4.3 高低字节
在十进制中靠左边的是高位,靠右边的是低位,在其他进制也是如此。例如 0x12345678,从高位到低位的字节依次是0x12、0x34、0x56和0x78。
字节序:
字节序是指多字节数据在 计算机内存中存储 或者 网络传输时 各字节的存储顺序。
Little endian 小端字节序(主机字节序,现代PC大多采用小端字节序)
低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
Big endian 大端字节序(网络字节序):
高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
对于数据 0x12345678(左端为高位),假设从地址0x4000开始存放,在大端和小端模式下,存放的位置分别为:
内存地址 小端模式 大端模式
0x4003 0x12 0x78
0x4002 0x34 0x56
0x4001 0x56 0x34
0x4000 0x78 0x12
采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节,而Big-endian模式对操作数的存放方式是从高字节到低字节。
小端存储后:0x78563412 大端存储后:0x12345678
2.4.4 高低地址:
C程序映射中内存的空间布局大致如下:
- 最高内存地址 0xFFFFFFFF
- 栈区(从高内存地址,往 低内存地址发展。即栈底在高地址,栈顶在低地址)
- 堆区(从低内存地址 ,往 高内存地址发展)
- 全局区(常量和全局变量)
- 代码区
- 最低内存地址 0x00000000
三 代码实现
3.1 服务端代码:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include<stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <fcntl.h>
#include <string.h>
#include "header.h"
int n_read = 0; //读到的字节数
void erro_printf(int data,char *commod); /* 打印函数调用失败的原因 */
//void msg_handler(char buf[1024],int fd); //根据的子进程指令做出相应的动作
int get_cmd(char *buf); //将读取到的数据进行比对
void mymemset(char *data); //将对应的字符串清空
void msg_handler(struct msg_handler pmsg,int fd); //buf为接收到的指令字符串,fd为向客户端的发送文件描述符
char *get_cmd_strtok(char buf[128]);
void main()
{
/* 1 socket */
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
erro_printf(socket_fd,"socket error");
/* 2 bind */
struct sockaddr_in s_addr;
s_addr.sin_family = AF_INET; //网络方式
s_addr.sin_port = htons(8888) ;
inet_aton("192.168.1.144",&s_addr.sin_addr);
int ret_bind = bind(socket_fd,(struct sockaddr * )&s_addr, sizeof(struct sockaddr_in));
erro_printf(ret_bind," bind error:");
/* 3 listen*/
listen(socket_fd,10);
int addr_len = sizeof(struct sockaddr_in);
/* 4 access*/
struct msg_handler msg;
while (1)
{
int client_fd = accept(socket_fd,(struct sockaddr * )&s_addr, &addr_len);
erro_printf(client_fd,"accept error:");
printf("get connect :%s\n",inet_ntoa(s_addr.sin_addr));
if(0 == fork()) //子进程,每当一个用户介入创建一个子进程
{
while (1)
{
mymemset(msg.readbuf);
mymemset(msg.sendbuf);
n_read = read(client_fd,msg.readbuf,sizeof(msg.readbuf)); //子进程卡在read函数
if(0 == read) //读取到的字节数为零
{
printf("Client quit\n");
break;
}else if(n_read > 0) //读取到数据
{
printf("来自客户端:%s \n",msg.readbuf);
msg_handler(msg,client_fd); //调用msg_handler函数,解析来自客户端的数据并做出操作
}
}
}
}
}
/*功能:将对应字符串清为0
* 函数:mymemset
* 参数:data
*返回值:无返回值
* ***********************************/
void mymemset(char *data)
{
memset(data,'\0',1024);
}
/*功能:打印函数调用失败的原因
* 函数:erro_printf
* 参数:data和commod,data为如fd之类的整数,commod 为自定义的提示信息
*返回值:无返回值
* ***********************************/
void erro_printf(int data,char *commod)
{
if(-1 == data)
{
perror(commod);
exit(-1);
}
}
/*************************************
* 函数:int get_cmd(char *buf)
* 参数:buf读到从客户端的指令
* 返回值:成功返回对应的宏,错误返回-1
* ***********************************/
void msg_handler(struct msg_handler pmsg,int fd) //buf为接收到的指令字符串,fd为向客户端的发送文件描述符
{
FILE *fp = NULL; //popen 文件句柄
int ret = get_cmd(pmsg.readbuf); //swich指令句柄
char *fileName;
char *buf;
switch (ret)
{
case LS:
case PWD:
fp = popen(pmsg.readbuf,"r"); //获取文件信息路径
fread(pmsg.readbuf,1024,1,fp);
puts(".........................................................................");
printf("%s \n",pmsg.readbuf);
puts(".........................................................................");
write(fd,pmsg.readbuf,strlen(pmsg.readbuf));
break;
case CD:
//memset(fileName,'\0',256);
fileName = get_cmd_strtok(pmsg.readbuf); //获取将要进入的文件目录
printf("%s\n",pmsg.readbuf);
printf("fdfgvr%s\n",fileName); //打印出调试信息,文件目录
int ret = chdir(fileName); //路径跳转
if (-1 == ret) {
perror("chdir error");
break;
}
write(fd,"cd 指令执行成功",strlen("cd 指令执行成功"));
break;
case GET:
fileName = get_cmd_strtok(pmsg.readbuf); //获取将要进入的文件目录
int accret = access(fileName,F_OK); //判断是否存在
if(-1 == accret)
{
perror("");
puts("NO FILE\n");
write(fd,"NO FILE\n",strlen("NO FILE\n"));
}else
{
printf("文件存在,文件名: %s \n",fileName);
int openfd1 = open(fileName,O_RDWR); //将要操作的文件打开
if(-1 == openfd1) //文件打开失败
{
perror("");
puts("error");
}
memset(pmsg.fileData,'\0',sizeof(pmsg.fileData));
int ret = read(openfd1,pmsg.fileData,sizeof(pmsg.fileData)); //将文件中的信息读取到data中
if(-1 == ret)
{
perror("");
puts("error");
}
printf("data = %s",pmsg.fileData); //将读取到的数据打印出来
close(openfd1);
//free(fileName);
write(fd,pmsg.fileData,strlen(pmsg.fileData)); //将读取到的文件数据传给客户端
usleep(10);
write(fd,"get OK",strlen("get OK")); //将读取到的文件数据传给客户端
break;
}
break;
case PUT:
//memset(fileName,'\0',256);
fileName = get_cmd_strtok(pmsg.readbuf);
printf("PUT 将要操作的文件名: %s\n",fileName);
int fd1 = open(fileName,O_WRONLY|O_CREAT|O_TRUNC,0600); //创建新文件接收数据
if(-1 == fd1)
{
perror("");
}
write(fd,"PUT 指令接收成功\n",strlen("PUT 指令接收成功\n"));
//memset(pmsg.fileData,'\0',sizeof(pmsg.fileData)); //清除缓存
read(fd,pmsg.fileData,sizeof(pmsg.fileData)); //将客户端的文件信息读取到缓存中
write(fd1,pmsg.fileData,strlen(pmsg.fileData)); //将数据写入新创建的信息中
printf("%s\n",pmsg.fileData);
close(fd1); //关闭文件
write(fd,"put指令执行成功\n",strlen("put指令执行成功\n")); //返回指令,防止程序卡住
break;
case LLS:
write(fd,"client LLS OK",strlen("client LLS OK"));
break;
case LCD:
write(fd,"client LCD OK",strlen("client LCD OK"));
break;
default:
write(fd,"指令出错,无法识别",strlen("指令出错,无法识别"));
break;
}
}
3.2 客户端代码:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include<stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <fcntl.h>
#include <string.h>
#include "header.h"
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>
/* 打印函数调用失败的原因 */
void erro_printf(int data,char *commod)
{
if(-1 == data)
{
perror(commod);
exit(-1);
}
}
void msg_handler(struct msg_handler pmsg,int fd);
void main()
{
/* socket */
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
erro_printf(client_fd,"socket error");
/* connect */
struct sockaddr_in s_addr;
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8888) ;
inet_aton("192.168.1.144",&s_addr.sin_addr);
int ret = connect(client_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr));
erro_printf(ret,"connect :");
printf("get connect :%s\n",inet_ntoa(s_addr.sin_addr)); //打印IP
/* read or send*/
struct msg_handler msg;
while(1){
memset(msg.readbuf,'\0',sizeof(msg.readbuf)); //清空指令
memset(msg.sendbuf,'\0',sizeof(msg.sendbuf));
printf(">");
gets(msg.sendbuf); //从键盘获取指令
msg_handler(msg,client_fd); //将从键盘获取的指令比对,并执行指令
int n_read = read(client_fd,msg.readbuf,sizeof(msg.readbuf));
printf("%s\n",msg.readbuf); //读取服务端指令执行完的最后提示信息
}
}
void msg_handler(struct msg_handler pmsg,int fd)
{
int cmd_ret = get_cmd(pmsg.sendbuf); //获取宏定义
char bufdata[1024];
char *fileName; //将要操作的文件名
int open_fd; //open函数文件操作符
int accret = 0;
switch (cmd_ret) //根据宏不同,执行不同指令
{
case LS:
case PWD:
case CD:
write(fd,pmsg.sendbuf,strlen(pmsg.sendbuf));
break;
case GET: //服务器给客户端发文件
write(fd,pmsg.sendbuf,strlen(pmsg.sendbuf)); //发送指令
read(fd,pmsg.readbuf,sizeof(pmsg.readbuf)); //读取发回来的文件
printf("readbuf = %s \n",pmsg.readbuf);
fileName = get_cmd_strtok(pmsg.sendbuf); //获取文件名
open_fd = open(fileName,O_WRONLY|O_CREAT|O_TRUNC,0600); //创建并打开文件
if(-1 == open_fd) //打开失败
{
perror("");
}
write(open_fd,pmsg.readbuf,strlen(pmsg.readbuf)); //将服务端传过来的代码写入文件中
close(open_fd); //关闭文件
break;
case LLS:
write(fd,pmsg.sendbuf,strlen(pmsg.sendbuf)); //发送指令
pmsg.dirName = getcwd(NULL,0); //获取当前路径
printf("%s\n",pmsg.dirName);
DIR *fdir;
fdir = opendir(pmsg.dirName); //打开当前目录
if(NULL == fdir)
{
perror("LLS opendir 失败原因:");
}
errno = 0;
struct dirent *ret1 ; //目录结构体
while(NULL != ( ret1 = readdir(fdir)))
{
printf("inode:%ld name:%s \n",ret1->d_ino,ret1->d_name); //打印出inode编号和文件名
}
closedir(fdir); //关闭目录
break;
case PUT:
write(fd,pmsg.sendbuf,strlen(pmsg.sendbuf)); //发送指令
fileName = get_cmd_strtok(pmsg.sendbuf); //获取文件名
accret = access(fileName,F_OK); //判断文件是否存在
if(-1 == accret)
{
puts("NO FILE\n");
perror("失败原因:");
}else //文件存在
{
memset(pmsg.fileData,'\0',sizeof(pmsg.fileData));
open_fd = open(fileName,O_RDONLY); //只读权限打开此文件
read(open_fd,pmsg.fileData,sizeof(pmsg.fileData));
memset(pmsg.readbuf,'\0',sizeof(pmsg.readbuf));
read(fd,pmsg.readbuf,sizeof(pmsg.readbuf)); //读取发回来的调试信息
write(fd,pmsg.fileData,strlen(pmsg.fileData)); //发送文件内容
printf("%s\n",pmsg.fileData);
}
break;
case LCD:
fileName = get_cmd_strtok(pmsg.sendbuf);
int chdir_ret = chdir(fileName);
if (-1 == chdir_ret) {
perror("chdir 失败原因:");
write(fd,"LCD执行失败",strlen("LCD执行失败"));
break;
}else
{
write(fd,pmsg.sendbuf,strlen(pmsg.sendbuf)); //发送指令
}
break;
default:
write(fd,pmsg.sendbuf,strlen(pmsg.sendbuf)); //发送指令
printf("指令形式有误,输入help获取帮助\n");
break;
}
}
3.3 子写头文件(header.h):
#define LS 0 //查看服务单前目录下的文件
#define PWD 1 //获取服务器当前目录
#define CD 2 //操作服务器的目录
#define GET 3 //获取文件
#define PUT 4 //发送文件
#define LLS 5 //查看客户端当前目录
#define LCD 6 //操作客户端的目录
struct msg_handler
{
char readbuf[1024];
char sendbuf[1024];
char strbuf[128];
//char fileName[256];
char *dirName;
char fileData[1024*5];
};
int get_cmd(char *buf)
{
printf("get_cmd buf = %s\n",buf);
if(strcmp("ls",buf) == 0) return LS;
if(strcmp("pwd",buf) == 0) return PWD;
if(strncmp("cd",buf,2) == 0) return CD;
if(strncmp("get",buf,3) == 0) return GET;
if(strcmp("lls",buf) == 0) return LLS;
if(strncmp("put",buf,3) == 0) return PUT;
if(strncmp("lcd",buf,3) == 0) return LCD;
return -1;
}
char *get_cmd_strtok(char buf[128])
{
char *p = NULL;
p = strtok(buf," ");
p = strtok(NULL," ");
return p;
}