学习笔记——小型ftp服务器搭建(linux环境下C实现)

目录

一 FTP指什么?

1.1FTP服务器概念:

1.2 FTP协议

二 功能介绍及开发的思路

2.1.server服务端

1 socket 函数原型:

2 bind函数原型:

3 connect函数原型:

4 listen函数原型:

 5 accecpt函数原型: 

2.3 指令介绍(自己实现的几个小功能)

2.4socket 知识补充

2.4.1 socket中TCP的三次握手建立连接:

2.4.2 socket中TCP的四次挥手释放连接:

2.4.3 高低字节

2.4.4 高低地址:

三 代码实现

3.1 服务端代码:

3.2 客户端代码:

3.3 子写头文件(header.h):


一 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@ChenPi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值