Linux - MiniFtp實現

FTP簡介

文件傳輸協議FTP(File Transfer Protocol,由RFC 959描述)。linux

FTP工做在TCP/IP協議族的應用層,其傳輸層使用的是TCP協議,它是基於客戶/服務器模式工做的。git

FTP支持的文件類型

①、ASCII碼文件,這是FTP默認的文本格式【經常使用】web

②、EBCDIC碼文件,它也是一種文本類型文件,用8位代碼表示一個字符,該文本文件在傳輸時要求兩端都使用EBCDIC碼【不經常使用】windows

③、圖象(Image)文件,也稱二進制文件類型,發送的數據爲連續的比特流,一般用於傳輸二進制文件【經常使用】bash

④、本地文件,字節的大小有本地主機定義,也就是說每一字節的比特數由發送方規定【不經常使用】服務器

因此咱們要實現的FTP也只支持ASCII碼文件和圖像文件類型,對於這兩種文件類型,到底有什麼區別呢?下面作一個簡要的介紹:session

對於文本文件和二進制文件,最直觀的區別就是文本文件是能夠查看,而二進制打開看到的是亂碼,實際上,這二者在物理結構(或存儲結構)上都是由一系列的比特位構成的,它們之間的區別僅僅是在邏輯上:ASCII碼文件是由7個比特位構成,最高位老是0(由於一個字節=8位),因此ASCII碼文件最多能表示的字符數爲2^7=128個,經過man幫助也能看到:數據結構

而若是最高位爲1則打開就會是亂碼,也就是二進制文件最高位應該就是1,這是一個區別。多線程

另一個區別就是\r\n換行符,在不一樣平臺上是不同的:windows上換行是用\r\n表示;linux上換行是用\n表示;mac上換行是用\r表示。若是在傳輸文件的時候,以這兩種文件類型傳輸實際上對\r\n的解析方式是不一樣的,至於有什麼不一樣,這裏經過FTP客戶端鏈接FTP服務端來作一個演示,首先啓動FTP服務器,這裏用vsftpd服務器:socket

接下來進行ftp文件配置:

配置好以後接下來從新啓動vsftpd服務:

接下來用一個ftp客戶端來進行鏈接,鏈接ftp服務器的客戶端有不少工具,這裏用「LeapFtp」:

接下來新建一個文件進行上傳:

能夠用十六進制的文本編輯器來查看一下內容:

接下來開始上傳它至FTP服務器:

上傳以後的大小也是8個字節,來用命令查看一下:

那若是是用二進制文件上傳又會是怎麼樣呢?

那這兩種類型難道沒有差異麼,實際上在個人機器上是沒差異,在有些機器上是有區別的,區別以下:

若是以ASCII方式來傳輸文件,而且從windows->linux會將\r\n轉換成\n,而從linux->windows會將\n轉換成\r\n;而若是以二進制文件來傳輸文件,那麼不作任何轉換。

在C語言階段其實咱們也學過了打開文件能夠以ASCII和二進制兩種方式打開,這二者的區別也就只是換行符的不一樣,跟上面同樣。

FTP文件的數據結構【僅作了解】

文件結構,這是FTP默認的方式,文件被認爲是一個連續的字節流,文件內部沒有表示結構的信息。

記錄結構,該結構只適用於文本文件(ASCII碼或EBCDIC碼文件)。記錄結構文件是由連續的記錄構成的。

頁結構,在FTP中,文件的一個部分被稱爲頁。當文件是由非連續的多個部分組成時,使用頁結構,這種文件稱爲隨機訪問文件。每頁都帶有頁號發送,以便收方能隨機地存儲各頁。

文件的傳輸方式【文件的數據結構會影響傳輸方式】

流方式,這是支持文件傳輸的默認方式,文件以字節流的形式傳輸。【主流FTP也僅僅實現了這種方式】

塊方式,文件以一系列塊來傳輸,每塊前面都帶有本身的頭部。頭部包含描述子代碼域(8位)和計數域(16位),描述子代碼域定義數據塊的結束標誌登內容,計數域說明了數據塊的字節數。

壓縮方式,用來對連續出現的相同字節進行壓縮,如今已不多使用。

FTP工做原理

啓動FTP

在客戶端,經過交互式的用戶界面,客戶從終端上輸入啓動FTP的用戶交互式命令

創建控制鏈接

客戶端TCP協議層根據用戶命令給出的服務器IP地址,向服務器提供FTP服務的21端口(該端口是TCP協議層用來傳輸FTP命令的端口)發出主動創建鏈接的請求。服務器收到請求後,經過3次握手,就在進行FTP命令處理的用戶協議解釋器進程和服務器協議解釋器進程之間創建了一條TCP鏈接。

之後全部用戶輸入的FTP命令和服務器的應答都由該鏈接進行傳輸,所以把它叫作控制鏈接。

創建數據鏈接

當客戶經過交互式的用戶界面,向FTP服務器發出要下載服務器上某一文件的命令時,該命令被送到用戶協議解釋器。

其中用戶的動做會解析成相對應的一些FTP命令,如看到的:

其實也能夠用windows的命令來進行FTP鏈接,也能很清晰地看出用戶的每一個動做都會解析成對應的FTP命令:

FTP命令【先列出來衆覽下,以後會一一實現】

FTP應答

FTP應答格式

服務器經過控制鏈接發送給客戶的FTP應答,由ASCII碼形式的3位數字和一行文本提示信息組成,它們之間用一個空格分割。應答信息的每行文本以回車<CR>和換行<LF>對結尾。

若是須要產生一條多行的應答,第一行在3位數字應答代碼以後包含一個連字符「-」,而不是空格符;最後一行包含相同的3位數字應答代碼,後跟一個空格符,關於這個能夠實際查看下:

FTP應答做用

確保在文件傳輸過程當中的請求和正在執行的動做保持一致

保證用戶程序老是能夠獲得服務器的狀態信息,用戶能夠根據收到的狀態信息對服務器是否正常執行了有關操做進行斷定。

FTP應答數字含義【作了解,不須要記,想知道什麼含義到時對照查看既可】

第一位數字標識了響應是好,壞或者未完成

第二位數響應大概是發生了什麼錯誤(好比,文件系統錯誤,語法錯誤)

第三位爲第二位數字更詳細的說明

如:

500 Syntax error, command unrecognized. (語法錯誤,命令不能被識別)可能包含由於命令行太長的錯誤。

501 Syntax error in parameters or arguments. (參數語法錯誤)

502 Command not implemented. (命令沒有實現)

503 Bad sequence of commands. (命令順序錯誤)

504 Command not implemented for that parameter. (沒有實現這個命令參數)

FTP應答示例【定義的宏,以後程序會用到,先列出來】

#define FTP_DATACONN 150

#define FTP_NOOPOK 200

#define FTP_TYPEOK 200

#define FTP_PORTOK 200

#define FTP_EPRTOK 200

#define FTP_UMASKOK 200

#define FTP_CHMODOK 200

#define FTP_EPSVALLOK 200

#define FTP_STRUOK 200

#define FTP_MODEOK 200

#define FTP_PBSZOK 200

#define FTP_PROTOK 200

#define FTP_OPTSOK 200

#define FTP_ALLOOK 202

#define FTP_FEAT 211

#define FTP_STATOK 211

#define FTP_SIZEOK 213

#define FTP_MDTMOK 213

#define FTP_STATFILE_OK 213

#define FTP_SITEHELP 214

#define FTP_HELP 214

#define FTP_SYSTOK 215

#define FTP_GREET 220

#define FTP_GOODBYE 221

#define FTP_ABOR_NOCONN 225

#define FTP_TRANSFEROK 226

#define FTP_ABOROK 226

#define FTP_PASVOK 227

#define FTP_EPSVOK 229

#define FTP_LOGINOK 230

#define FTP_AUTHOK 234

#define FTP_CWDOK 250

#define FTP_RMDIROK 250

#define FTP_DELEOK 250

#define FTP_RENAMEOK 250

#define FTP_PWDOK 257

#define FTP_MKDIROK 257

#define FTP_GIVEPWORD 331

#define FTP_RESTOK 350

#define FTP_RNFROK 350

#define FTP_IDLE_TIMEOUT 421

#define FTP_DATA_TIMEOUT 421

#define FTP_TOO_MANY_USERS 421

#define FTP_IP_LIMIT 421

#define FTP_IP_DENY 421

#define FTP_TLS_FAIL 421

#define FTP_BADSENDCONN 425

#define FTP_BADSENDNET 426

#define FTP_BADSENDFILE 451

#define FTP_BADCMD 500

#define FTP_BADOPTS 501

#define FTP_COMMANDNOTIMPL 502

#define FTP_NEEDUSER 503

#define FTP_NEEDRNFR 503

#define FTP_BADPBSZ 503

#define FTP_BADPROT 503

#define FTP_BADSTRU 504

#define FTP_BADMODE 504

#define FTP_BADAUTH 504

#define FTP_NOSUCHPROT 504

#define FTP_NEEDENCRYPT 522

#define FTP_EPSVBAD 522

#define FTP_DATATLSBAD 522

#define FTP_LOGINERR 530

#define FTP_NOHANDLEPROT 536

#define FTP_FILEFAIL 550

#define FTP_NOPERM 550

#define FTP_UPLOADFAIL 553

FTP兩種工做模式

上次咱們說過,FTP是由兩種類型的鏈接構成的,一種是控制鏈接【主要是接收FTP客戶端發來的命令請求,而且對這些命令進行應答】,一種是數據鏈接【雙方之間進行數據的傳輸,包括目錄列表的傳輸以及文件的傳輸】,其中控制鏈接老是由客戶端向服務器發起,而數據鏈接則不一樣了,它有兩種工做模式:主動模式【由服務器向客戶端發起鏈接而創建數據鏈接通道】和被動模式【由客戶端向服務器發起鏈接而創建數據鏈接通道】。下面來看一下這兩個工做模式的工做過程:

主動模式

