实现轻量化FTP后台服务器

Posted _Camille

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现轻量化FTP后台服务器相关的知识,希望对你有一定的参考价值。

本篇内容是实现ftp后台服务器,以达到多人文件共享,功能可参考vsftpd,是一个轻量化的ftp。


前言

有时我们需要一个文件服务器,用于大家的文件共享、上传、下载,并且可以达成不同平台之间的共享,比如Windows系统和Linux系统,因此我们需要实现一个服务,已满足于我们的共享需求,并配合于相应的客户端(leapftp)进行使用。显然vsftpd就是这样的,在服务器上安装vsftpd并且启动,客户端(Linux使用lftp,Windows使用leapftp)连接服务器就可以实现共享文件资源。
vsftpd的安装(root):yum install vsftpd -y
vsftpd的配置:vim /etc/vsftpd/vsftpd.conf
vsftpd的启动、停止:systemctl start vsftpd | systemctl stop vsftpd
防火墙关闭/状态查看:systemctl stop firewalld/systemctl status firewalld

Linux下的客户端
Linuxlftp的安装 :yum install lftp -y
lftp连接ftp :lftp 服务器IP
lfpt命令:
1.help:查看全部可操作的命令
2.ls : 显示FTP服务器文件列表 ;!ls: 显示本地文件列表
3.cd: 切换远端目录; !cd: 切换本地目录
4.get: 从FTP服务器下载单个文件到本地当前目录 ;mget: 从FTP服务器下载多个文件到本地当前目录; pget :使用多个线程来下载远端文件
5.put : 将单个文件上传到FTP服务器 ;mput :将多个文件上传到FTP服务器
6.mv :移动FTP服务器上的文件
7.rm: 删除FTP服务器上的文件 (使用参数 -r 递归删除) ;mrm: 删除FTP服务器上的多个目录
8.mkdir :在FTP服务器上建立目录
9.pwd : 显示日前FTP服务器所有目录 ;lpwd: 显示本地目录
10.exit :退出ftp会话过程
发现没有权限创建文件或是目录,可能是selinux引起的登陆问题。 为避免每次开机都要作这个操作,可在setsebool命令后面加上-P选项,使改动永久有效
getsebool -a | grep ftp
setsebool allow_ftpd_full_access on
sestatus -b| grep ftp
当使用以上命令出现selinux=disabled时,解决方案可以查看这篇博客: selinux=disable

Windows环境下客户端
lefpftp:安装包


开发环境:Linux、Windows、leapftp、socket、Tcp、centos-7

一、项目总体设计

1.FTP是什么?

FTP(File Transfer Protocol,FTP)就是文件传输协议。用于互联网双向传输,控制文件下载空间在服务器复制文件从本地计算机或本地上传文件复制到服务器上的空间。文件传输协议是用于在网络上进行文件传输的一套标准协议,它工作在 OSI 模型的第七层,TCP 模型的第四层, 即应用层, 使用 TCP 传输而不是 UDP, 客户在和服务器建立连接前要经过一个“三次握手”的过程, 保证客户与服务器之间的连接是可靠的, 而且是面向连接, 为数据传输提供可靠保证。

2.FTP工作原理


在客户端,通过交互的用户界面,客户从终端输入启动FTP的用户交互式命令建立控制连接客户端TCP协议层根据用户命令给出的服务器IP地址,向服务器提供FTP服务的21端口(该端口是TCP协议层用 来传输FTP命令的端口)发出主动建立连接的请求,服务器收到请求后,通过3次握手,就在进行FTP命令处理的用户协议解释器进程和服务器协议解释器进程之间建立一条TCP连接,当客户通过交互式的用户界面,向FTP服务器发出要下载服务器上某一文件的命令时,该命令被送到用户协议解释器,当客户发出退出FTP的交互式命令时,控制连接被关闭,FTP服务结束。
FTP协议的命令和应答
ftp不同于一般的服务,该服务需要同时开启两个端口,一个端口是命令通道,另一个端口为数据传输通道。根据数据传输通道建立的不同,分为了主动模式和被动模式。

