使用c++实现一个FTP客户端(二)

客户端使用了Windows Socket提供的API,支持上传、下载、删除、查看文件,断点续传,二进制/ASCII模式切换,被动模式切换,记录操作日志等功能。

  代码包含的类如下:

    ①MySocket类,对SOCKET进行了简单的封装

//对winsock SOCKET的封装
 class MySocket
 {
 public:
     MySocket();
     //~MySocket();
     //重载向SOCKET类型转换的运算符
     operator SOCKET() const; 
     //设置地址信息
     void SetAddrInfo(std::string host, int port);
     bool Connect();
     //bool Disconnect();
     bool Create(int af = AF_INET, int type = SOCK_STREAM, int protocol = IPPROTO_TCP);
     bool Close();
     //获取主机ip
     std::string GetHostIP() const; 
     //获取主机端口
     int GetPort() const; 
 private:
     SOCKET sock;
     SOCKADDR_IN addr_in; //记录连接的服务器的地址信息
     bool conn_flag; //判断是否已连接
 };

    ②Record类,存储了客户端与服务器的交互信息的数据结构


//枚举类型,CMD代表命令信息,RES代表响应信息
enum log_type { CMD = 1, RES = 2 };

//与服务器的交互信息
class Record
{
    friend std::ostream & operator<<(std::ostream &os, const Record &rcd);
public:
    Record(log_type t, std::string m);
    Record(const Record &rcd);
    Record & operator=(const Record &rcd);
    //获取信息内容
    std::string GetMsg() const;
private:
    log_type type; //信息类型
    std::string msg;
};

    ③Logger类,负责控制传输端口的发送命令,接收服务器响应,记录、显示操作日志等功能,包含一个Record类的vector,用于存储此次程序运行的信息

class Logger
{
public:
    Logger(const std::string &host, int port);
    ~Logger();
    Logger(const Logger &logger) = delete;
    Logger & operator=(const Logger &logger) = delete;
    //发送命令
    void SendCmd(const std::string &cmd);
    //接收来自服务器的响应
    void RecvResponse();
    //记录信息
    void Log(log_type type, const std::string &cmd);
    //获取最后一条交互信息,用于验证命令是否执行成功
    std::string GetLastLog() const;
    void DisplayLog() const;
private:
    MySocket sock_cmd; //发送接收命令的socket
    std::vector<Record> vec_rcd; //保存此次客户端运行的交互信息
    //将信息记录到文本文件中
    void WriteRecord();
};

    ④File类,用于存储文件信息的数据结构

class File
{
    friend std::ostream & operator<<(std::ostream &os, const File &file);
public:
    //斜杠代表根目录
    File(const std::string &n = "", const std::string &t = "", const int &s = 0, const std::string &p = "/");
    int GetSize() const;
private:
    std::string name;
    std::string path;
    std::string create_time;
    int size;
};

    ⑤FTPClient类,代码的核心类

class FTPClient
{
public:
    FTPClient(const string &host, int port);
    bool Login(const string &usr, const string &pwd);
    //进入被动模式
    bool EnterPasvMode();
    //更新文件列表
    void UpdateFileList();
    //获取指定文件信息
    File GetFileInfo(const string &f);
    void DisplayLog() const;
    //以二进制格式下载文件
    bool DownloadBinary(const string &f);
    //以ASCII格式下载文件
    bool DownloadASCII(const string &f);
    //上传文件
    bool Upload(const string &f, bool binary);
    //删除指定文件
    bool Delete(const string &f);
    //退出客户端
    bool Quit();
private:
    Logger logger;
    MySocket sock_data; //用于传输数据的socket
    string host;
    int port;
    //
    void GetFileList();
    bool EnterASCIIMode(); //进入ASCII模式
    bool EnterBinaryMode(); //进入二进制模式
};