FTP客戶端首先向服務器端的21端口發起鏈接,通過三次握手建設立控制鏈接通道,客戶端本地也會選擇一個動態的端口號AA,一旦控制鏈接通道創建以後,雙方就能夠交換信息了:客戶端能夠經過控制鏈接通道發起命令請求,服務器也能夠經過它向客戶端對這些命令請求進行應答。

接下來,若是要涉及到數據的傳輸,勢必要建立一個數據鏈接:

在建立數據鏈接以前,要選擇工做模式,若是是PORT模式,客戶端會上服務器端發送PORT命令,這也是經過控制鏈接通道完成的,向服務器的21端口傳送一個PORT命令,而且告知客戶端的一個端口號BB,由於這個信息服務器端才知道要鏈接客戶端的哪一個端口號,服務器端獲得了這個信息,最後就向BB端口號發起了一個請求,創建了一個數據鏈接通道,數據鏈接通道一旦創建完畢,就能夠進行數據的傳輸了,包含目錄列表、文件的傳輸,一旦數據傳輸完畢,數據鏈接通道就會關閉掉,它是臨時的。

這裏須要注意一點:

接下來用實驗來講明一下雙方創建的詳細命令,這邊經過登陸一個客戶端來看一下雙方之間所交換的命令:

接下來進行數據傳輸,假設要傳輸一個列表,刷新一下。在得到列表以前須要建立一個數據鏈接,而在建立數據鏈接時須要根據模式來建立數據鏈接,這裏面採用的是PORT模式:

其整個的工做過程以下:

被動模式

在瞭解了主動模式以後,被動模式就比較好理解了,以下:

從中能夠發現,主被動模式只是鏈接創建的方向不一樣而已,一樣的,也經過實驗來查看一下PASV模式所要交換的FTP命令:

這時一樣請求列表:

其整個的工做過程以下:

以上就是FTP的兩種工做模式,那爲何要有這兩種模式呢?這其實是跟NAT或防火牆對主被動模式有關係,下面就來了解下:

NAT或防火牆對主被動模式的影響

什麼是NAT

NAT的全稱是(Network Address Translation),經過NAT能夠將內網私有IP地址轉換爲公網IP地址。必定程度上解決了公網地址不足的問題。

其地址映射關係能夠以下:

192.168.1.100:5678【內網IP】 -> 120.35.3.193:5678【NAT轉換IP】 -> 50.118.99.200:80【外網IP】

從而就創建了一個鏈接,而鏈接的創建是經過NAT服務器進行地址轉換完成的。

FTP客戶端處於NAT或防火牆以後的主動模式

創建控制鏈接通道

由於NAT會主動記錄由內部發送外部[相反則沒法記錄]的鏈接信息,而控制鏈接通道的創建是由客戶向服務器端鏈接的,所以這一條接能夠順利地創建起來。
複製代碼

客戶端與服務器端數據鏈接創建時的通知

客戶端先啓用PORT BB端口,並經過命令通道告知FTP服務器,且等待服務器端的主動鏈接。
複製代碼

服務器主動鏈接客戶端

因爲經過NAT轉換以後,服務器只能得知NAT的地址並不知道客戶端的IP地址,所以FTP服務器會以20端口主動地向NAT的PORT BB端口發送主動鏈接請求,但NAT並無啓用PORT BB端口,於是鏈接被拒絕。
複製代碼

FTP客戶端處於NAT或防火牆以後的被動模式

FTP服務器處於NAT或防火牆以後的被動模式

FTP服務器處於NAT或防火牆以後的主動模式

參數配置

咱們要將程序中的開關作成可配置的,這裏能夠看一下VSFTP的配置文件:

空閒斷開

保存並重啓VSFTP服務:

可見過了5秒空閒鏈接就斷開了,這時進程也結束了:

限速

也就是上傳跟下載文件的限速功能,下面也來演示一下,默認狀況下是沒有限速的:

其速度傳輸過程序中會慢慢降到100K的樣子。

鏈接數限制

這裏包含兩個方面的限制:總鏈接數的限制,針對全部IP來講的、同一個IP鏈接數的限制,下面來進行配置:

接下來配置同一個IP的鏈接數的限制:

斷點續載與斷點續傳

當成功鏈接一個客戶端時,這時能夠看到建立了兩個進程:

可見該FTP服務器是採用多進程的方式來實現的,爲何不用多線程的方式呢?

對於FTP服務器來說,多線程的方式是絕對不可取的,由於:

那爲何鏈接一個客戶端要建立兩個進程呢?先看一下系統邏輯結構:

從中能夠發現,服務進程是直接跟客戶端進行通信,而nobody進程並無,它僅僅是跟服務進程通訊,來協助服務進程來創建數據鏈接通道,以及須要一些特珠權限的控制,好比服務進程創建了鏈接以後,假設是PORT模式,因爲是服務器端主動鏈接客戶端,服務器端須要綁定20端口來鏈接客戶端,而服務進程是沒有權限來綁定20端口的,也就意味着沒辦法正常創建數據鏈接通道,因此須要加入nobody進程。而nobody和服務進程是採用內部通訊的協議,這個協議對外是不可見的,徹底能夠由咱們本身來定義,因此能夠用UNIX域協議來進行通信,而不用TCP/IP協議了。