主动模式
1、 客户端向服务器端发送PORT命令
客户端创建数据套接字
客户端绑定一个临时端口
客户端在套接字上监听
将IP与端口格式化为h1,h2,h3,h4,p1,p2
2、 服务器端以200响应
服务器端解析客户端发过来的IP与端口暂存起来,以便后续建立数据连接
3、 客户端向服务器端发送LIST
服务器端检测在收到LIST命令之前是否接收过PORT或PASV命令
如果没有接受过,则响应425Use PORT or PASV first
如果有接收过,并且是PORT,则服务器端创建数据套接字(bind 20端口),调用connect主动连接
客户端IP与端口,从而建立数据连接
4、 服务器发送150应答给客户端,表示准备就绪,可以开始传输了
5、 开始传输列表
6、 服务器发送226应答给客户端,表示数据传输结束
传输结束,服务器端主动关闭数据套接字。

被动模式
1、 客户端向服务器端发送PASV命令
2、 服务器端以227响应
服务器端创建监听套接字
服务器端绑定一个临时端口
服务器在套接字上监听
将IP与端口格式化为h1,h2,h3,h4,p1,p2响应给客户端,以便客户端发起数据连接
3、 客户端向服务器端发送LIST
服务器端检测在收到LIST命令之前是否接受过PORT或PASV命令
如果没有接收过,则响应424 Use PORT or PASV first
如果有接收过,并且是PASV,则调用accept被动接受客户端的连接,返回已连接套接字,从而建立
数据连接
4、 服务器发送150应答给客户端,表示准备就绪,可以开始传输
5、 开始传输列表
6、 服务器发送226应答给客户端,表示数据传输结束
传输结束,客户端主动关闭数据套接字
云服务器出现被动模式连接失败的先检查IP和端口,IP为公网,端口21,如果IP和端口号都没有问题请点这里

3.项目目标

实现一个ftp的后台服务器,实现过程中需要配合现成的客户端leapftp使用,是一个交互开发的过程(会依赖leapftp,是一个相互解析的过程),项目具体需要完成6个功能FTP命令列表实现、参数配置、空闲断开、限速
、连接数限制、断点续传。

二、项目系统设计

1.系统逻辑结构

2.代码设计

首先本次项目的目的是实现vsftpd那样的服务器,因此我们先看一下vsftpd和leapftp客户端之间是怎样交互的。
主动模式下的leapftp:

被动模式下的leapftp:

蓝色字体为vsftpd服务器的响应,因此我要做的就是模仿蓝色字体来响应绿色字体(客户端)的响应,来做出属于自己的ftp服务器

以下是代码:

1.代码初设:

本次代码初设的任务只要是让客户端先连接上服务器,读取到服务器的文件列表。

通用头文件common.h

#ifndef _COMMON_H_
#define _COMMON_H_

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pwd.h>
#include<shadow.h>
#include<crypt.h>
#include<dirent.h>
#include<sys/stat.h>
#include<time.h>

 //错误退出的封装
#define ERR_EXIT(msg)\\
		do{\\
			perror(msg);\\
			exit(EXIT_FAILURE);\\
		}while(0)
 //为什么用do while语句,将1718行两句当做一个整体处理
 
#define MAX_COMMOND_LINE_SIZE 1024
#define MAX_CMD_SIZE          128
#define MAX_ARG_SIZE          1024
#define MAX_BUFFER_SIZE       1024
#define MAX_CWD_SIZE          512

#endif /* _COMMON_H_ */

系统文件

sysutil.h

#ifndef _SYSUTIL_H_
#define _SYSUTIL_H_

#include"common.h"

int tcp_server(const char *host, unsigned short port);//服务器套接字的创建
int tcp_client();//客户端套接字的创建