一、gethostbyname(),inet_ntoa()等函数已经过时

    使用上面两个函数时编译器会报错并提示函数已经是过时的了(obsolete),应该用getaddrinfo()与InetNtop()代替,这两个函数都是协议无关的,同时支持IPv4和IPv6,下面是一个使用例子:

 string GetIPAddress(int af)
 {
     char host_name[IP_SIZE];
     char buf_ip[IP_SIZE];
     //
     addrinfo hints;
     memset(&hints, 0, sizeof(addrinfo));
     hints.ai_family = af;
     hints.ai_socktype = SOCK_STREAM;
     hints.ai_protocol = IPPROTO_TCP;
     //
     addrinfo *result = nullptr;
     //获取主机名字
     int ret_val = ::gethostname(host_name, IP_SIZE);
     if (ret_val == SOCKET_ERROR)
     {
         cerr << "Failed to get host name!\n";
         return "";
     }
     //通过主机名字获取ip地址
     ret_val = ::getaddrinfo(host_name, nullptr, &hints, &result);
     if (ret_val != 0)
     {
         cerr << "Failed tp get host by name!\n";
         return "";
     }
     SOCKADDR_IN *addr = (SOCKADDR_IN*)result->ai_addr;
     ::InetNtop(af, &addr->sin_addr, buf_ip, IP_SIZE);
     //释放地址资源
     ::freeaddrinfo(result);
     return (string)buf_ip;
 }

    关于两个函数的典型用法可以参考MSDN:https://msdn.microsoft.com/en-us/library/ms738520(v=vs.85).aspx

                       https://msdn.microsoft.com/en-us/library/cc805843(v=vs.85).aspx

  二、换行符的问题

    c++中如果输出时需要换行可以使用\n,但需要注意的是,在windows中回车换行表示为\r\n,而linux中表示为\n,而这也是FTP协议中二进制模式与ASCII模式的区别之一:ASCII模式会对文件进行转换,将换行符转换为客户端系统的表示方法,而二进制模式则不对文件进行改动。所以在windows环境下,FTP客户端与服务器交互过程中,客户端发送命令时要以\r\n结尾,而接收服务器的多行数据时每行数据的换行符均为\r\n。

  三、被动模式

    在FTP客户端与服务器进行数据传输时,一般使用被动模式,而客户端与服务器的数据连接在每次传输完成后都会关闭,这意味着每次客户端与服务器传输数据前都要先建立数据连接,也就意味着每次都要重新进入被动模式。通过发送PASV命令可以请求进入被动模式,若进入成功,服务器返回一条形如 227 Entering Passive Mode (a,b,c,d,e,f). 的消息,其中a.b.c.d表示服务器的IP地址,通过e,f可计算得到客户端应连接的服务器端口号,计算公式为:端口号=e*256 + f。

  四、断点续传

    当客户端下载文件过程因种种原因中断后,下次启动下载时就要用到断点续传,避免重新下载。

    断点续传的实现步骤如下:

      1.调用Windows API函数CreateFile()打开文件,然后使用GetFileSize()获取已下载的字节数。

      2.从服务器中获取目标文件的字节数,进行比较。

      3.断点续传的开始位置为已下载的字节数加1。

      4.发送命令“REST offset\r\n”,其中offset为计算出来的文件偏移量。

      5.若服务器响应成功,则发送命令“RETR 文件名\r\n”,若响应成功,文件开始断点续传。

    断点续传关键部分代码如下:


 

HANDLE h_file = ::CreateFile(str_path.c_str(), 0, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (h_file == INVALID_HANDLE_VALUE)
{
    return false;
}
int dld_size = ::GetFileSize(h_file, nullptr);
::CloseHandle(h_file);
//
int file_size = GetFileInfo(f).GetSize();
if (file_size == dld_size)
{
    cout << "File already downloaded!\n";
    return false;
}
//
int read_start = dld_size + 1;
file.open(str_path, fstream::out | fstream::app);
//
char buf_num[32];
memset(buf_num, 0, sizeof(buf_num));
_itoa_s(read_start, buf_num, sizeof(buf_num), 10);
string cmd_dld = "REST ";
cmd_dld += buf_num;
cmd_dld += "\r\n";
//
EnterPasvMode();
//
logger.SendCmd(cmd_dld);
logger.RecvResponse();
if (logger.GetLastLog().substr(0, 3) == "500")
{
    cerr << "File name incorrect!\n";
    return false;
}
//
cmd_dld = "RETR " + f + "\r\n";
logger.SendCmd(cmd_dld);
logger.RecvResponse();
//
while (::recv(sock_data, dld_file, FILE_SIZE, 0) != 0)
{
    cout << strlen(dld_file) << "\n";
    file << dld_file;
    memset(dld_file, 0, FILE_SIZE);
}
file.close();
sock_data.Close();

猜你喜欢

转载自blog.csdn.net/weixin_39345003/article/details/81082779