Простейший ftp клиент на С++

В этой статье я расскажу основные принципы построения ftp клиента на языке C++. Для понимания этой статьи вам необходимо иметь начальные представления о том, что такое сокеты. Я расскажу лишь о принципах построения программы.
Прежде всего стоит отметить, что фтп соединение состоит из двух сокетов (один для соединения данных, другой — для управления соединением).
По управляющему соединению передаются команды серверу (обычно это 4-ёх байтовые строки) и считываются ответы, по соединению данных передаются файлы, списки файлов и т.д.

Теперь подключим необходимые библиотеки:

  • Если вы используете linux:
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>

Если вы используете Windows:

#include <winsock2.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>

Кроме того следует подключить (если вы пользуетесь Visual Studio, это делается в свойствах проекта — настройках компановщика) необходимые в Windows lib файлы: Ws2_32.lib и Wsock32.lib

Следующим шагом будет установка управляющего соединения. Создадим для этого функцию init_sock(). Для упрощения кода преобразование доменного имени в IP адрес опустим. Соединение будем устанавливать на локальный ftp сервер (127.0.0.1), на стандартный 21 порт.

int init_sock() {
    int len;
    sockaddr_in address;
    int result;
    int s;
    s = socket(AF_INET, SOCK_STREAM,0);
    address.sin_family = AF_INET;   ///интернет домен
    address.sin_addr.s_addr = inet_addr("127.0.0.1");   ///соединяемся с 127,0,0,1
    address.sin_port = htons(21);    /// 21 порт
    len = sizeof(address);
    result = connect(s, (sockaddr *)&address, len);   ///установка соединения
    if (result == -1) {
        perror("oops: client");
        return -1;
    }
    return s;
}

Итак мы установили управляющее соединение с сервером. Теперь необходимо считать ответ сервера. Для этого воспользуемся функцией select, которая проверяет наличие данных в потоке; recv — для получения данных из потока.
Немного об этих функциях:

int readServ(int s) {
    int rc;
    fd_set fdr;
    FD_ZERO(&fdr);
    FD_SET(s,&fdr);
    timeval timeout;
    timeout.tv_sec = 1;   ///зададим  структуру времени со значением 1 сек
    timeout.tv_usec = 0;  
    do {
        char buff[512] ={' '};
        recv(s,&buff,512,0);   ///получаем данные из потока
        cout << buff;
        rc = select(s+1,&fdr,NULL,NULL,&timeout);    ///ждём данные для чтения в потоке 1 сек. подробнее о select 
    } while(rc);     ///проверяем результат
    return 2;
}

Таким образом вы можете посылать команды на сервер  и считывать его ответ. Имея две эти функции можно построить такую функцию main:

int main() {
   int s;
   s = init_sock();
   readServ(s);
   close(s);  ///закрытие соединения
   return 0;
}

Теперь приступим к созданию соединения данных. Для этого создадим функцию init_data()

int init_data() {
    send(s,"PASV\r\n",strlen("PASV\r\n"),0);
    char buff[128];
    recv(s,buff,128,0);
    cout << buff; ////выводим на экран полученную от сервера строку
    int a,b;
    char *tmp_char;
    tmp_char = strtok(buff,"(");
    tmp_char = strtok(NULL,"(");
    tmp_char = strtok(tmp_char, ")");
    int c,d,e,f;
    sscanf(tmp_char, "%d,%d,%d,%d,%d,%d",&c,&d,&e,&f,&a,&b);
    int len;
    sockaddr_in address;
    int result;
    int port = a*256 + b;
    ds = socket(AF_INET, SOCK_STREAM,0);
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr(addr);    ///addr - у меня глобальная переменная с адресом сервера
    address.sin_port = htons(port);
    len = sizeof(address);
    result = connect(ds, (sockaddr *)&address, len);
    if (result == -1) {
        perror("oops: client");
        return -1;
    }
    return 0;
}

Теперь поговорим об этой функции. Прежде всего она отправляет запрос на пассивное соединение данных. Что это? Это такой вид соединения, когда мы создаём сокет на указанный сервером адрес  и порт. После этого запроса мы получаем ответ сервера — строку, которая содержит адрес и порт — затем выводим её на экран.

Для независимости от языка сервера, разберём строку оставив только выражение в скобках. Для этого будем пользоваться функцией strtok(). Подробнее о ней можно прочитать здесь.
Разобрав строку — считаем из неё переменные адреса и порта в целочисленные переменные при помосчи sscanf().
Теперь вычислим необходимый порт: а*256 + b
Затем всё аналогично управляющему соединению. Оговорюсь лишь о том, что переменная addr, которая используется для хранения адреса — глобальная. Вы можете получить адрес заново использую sprintf() и полученные данные в переменных c,d,e,f.

Для получения возможности загружать файлы на сервере необходимоо залогинится. После инициализации соединения отправим запрос на логин. Делается это примерно так:

int login() {
    cout << "Введите имя: "; char name[64]; cin >> name;
    char str[512];
    sprintf(str,"USER %s\r\n",name);
    send(s,str,strlen(str),0);
    readServ();
    cout << "Введите пароль: "; char pass[64]; cin >> pass;
    sprintf(str,"PASS %s\r\n",pass);
    send(s,str,strlen(str),0);
    readServ();
    return 0;
}

В этой функции мы формируем строку с именем, отправляем её, считываем ответ. После чего проделываем тоже самое с паролем пользователя. Я не предохранял его от чужих глаз… это не важно для понимания структуры ftp.

После того как мы удачно залогинились на сервере отправим запрос на закачку файла:

int get(char *file) {
    char str[512];
    sprintf(str,"RETR %s\r\n",file);
    send(s,str,strlen(str),0);

    /* получение размера файла */
    char size[512];
    recv(s,size,512,0);
    cout << size;

    char *tmp_size;
    tmp_size = strtok(size,"(");
    tmp_size = strtok(NULL,"(");
    tmp_size = strtok(tmp_size, ")");
    tmp_size = strtok(tmp_size, " ");

    int file_size;
    sscanf(tmp_size,"%d",&file_size);
    FILE *f;
    f = fopen(file, "wb");   ///важно чтобы файл писался в бинарном режиме
    int read = 0;  ///изначально прочитано 0 байт
    do {
            char buff[2048];   ////буфе для данных
            int readed = recv(ds,buff,sizeof(buff),0);   ///считываем данные с сервера. из сокета данных
            fwrite(buff,1,readed,f);   ///записываем считанные данные в файл
            read += readed;  ///увеличиваем количество скачанных данных
        } while (read < file_size);
    fclose(f);
    cout << "Готово. Ожидание ответа сервера...\n";
    return 0;
}

Эта довольно громоздкая функция получает в качестве параметра имя загружаемого файла, передаёт запрос на его закачку, из ответа вычисляет при помощи strtok размер файла и пока не загрузится весь файл считывается по 2048 байт и записывается в него.

..уже собственно неочем рассказывать.. вот мой проект
Все функции делаются аналогично..
Ловить ошибки мне лень.. такчто извольте.. Вопросы жду в коментах..

 

Похожий код:

Фото аватара
Алексей Петров

Программист, разработчик с 5 летним опытом работы. Учусь на разработчика игр на Unity и разработчика VR&AR реальности (виртуальной реальности). Основные языки программирования: C#, C++.

Оцените автора
Бла, бла код
Добавить комментарий

  1. lamazavr

    Статья постепенно дополняется.. так что строго не судите..

    Ответить
  2. lamazavr

    добавил архив с моими исходниками..

    Ответить
  3. lamazavr

    Сделай такую статью про сервер, если есть время.
     

    Ответить
  4. lamazavr

    sockaddr_in   что ето за тип даных? он используеться в 3-й строчке листинга после заглавочных файлов.

    Ответить
  5. lamazavr

    Структура sockaddr_in описывает сокет для работы с протоколами IP
        sockaddr_in address;

        address.sin_family = AF_INET;   ///интернет

        address.sin_addr.s_addr = inet_addr(«127.0.0.1»);   ///соединяемся с 127,0,0,1

        address.sin_port = htons(21);    /// 21 порт

    Ответить
  6. lamazavr

    Нет так и не получил…. я уже третий день парюсь… в боланде в дебаге вижу что в переменной в буф есть  а когда выжову их то ничего не выходит

    Ответить
  7. lamazavr

    Я под Linux писал. Там это делать не нужно.
    Данные то получили?

    Ответить
  8. lamazavr

    я пробовал ставить 64 байт буфер. в nsize у меня поподает 21 байт полученных данных. т.е. данные приходят. сервер написан на пхп под линукс а клиент пишу на с++ под винду.  Сейчас буду по вашим исходникам свои подправлять.. В ваших исхдоника мне не понятно по чему вы не делаете инициализацию winsocks типа такого     if (WSAStartup(0x202,(WSADATA *)&buff[0])){

            printf(«WSAStart error %d

    «,WSAGetLastError());

            return 1;

        }
     
     

    Ответить
  9. lamazavr

    можно с вами в icq пообщаться?
     

    Ответить
  10. lamazavr

    если не трудно постучи в аську 271-013-909
     

    Ответить
  11. lamazavr

    Подскажите!

    делаю так:

    char buff[1024];

    nsize=recv(my_sock,buff,64,0);

    Вижу что сервер отвечает. В переменной nsize появляются принятые байты.

    а вот данные из переменной buff считать не могу

    printf(«S=%s»,buff);

    printf(«S=%s»,&buff);

    cout << buff;

    ни чего не показывает (((

    Где ошибаюсь?
     

    Ответить
  12. lamazavr

    Во первых зачем вам буфер на 1024 байта, если вы читаете 64.
    Что попадает в nsize?
    проверку на правельность открытия сокета делали?
    Windows или Linux ?
     

    Ответить