功能實現

#ifndef LINUX_FTP_COMMON_H
#define LINUX_FTP_COMMON_H

#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>


#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define ERR_EXIT(m) \
  do \
  { \
    perror(m); \
    exit(EXIT_FAILURE); \
  } \
  while (0)


#endif //LINUX_FTP_COMMON_H
複製代碼
#ifndef LINUX_FTP_SYSUTIL_H
#define LINUX_FTP_SYSUTIL_H

#include "common.h"

int tcp_server(const char *host, unsigned short port);

int getlocalip(char *ip);

void activate_nonblock(int fd);
void deactivate_nonblock(int fd);

int read_timeout(int fd, unsigned int wait_seconds);
int write_timeout(int fd, unsigned int wait_seconds);
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds);
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds);

ssize_t readn(int fd, void *buf, size_t count);
ssize_t writen(int fd, const void *buf, size_t count);
ssize_t recv_peek(int sockfd, void *buf, size_t len);
ssize_t readline(int sockfd, void *buf, size_t maxline);

void send_fd(int sock_fd, int fd);
int recv_fd(const int sock_fd);

#endif //LINUX_FTP_SYSUTIL_H
複製代碼
//
// Created by zpw on 2019-06-08.
//

#include "sysutil.h"

/**
 * tcp_server - 啓動TCP服務器
 * @param host 服務器IP地址或者服務器主機名
 * @param port 服務器端口
 * @return 成功返回監聽套接字
 */
int tcp_server(const char *host, unsigned short port) {
    //建立套接字
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
        ERR_EXIT("socket");
    }

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    if (host != NULL) {
        if (inet_aton(host, &servaddr.sin_addr) == 0) {
            //證實傳過來的是主機名而不是點分十進制的IP地址,接下來要進行轉換
            struct hostent *hp;
            hp = gethostbyname(host);
            if (hp == NULL) {
                ERR_EXIT("gethostbyname");
            }
            servaddr.sin_addr = *(struct in_addr *) hp->h_addr;
        }
    } else {
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    }
    servaddr.sin_port = htons(port);//端口號

    //設置地址重複利用
    int on = 1;
    if ((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const char *) &on, sizeof(on))) < 0) {
        ERR_EXIT("gethostbyname");
    }

    //綁定
    if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
        ERR_EXIT("bind");
    }

    //監聽
    if (listen(listenfd, SOMAXCONN) < 0) {
        ERR_EXIT("listen");
    }


    return listenfd;
}

int getlocalip(char *ip) {
    char host[100] = {0};
    if (gethostname(host, sizeof(host)) < 0) {
        return -1;
    }

    struct hostent *hp;
    if ((hp = gethostbyname(host)) == NULL) {
        return -1;
    }

    strcpy(ip, inet_ntoa(*(struct in_addr *) hp->h_addr));
    return 0;
}

/**
 * activate_noblock - 設置I/O爲非阻塞模式
 * @fd: 文件描符符
 */
void activate_nonblock(int fd) {
    int ret;
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        ERR_EXIT("fcntl");
    }

    flags |= O_NONBLOCK;
    ret = fcntl(fd, F_SETFL, flags);
    if (ret == -1) {
        ERR_EXIT("fcntl");
    }
}

/**
 * deactivate_nonblock - 設置I/O爲阻塞模式
 * @fd: 文件描符符
 */
void deactivate_nonblock(int fd) {
    int ret;
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        ERR_EXIT("fcntl");
    }

    flags &= ~O_NONBLOCK;
    ret = fcntl(fd, F_SETFL, flags);
    if (ret == -1) {
        ERR_EXIT("fcntl");
    }
}

/**
 * read_timeout - 讀超時檢測函數,不含讀操做
 * @fd: 文件描述符
 * @wait_seconds: 等待超時秒數,若是爲0表示不檢測超時
 * 成功(未超時)返回0,失敗返回-1,超時返回-1而且errno = ETIMEDOUT
 */