char* statbuf_get_perms(struct stat *sbuf);//文件权限的组织
char* statbuf_get_date(struct stat *sbuf);//文件修改日期的组织
#endif /* _SYSUTIL_H_ */

sysutil.c:

#include "sysutil.h"

int tcp_server(const char* host, unsigned short port)
{
    int listenfd;
    if((listenfd = socket(AF_INET, SOCK_STREAM, 0))<0)
    {
        ERR_EXIT("socket");
    }

    struct sockaddr_in addrSer;
    addrSer.sin_family = AF_INET;
    addrSer.sin_port = htons(port);
    addrSer.sin_addr.s_addr = inet_addr(host);
//设置地址重用
    int on =1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))<0)
        ERR_EXIT("setsockopt");
    if(bind(listenfd, (struct sockaddr*)&addrSer, sizeof(addrSer))<0)
        ERR_EXIT("bind");
    if(listen(listenfd, SOMAXCONN) <0)
        ERR_EXIT("listen");

    return listenfd;
}

int tcp_client()
{
    int sock;
    if((sock = socket(AF_INET, SOCK_STREAM,0))<0)
        ERR_EXIT("socket");
    return sock;
}

char* statbuf_get_perms(struct stat *sbuf)
{
    static char perms[] = "----------";
    mode_t mode = sbuf->st_mode;
    switch(mode & S_IFMT)
    {
        case S_IFREG:
            perms[0]='-';
            break;
        case S_IFDIR:
            perms[0]='d';
            break;
        case S_IFCHR:
            perms[0]='c';
            break;
        case S_IFIFO:
            perms[0]='p';
            break;
        case S_IFBLK:
            perms[0]='b';
            break;
        case S_IFLNK:
            perms[0]='l';
            break;
    }

    if(mode & S_IRUSR)
        perms[1]='r';
    if(mode & S_IWUSR)
        perms[2]='w';
    if(mode & S_IXUSR)
         perms[3] = 'x';
    if(mode & S_IRGRP)
         perms[4] = 'r';
    if(mode & S_IWGRP)
         perms[5] = 'w';
    if(mode & S_IXGRP)
         perms[6] = 'x';
    if(mode & S_IROTH)
         perms[7] = 'r';
    if(mode & S_IWOTH)
         perms[8] = 'w';
    if(mode & S_IXOTH)
         perms[9] = 'x';                        
     return perms;
}

char* statbuf_get_date(struct stat *sbuf)
{
    static char date[64] = {0};
    struct tm *ptm = localtime(&sbuf->st_mtime);
    strftime(date, 64, "%b %e %H:%M", ptm);
    return date;
}

字符串剪切函数

该函数用于在获取客户端的命令后,解析命令行,对命令中/r/n进行过滤
头文件:str.h

#ifndef _STR_H_
#define _STR_H_

#include"common.h"

void str_trim_crlf(char *str);
void str_split(const char *str, char *left, char *right, char token);

#endif /* _STR_H_ */

函数实现str.c:

#include"str.h"

void str_trim_crlf(char *str)
{
	char *p = str + (strlen(str)-1);
	while(*p=='\\r' || *p=='\\n')
		*p-- = '\\0';
}


void str_split(const char *str, char *left, char *right, char token)
{//left存命令,right存内容
	char *pos = strchr(str, token);
	if(pos == NULL)
		strcpy(left, str);
	else
	{
		strncpy(left, str, pos-str);
		strcpy(right, pos+1);
	}
}

服务进程:

进程组中的服务进程,是主进程的子进程(nobody)的子进程
头文件ftpproto.h:

#ifndef _FTPPROTO_H_
#define _FTPPROTO_H_

#include"common.h"
#include"session.h"

void handle_child(session_t *sess);

#endif /* _FTPPROTO_H_ */

ftpproto.c:

#include "ftpproto.h"
#include "session.h"
#include "str.h"
#include "ftpcodes.h"
#include "sysutil.h"
#include "privsock.h"