int read_timeout(int fd, unsigned int wait_seconds) {
    int ret;
    if (wait_seconds > 0) {
        fd_set read_fdset;
        struct timeval timeout;
        FD_ZERO(&read_fdset);
        FD_SET(fd, &read_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do {
            ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == 0) {
            ret = -1;
            errno = ETIMEDOUT;
        } else if (ret == 1) {
            ret = 0;
        }
    }

    return ret;
}

/**
 * write_timeout - 讀超時檢測函數,不含寫操做
 * @fd: 文件描述符
 * @wait_seconds: 等待超時秒數,若是爲0表示不檢測超時
 * 成功(未超時)返回0,失敗返回-1,超時返回-1而且errno = ETIMEDOUT
 */
int write_timeout(int fd, unsigned int wait_seconds) {
    int ret;
    if (wait_seconds > 0) {
        fd_set write_fdset;
        struct timeval timeout;
        FD_ZERO(&write_fdset);
        FD_SET(fd, &write_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do {
            ret = select(fd + 1, NULL, NULL, &write_fdset, &timeout);
        } while (ret < 0 && errno == EINTR);
        if (ret == 0) {
            ret = -1;
            errno = ETIMEDOUT;
        } else if (ret == 1) {
            ret = 0;
        }
    }
    return ret;
}

/**
 * accept_timeout - 帶超時的accept
 * @fd: 套接字
 * @addr: 輸出參數,返回對方地址
 * @wait_seconds: 等待超時秒數,若是爲0表示正常模式
 * 成功(未超時)返回已鏈接套接字,超時返回-1而且errno = ETIMEDOUT
 */
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) {
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);
    if (wait_seconds > 0) {
        fd_set accept_fdset;
        struct timeval timeout;
        FD_ZERO(&accept_fdset);
        FD_SET(fd, &accept_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do {
            ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == -1) {
            return -1;
        } else if (ret == 0) {
            errno = ETIMEDOUT;
            return -1;
        }
    }

    if (addr != NULL) {
        ret = accept(fd, (struct sockaddr *) addr, &addrlen);
    } else {
        ret = accept(fd, NULL, NULL);
    }
    return ret;
}

/**
 * connect_timeout - connect
 * @fd: 套接字
 * @addr: 要鏈接的對方地址
 * @wait_seconds: 等待超時秒數,若是爲0表示正常模式
 * 成功(未超時)返回0,失敗返回-1,超時返回-1而且errno = ETIMEDOUT
 */
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) {
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0) {
        activate_nonblock(fd);
    }

    ret = connect(fd, (struct sockaddr *) addr, addrlen);

    if (ret < 0 && errno == EINPROGRESS) {
        fd_set connect_fdset;
        struct timeval timeout;
        FD_ZERO(&connect_fdset);
        FD_SET(fd, &connect_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do {
            ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == 0) {
            ret = -1;
            errno = ETIMEDOUT;
        } else if (ret < 0) {
            return -1;
        } else if (ret == 1) {
            /* ret返回爲1,可能有兩種狀況,一種是鏈接創建成功,一種是套接字產生錯誤,*/
            /* 此時錯誤信息不會保存至errno變量中,所以,須要調用getsockopt來獲取。 */
            int err;
            socklen_t socklen = sizeof(err);
            int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);
            if (sockoptret == -1) {
                return -1;
            }
            if (err == 0) {
                ret = 0;
            } else {
                errno = err;
                ret = -1;
            }
        }
    }

    if (wait_seconds > 0) {
        deactivate_nonblock(fd);
    }

    return ret;
}

/**
 * readn - 讀取固定字節數
 * @fd: 文件描述符
 * @buf: 接收緩衝區
 * @count: 要讀取的字節數
 * 成功返回count,失敗返回-1,讀到EOF返回<count
 */