static void ftp_reply(session_t *sess, unsigned int code, const char *text)
{
    char buffer[MAX_BUFFER_SIZE] = {0};
    sprintf(buffer, "%d %s\\r\\n", code, text);
    send(sess->ctrl_fd, buffer,strlen(buffer),0);
}

static void do_user(session_t *sess);
static void do_pass(session_t *sess);
static void do_syst(session_t *sess);
static void do_feat(session_t *sess);
static void do_pwd(session_t *sess);
static void do_type(session_t *sess);
static void do_port(session_t *sess);
static void do_pasv(session_t *sess);
static void do_list(session_t *sess);
static void do_pasv(session_t *sess);
static void do_list(session_t *sess);
//命令映射
typedef struct ftpcmd
{
    const char *cmd;//命令
    void (*cmd_handler)(session_t *sess);//命令处理
}ftpcmd_t;

ftpcmd_t ctrl_cmds[] = 
{
    {"USER",do_user},
    {"PASS",do_pass},
    {"SYST",do_syst},
    {"FEAT",do_feat},
    {"PWD" ,do_pwd },
    {"TYPE",do_type},
    {"PORT",do_port},
    {"PASV",do_pasv},
    {"LIST", do_list},
};
//ftp服务进程
void handle_child(session_t *sess)
{
    //send(sess->ctrl_fd, "220(myFTP 1.0.1)\\r\\n",strlen("220(myFTP 1.0.1)\\r\\n"),0);
    ftp_reply(sess, FTP_GREET,"myFTP 1.0.1");
    //ftp服务进程
    
    while(1)
    {
        //不停等待客户端的命令并作出处理
        memset(sess->cmdline,0,MAX_COMMOND_LINE_SIZE);
        memset(sess->cmd,0,MAX_CMD_SIZE);
        memset(sess->arg,0,MAX_ARG_SIZE);
        int ret =recv(sess->ctrl_fd, sess->cmdline,MAX_COMMOND_LINE_SIZE,0);
        if(ret < 0)
            ERR_EXIT("recv");
        if(ret == 0)
            exit(EXIT_SUCCESS);
        //printf("cmdline = %s\\n", sess->cmdline);
        str_trim_crlf(sess->cmdline);
        str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
        //printf("cmdline = %s\\n", sess->cmdline);
        //printf("cmd = %s\\n", sess->cmd);
        //printf("arg = %s\\n", sess->arg);
        //命令映射的查找
        int table_size = sizeof(ctrl_cmds)/sizeof(ctrl_cmds[0]);
        int i;
        for(i=0; i<table_size;++i)
        {
            if(strcmp(sess->cmd, ctrl_cmds[i].cmd) == 0)
            {
                if(ctrl_cmds[i].cmd_handler)
                    ctrl_cmds[i].cmd_handler(sess);
                else
                    ftp_reply(sess,FTP_COMMANDNOTIMPL, "Unimplement command.");
                break;
            }
        }
        if(i>=table_size)
            ftp_reply(sess,FTP_BADCMD,"Unknown command");
    }
}

static void do_user(session_t *sess)
{
    struct passwd *pwd = getpwnam(sess->arg);
    if(pwd != NULL)
        sess->uid = pwd->pw_uid;//保存用户IDuid

    ftp_reply(sess,FTP_GIVEPWORD, "Please specify the password");
}
static void do_pass(session_t *sess)
{
    //验证登录鉴权
     struct passwd *pwd = getpwuid(sess->uid);//获取用户名信息
     if(pwd == NULL)
     {
         ftp_reply(sess, FTP_LOGINERR,"Login incorrect.");
         return;
     }
     struct spwd *spd = getspnam(pwd->以上是关于实现轻量化FTP后台服务器的主要内容,如果未能解决你的问题,请参考以下文章

实现轻量化FTP后台服务器

实现轻量化FTP后台服务器

实现轻量化FTP后台服务器

实现轻量化FTP后台服务器

个人项目:实现轻量化FTP服务器

个人项目:实现轻量化FTP服务器