ssize_t readn(int fd, void *buf, size_t count) {
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char *) buf;

    while (nleft > 0) {
        if ((nread = read(fd, bufp, nleft)) < 0) {
            if (errno == EINTR)
                continue;
            return -1;
        } else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

/**
 * writen - 發送固定字節數
 * @fd: 文件描述符
 * @buf: 發送緩衝區
 * @count: 要讀取的字節數
 * 成功返回count,失敗返回-1
 */
ssize_t writen(int fd, const void *buf, size_t count) {
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char *) buf;

    while (nleft > 0) {
        if ((nwritten = write(fd, bufp, nleft)) < 0) {
            if (errno == EINTR)
                continue;
            return -1;
        } else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

/**
 * recv_peek - 僅僅查看套接字緩衝區數據,但不移除數據
 * @sockfd: 套接字
 * @buf: 接收緩衝區
 * @len: 長度
 * 成功返回>=0,失敗返回-1
 */
ssize_t recv_peek(int sockfd, void *buf, size_t len) {
    while (1) {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

/**
 * readline - 按行讀取數據
 * @sockfd: 套接字
 * @buf: 接收緩衝區
 * @maxline: 每行最大長度
 * 成功返回>=0,失敗返回-1
 */
ssize_t readline(int sockfd, void *buf, size_t maxline) {
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    while (1) {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0) {
            return ret;
        } else if (ret == 0) {
            return ret;
        }

        nread = ret;
        int i;
        for (i = 0; i < nread; i++) {
            if (bufp[i] == '\n') {
                ret = readn(sockfd, bufp, i + 1);
                if (ret != i + 1)
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft) {
            exit(EXIT_FAILURE);
        }

        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread) {
            exit(EXIT_FAILURE);
        }

        bufp += nread;
    }

    return -1;
}

void send_fd(int sock_fd, int fd) {
    int ret;
    struct msghdr msg;
    struct cmsghdr *p_cmsg;
    struct iovec vec;
    char cmsgbuf[CMSG_SPACE(sizeof(fd))];
    int *p_fds;
    char sendchar = 0;
    msg.msg_control = cmsgbuf;
    msg.msg_controllen = sizeof(cmsgbuf);
    p_cmsg = CMSG_FIRSTHDR(&msg);
    p_cmsg->cmsg_level = SOL_SOCKET;
    p_cmsg->cmsg_type = SCM_RIGHTS;
    p_cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
    p_fds = (int *) CMSG_DATA(p_cmsg);
    *p_fds = fd;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = &vec;
    msg.msg_iovlen = 1;
    msg.msg_flags = 0;

    vec.iov_base = &sendchar;
    vec.iov_len = sizeof(sendchar);
    ret = sendmsg(sock_fd, &msg, 0);
    if (ret != 1)
        ERR_EXIT("sendmsg");
}

int recv_fd(const int sock_fd) {
    int ret;
    struct msghdr msg;
    char recvchar;
    struct iovec vec;
    int recv_fd;
    char cmsgbuf[CMSG_SPACE(sizeof(recv_fd))];
    struct cmsghdr *p_cmsg;
    int *p_fd;
    vec.iov_base = &recvchar;
    vec.iov_len = sizeof(recvchar);
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = &vec;
    msg.msg_iovlen = 1;
    msg.msg_control = cmsgbuf;
    msg.msg_controllen = sizeof(cmsgbuf);
    msg.msg_flags = 0;

    p_fd = (int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
    *p_fd = -1;
    ret = recvmsg(sock_fd, &msg, 0);
    if (ret != 1)
        ERR_EXIT("recvmsg");

    p_cmsg = CMSG_FIRSTHDR(&msg);
    if (p_cmsg == NULL)
        ERR_EXIT("no passed fd");


    p_fd = (int *) CMSG_DATA(p_cmsg);
    recv_fd = *p_fd;
    if (recv_fd == -1)
        ERR_EXIT("no passed fd");

    return recv_fd;
}
複製代碼

編寫好這個函數以後,則在main函數中去調用一下:

接着則要編寫接受客戶端的鏈接:

#ifndef _SESSION_H_
#define _SESSION_H_

#include "common.h"

void begin_session(int conn);

#endif /* _SESSION_H_ */
複製代碼
#include "common.h"
#include "session.h"

void begin_session(int conn)
{
}
複製代碼

而根據上次介紹的邏輯結構來看:

因此須要建立兩個進程:

而後再把這兩個進程作的事也模塊化,FTP服務進程主要是處理FTP協議相關的一些細節,模塊能夠叫ftpproto,而nobody進程主要是協助FTP服務進程,只對內,模塊能夠叫privparent。

因此這裏須要創建一個通道來讓兩進程之間能夠相互通訊,這裏採用socketpair來進行通訊:

另外能夠定義一個session結構體來表明一個會話,裏面包含多個信息:

#ifndef _SESSION_H_
#define _SESSION_H_

#include "common.h"

typedef struct session
{
    // 控制鏈接
    int ctrl_fd;
    char cmdline[MAX_COMMAND_LINE];
    char cmd[MAX_COMMAND];
    char arg[MAX_ARG];

    // 父子進程通道
    int parent_fd;
    int child_fd;
} session_t;
void begin_session(session_t *sess);

#endif /* _SESSION_H_ */
複製代碼

上面用到了三個宏,也須要在common.h中進行定義:

這時在main中就得聲明一下該session,並將其傳遞:

這時再回到begin_session方法中,進一步帶到父子進程中去處理:

下面則在session的父子進程中進行函數的聲明:

ftpproto.h:

#ifndef _FTP_PROTO_H_
#define _FTP_PROTO_H_

#include "session.h"

void handle_child(session_t *sess);

#endif /* _FTP_PROTO_H_ */
複製代碼

ftpproto.c:

#include "ftpproto.h"
#include "sysutil.h"

void handle_child(session_t *sess)
{
    
}
複製代碼

privparent.h:

#ifndef _PRIV_PARENT_H_
#define _PRIV_PARENT_H_

#include "session.h"
void handle_parent(session_t *sess);

#endif /* _PRIV_PARENT_H_ */
複製代碼

privparent.c:

#include "privparent.h"

void handle_parent(session_t *sess)
{
    
}
複製代碼

在session.c中須要包含這兩個頭文件:

接下來咱們將注意力集中在begin_session函數中,首先咱們須要將父進程改爲nobody進程,怎麼來改呢?這裏須要用到一個函數:

下面來編寫handle_child()和handle_parent():

另外在鏈接時,會給客戶端一句這樣的提示語:

主要仍是將經歷投射到handle_child()服務進程上來,其它的先不用關心:

而它主要是完成FTP協議相關的功能,因此它的實現放在了ftpproto.c,目前鏈接成功以後效果是:

其中"USER webor2006"後面是包含"\r\n"的,FTP的協議規定每條指令後面都要包含它,這時handle_child()函數就會收到這個命令並處理,再進行客戶端的一些應答,客戶端纔可以進行下一步的動做,因爲目前尚未處理該命令,因此客戶端阻塞了,接下來讀取該指令來打印一下:

接下來命令中的\r\n,接下來的操做會涉及到一些字符串的處理,因此先來對其進行封裝一下,具體字符串的處理函數以下:

str.h:

#ifndef _STR_H_
#define _STR_H_

void str_trim_crlf(char *str);
void str_split(const char *str , char *left, char *right, char c);
int str_all_space(const char *str);
void str_upper(char *str);
long long str_to_longlong(const char *str);
unsigned int str_octal_to_uint(const char *str);


#endif /* _STR_H_ */
複製代碼

str.c:

#include "str.h"
#include "common.h"

void str_trim_crlf(char *str)
{

}

void str_split(const char *str , char *left, char *right, char c)
{

}

int str_all_space(const char *str)
{
    return 1;
}

void str_upper(char *str)
{
}

long long str_to_longlong(const char *str)
{
    return 0;
}

unsigned int str_octal_to_uint(const char *str)
{
    unsigned int result = 0;
    return 0;
}
複製代碼

①:去除字符串\r\n:rhstr_trim_crlf()

實現思路:

void str_trim_crlf(char *str)
{
    char *p = &str[strlen(str)-1];
    while (*p == '\r' || *p == '\n')
        *p-- = '\0';
}
複製代碼

②:解析FTP命令與參數:str_split()

接下來將命令進行分割:

void str_split(const char *str , char *left, char *right, char c)
{
    //首先查找要分割字符串中首次出現字符的位置
    char *p = strchr(str, c);
    if (p == NULL)
        strcpy(left, str);//表示沒有找到,該命令沒有參數,則將一整串拷貝到left中
    else
    {//表示找到了,該命令有參數
        strncpy(left, str, p-str);
        strcpy(right, p+1);
    }
}
複製代碼

③:判斷全部的字符是否爲空白字符:str_all_space()

④:將字符串轉換成大寫:str_upper()

其實這個錯誤是一個很好檢驗C語言基本功的,修改程序以下:

⑤:將字符串轉換爲長長整型:str_to_longlong()

可能會想到atoi系統函數能夠實現,可是它返回的是一個整型:

可是也有一個現成的函數能夠作到:atoll:

long long str_to_longlong(const char *str)
{
    return atoll(str);
}
複製代碼

可是不是全部的系統都支持它,所以這裏咱們本身來實現,其實現思路也比較簡單,規則以下:

12345678=8*(10^0) + 7*(10^1) + 6*(10^2) + ..... + 1*(10^7)

因此實現以下:

⑥:將八進制的整形字符串轉換成無符號整型str_octal_to_uint()

其實現原理跟上面的差很少:

123456745=5*(8^0) + 4*(8^1) + 7*(8^2) + .... + 1*(8^8)

代碼編寫也跟上面函數同樣,這裏採用另一種方式來實現,從高位算起:

先拿10進制來進行說明,好理解:

123456745能夠通過下面這個換算獲得:

0*10+1=1

1*10+2=12

12*10+3=123

123*10+4=1234

....

因此換算成八進制,其原理就是這樣:

0*8+1=1

1*8+2=12

12*8+3=123

123*8+4=1234

....

因此依照這個原理就能夠進行實現了,因爲八進制可能前面爲0,如:0123450,因此須要把第一位0給過濾掉,以下:

而公式裏面應該是result8+digit來進行計算,這裏用位操做來改寫,也就是result8=result <<= 3,移位操做效率更加高效,因此最終代碼以下:

上一次對字符串工具模塊進行了封裝,此次主要是對"參數配置模塊"的封裝,FTP中有不少配置相關的選項,不可能硬編碼到代碼中,而應該將它們配置到配置文件當中,像vsftpd的配置文件以下:

而對於miniftpd全部的參數配置項以下:

對於上面這些變量應該是與對應的配置項進行一一對應的,因此須要定義三張表格來進行一一對應:

下面定義兩個操做配置文件的函數:

下面則開始進行編碼,首先先新建配置文件模塊文: tunable.h:對其變量進行聲明:

#ifndef _TUNABLE_H_
#define _TUNABLE_H_

extern int tunable_pasv_enable;
extern int tunable_port_enable;
extern unsigned int tunable_listen_port;
extern unsigned int tunable_max_clients;
extern unsigned int tunable_max_per_ip;
extern unsigned int tunable_accept_timeout;
extern unsigned int tunable_connect_timeout;
extern unsigned int tunable_idle_session_timeout;
extern unsigned int tunable_data_connection_timeout;
extern unsigned int tunable_local_umask;
extern unsigned int tunable_upload_max_rate;
extern unsigned int tunable_download_max_rate;
extern const char *tunable_listen_address;


#endif /* _TUNABLE_H_ */
複製代碼

另外新建一個配置文件:

接下來還要暴露兩個接口出來,對文件和配置項的解析:

parseconf.h:

#ifndef _PARSE_CONF_H_
#define _PARSE_CONF_H_

void parseconf_load_file(const char *path);
void parseconf_load_setting(const char *setting);

#endif /* _PARSE_CONF_H_ */
複製代碼

parseconf.c:

#include "parseconf.h"
#include "common.h"
#include "tunable.h"

void parseconf_load_file(const char *path){
    
}

void parseconf_load_setting(const char *setting){
    
}
複製代碼

另外,因爲fgets函數讀取的一行字符包含'\n',因此須要將其去掉,能夠用咱們以前封裝的現成方法:

接下來實現命令行的解析函數,在正式解析以前,須要將配置文件中的配置項與配置項變量對應關係表用代碼定義出來,以下:

#include "parseconf.h"
#include "common.h"
#include "tunable.h"

static struct parseconf_bool_setting
{
  const char *p_setting_name;
  int *p_variable;
}
parseconf_bool_array[] =
{
    { "pasv_enable", &tunable_pasv_enable },
    { "port_enable", &tunable_port_enable },
    { NULL, NULL }
};

static struct parseconf_uint_setting
{
    const char *p_setting_name;
    unsigned int *p_variable;
}
parseconf_uint_array[] =
{
    { "listen_port", &tunable_listen_port },
    { "max_clients", &tunable_max_clients },
    { "max_per_ip", &tunable_max_per_ip },
    { "accept_timeout", &tunable_accept_timeout },
    { "connect_timeout", &tunable_connect_timeout },
    { "idle_session_timeout", &tunable_idle_session_timeout },
    { "data_connection_timeout", &tunable_data_connection_timeout },
    { "local_umask", &tunable_local_umask },
    { "upload_max_rate", &tunable_upload_max_rate },
    { "download_max_rate", &tunable_download_max_rate },
    { NULL, NULL }
};

static struct parseconf_str_setting
{
    const char *p_setting_name;
    const char **p_variable;
}
parseconf_str_array[] =
{
    { "listen_address", &tunable_listen_address },
    { NULL, NULL }
};

void parseconf_load_file(const char *path){
    FILE *fp = fopen(path, "r");
    if (fp == NULL)
        ERR_EXIT("fopen");

    char setting_line[1024] = {0};
    while (fgets(setting_line, sizeof(setting_line), fp) != NULL)
    {
        if (strlen(setting_line) == 0
            || setting_line[0] == '#'
            || str_all_space(setting_line))
            continue;

        str_trim_crlf(setting_line);
        parseconf_load_setting(setting_line);
        memset(setting_line, 0, sizeof(setting_line));
    }

    fclose(fp);
}

void parseconf_load_setting(const char *setting){
    
}
複製代碼

可見有三種類型的參數,下面一個個來進行解析,對於"pasv_enable=YES"一個配置,可能會寫成「 pasv_enable=YES」,因此先去掉左控格:

而後須要將key=pasv_enable;value=YES分隔開,這裏能夠用以前封裝的現成的命令:

但也有可能用戶沒有配置value,如「pasv_enable=」,因此這是不合法的,也應該作下判斷:

接下來,就須要拿這個key在上面的配置表格變量中進行搜索,若是找到了,則將其值賦值給該配置變量,以下:

若是說沒有找到話,也就說明當前的配置項不是字符串類型的,這時,還得繼續去其它類型的配置項中進行搜尋,以下:

而對於布爾類型,能夠有如下幾種形式:

AA=YES

AA=yes

AA=TRUE

AA=1

因此,首先將value統一成大寫:

當遍歷boolean類型配置項中也沒有找到時,則須要在無符號整形中進行查找,其中無符號整形有兩種形式:一種八進制,以0開頭,好比"local_umask=077";另外一種是十進制,如:"listen_port=21",因此須要作下判斷,代碼基本相似:

接下來能夠應用某些配置項了:

可見這樣代碼就變成可配置的了,另外配置文件的文件名能夠作成宏:

這節來實現用戶登陸的驗證,首先用客戶端來登陸vsftpd來演示登陸的過程:

接下來實現它,與協議相關的模塊都是在ftpproto.c中完成的,目前的代碼以下:

#include "ftpproto.h"
#include "sysutil.h"
#include "str.h"

void do_user(session_t *sess);
void do_pass(session_t *sess);

void handle_child(session_t *sess)
{
    writen(sess->ctrl_fd, "220 (miniftpd 0.1)\r\n", strlen("220 (miniftpd 0.1)\r\n"));
    int ret;
    while (1)
    {
        memset(sess->cmdline, 0, sizeof(sess->cmdline));
        memset(sess->cmd, 0, sizeof(sess->cmd));
        memset(sess->arg, 0, sizeof(sess->arg));
        ret = readline(sess->ctrl_fd, sess->cmdline, MAX_COMMAND_LINE);
        if (ret == -1)
            ERR_EXIT("readline");
        else if (ret == 0)
            exit(EXIT_SUCCESS);

        printf("cmdline=[%s]\n", sess->cmdline);
        // 去除\r\n
        str_trim_crlf(sess->cmdline);
        printf("cmdline=[%s]\n", sess->cmdline);
        // 解析FTP命令與參數
        str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
        printf("cmd=[%s] arg=[%s]\n", sess->cmd, sess->arg);
        // 將命令轉換爲大寫
        str_upper(sess->cmd);
        // 處理FTP命令
        if (strcmp("USER", sess->cmd) == 0)
        {
            do_user(sess);
        }
        else if (strcmp("PASS", sess->cmd) == 0)
        {
            do_pass(sess);
        }
    }
}

void do_user(session_t *sess)
{
    //USER jjl
}

void do_pass(session_t *sess)
{
    // PASS 123456
}
複製代碼
相關文章
相關標籤/搜索