четверг, 28 апреля 2016 г.

Разработка лаунчера для MMORPG 

 Project Genom, часть 2

Юрий “yurembo” Язев
независимый игродел

Сетевая подсистема 


Введение


Для начала, я рекомендую прочитать первую часть обзора, в ней содержится подробный план разработки.
Важным элементом лаунчера является сетевая подсистема. Во второй части обзора разработки лаунчера мы рассмотрим сетевую подсистему лаунчера уже на клиентской стороне.

Имея опыт разработки сетевых приложений с помощью WinINet API, я благополучно начал разработку сетевой части на ее основе. WinINet API — стандартная клиентская сетевая библиотека, разработанная Microsoft для операционных систем семейства Windows.
Также, во время разработки я написал тестовую версию лаунчера, где сетевая подсистема была построена на кроссплатформенной библиотеке libcurl. Она поддерживает практически весь стек протоколов TCP/IP и работает вполне достойно. Однако после многочисленных тестов выяснилось, что она уступает WinINet, главным образом, в скорости работы и в проблеме, указанной ниже.  Поэтому через некоторое время я отказался от ее поддержки, потому что это стало бессмысленным занятием. К тому же, кроссплатформенность была не нужна.
В итоге, финальную версию лаунчера я разработал на основе сетевой библиотеки WinINet.

 Разработка лаунчера


Итак, в начале, при первом запуске лаунчера, после нажатия кнопки «Установить» или, при последующих запусках лаунчера, автоматически вызывается метод OnBnClickedDownloadButton, который находится в файле PGLauncherDlg.cpp, вот, с него и начнем погружение в код:

void CPGLauncherDlg::OnBnClickedDownloadButton()
{
   _CurrentFolder = wide_string(_dirGameInstall, loc);

   showLabels();
   destroyProgressBar();
   buildProgressBar();
  
   _onEnterClicked = false;

   CWnd *windlg = GetForegroundWindow();
   if (windlg == nullptr) return;
   connect.createHTTPConnect();//создание HTTP соединения
   connect.createFTPConnect();//создание FTP соединения
   if (!connect.checkInternetConnect()) {
       global_Download = true;
       return;
   }
   connect.setWin(windlg);

   SetCurrentDirectory(_CurrentFolder.c_str());

   connect.setTotalProgress(m_TotalSkinProgress);
   connect.setPercentText(&m_PercentText);
   connect.setSpeedText(&m_SpeedText);
   wstring wfile = getWideFileName(_infoFileName);
   LPCWSTR file = wfile.c_str();
   if (connect.GetFtpFileToCreate(file, 0, 0)) {
       reader.setWin(windlg);
       reader.setTotalProgress(m_TotalSkinProgress);
       reader.setPercentText(&m_PercentText);
       reader.setSpeedText(&m_SpeedText);
       reader.setUpdateText(m_UpdateText);
       reader.setEnterButton(m_EnterButton);
       thread t(&CFileReader::readLocalFile, &reader, ref(connect));
       t.detach();
   }
}

Первым делом, мы, с помощью функции wide_string, преобразуем путь к папке игры из string в wstring: _CurrentFolder = wide_string(_dirGameInstall, loc); сохраняя его в переменной _CurrentFolder. Все дополнительные функции размещены в файле functions.h.

//функция для преобразования string в wstring
static std::wstring wide_string(std::string const &s, std::locale const &loc)
{
   if (s.empty())
       return std::wstring();
std::ctype<wchar_t> const &facet = std::use_facet<std::ctype<wchar_t> >(loc);
   char const *first = s.c_str();
   char const *last = first + s.size();
   std::vector<wchar_t> result(s.size());

   facet.widen(first, last, &result[0]);

   return std::wstring(result.begin(), result.end());
}

Затем, в методе showLabels делаются видимыми 3 надписи (пользовательский интерфейс мы не будем рассматривать в этой статье). После этого скрывается шкала прогресса - метод destroyProgressBar, затем она конструируется вновь - в методе buildProgressBar. Далее, получаем указатель на окно. После этого, в случае успешного получения указателя, с помощью методов класса CFtpConnection создаются подключения:

connect.createHTTPConnect();//создание HTTP соединения
connect.createFTPConnect();//создание FTP соединения

Далее, с помощью метода checkInternetConnect выполняется проверка подключения к интернету, в случае, если его нет, прекращаем выполнение функции, так как в таком случае ничего сделать нельзя. Посредством метода setWin объекта connect передаем ему указатель на окно приложения, зачем это нужно узнаем позже. Функция SetCurrentDirectory устанавливает для приложения текущую рабочую папку, в данном случае, это каталог с игрой.
После этого, методами setTotalProgress, setPercentText, setSpeedText передаются ссылки на элементы пользовательского интерфейса объекту connect класса CFtpConnect.
Потом, когда имя файла будет преобразовано к типу LPCWSTR, происходит вызов метода GetFtpFileToCreate объекта класса CFtpConnect. Этот метод получает имя файла, который надо скачать, а так же размер файла и общий объем скачивания, если же они оба равны 0, тогда они не берутся во внимание. Если скачивание указанного файла происходит без проблем, метод возвращает true, иначе, false. Когда GetFtpFileToCreate завершается с успехом, выполняется блок кода, в котором инициализируется reader (объект класса CFileReader), для него так же устанавливаются указатели на графические элементы пользовательского интерфейса. Под конец этого блока создается объект класса thread - поток для выполнения функции чтения файла:

thread t(&CFileReader::readLocalFile, &reader, ref(connect));

Конструктор потока принимает: указатель на выполняемую в отдельном потоке функцию, объект, который будет ее выполнять и дополнительный параметр - в данном случае, ссылку на объект connect. Так как, во время чтения файла, выполняются другие функции, в том числе, для скачивания файлов, получается, что это долгая по исполнению функция. Если ее выполнение оставить в главном - фоновом потоке, тогда на протяжении всего времени ее выполнения (которое может затянуться на несколько часов, поскольку клиент игры занимает несколько гигабайтов), пользовательский интерфейс будет не доступен, и наше приложение, в таком случае, будет выглядеть, как зависшее. Тем не менее, выделив выполнение длительной операции чтения и скачивания файлов с сервера в отдельный поток, мы избавили себя от такого поведения нашего приложения. После того, как выполнение метода readLocalFile будет завершено, можно освободить поток, это выполняется методом detach.
Замечу: здесь, с сервера скачивается текстовый файл, в котором прописан список файлов, которые надо скачать.
Метод OnBnClickedDownloadButton класса CPGLauncherDlg, выполняющий инициализацию процедуры скачивания подошел к концу. Рассмотрим классы CFileReader и CFtpConnect. Объекты этих классов: reader и connect создаются в стеке. В их конструкторах обнуляются объекты, для подготовки к инициализации. Сначала, плотнее подойдем к классу CFtpConnect. В первую очередь, чтобы объект этого класса мог что-то сделать, надо создать соединение. Как вы уже знаете, класс CFtpConnect может работать с двумя протоколами: FTP и HTTP. Для создания соединения посредством первого вызывается метод createFTPConnect:

void CFtpConnect::createFTPConnect()
{
   wstring stemp = wide_string(_appName, loc);
   LPCWSTR sw = stemp.c_str();
   hFTPInternet = InternetOpen(sw, INTERNET_OPEN_TYPE_PROXY, TEXT("proxy"), TEXT("test"), 0);
   if (!hFTPInternet) {
   //InternetErrorOut(hWin->GetSafeHwnd(), GetLastError(), 
//TEXT("Connect Failed"));
        return;
   }
   if (hFTPInternet) {
      wstring sip = wide_string(_ipAddr, loc);
      LPCWSTR ip = sip.c_str();
      wstring sun = wide_string(_userName, loc);
      LPCWSTR un = sun.c_str();
      wstring sup = wide_string(_userPassword, loc);
      LPCWSTR up = sup.c_str();
      hFTPConnect = InternetConnect(hFTPInternet, ip, INTERNET_DEFAULT_FTP_PORT, un, up, INTERNET_SERVICE_FTP, INTERNET_FLAG_PASSIVE, 0);
      /*FTP соединения бывают 2-х видов: пассивные и активные, в данной функции в качестве 7-го параметра надо использовать флаг INTERNET_FLAG_PASSIVE, то есть включить пассивное соединение, по тестам оно работает на активных и пассивных клиентах;
если же не использовать флаг (поставить 0 в качестве параметра), будет использовано активное соединение, которое работает только на активных клиентах*/
      if (!hFTPConnect) {                                               //InternetErrorOut(hWin->GetSafeHwnd(), GetLastError(), TEXT("FTP //Failed"));
          return;
      }
    }

    return;
}

Функция InternetOpen инициализирует функциональность WinINet. На выходе она выдает указатель, который понадобиться для последующих вызовов функций библиотеки WinINet. Поэтому, если данная функция возвращает NULL, дальнейшее выполнение не имеется смысла. Далее, параметры для подключения преобразуются в широкие символы. А, с помощью функции InternetConnect происходит открытие сессии подключения по FTP или HTTP протоколам, в зависимости от параметров. 1-й параметр - указатель на WinINet, 2-й - ip-адрес, 3-й - номер порта для передачи сообщений, 4-й - имя пользователя, 5-й пароль, 6 - протокол (сервис) доступа, 7-й параметр - определяет использование типа FTP-протокола: активный или пассивный, 8-й - дополнительные флаги, обычно 0. Результат выполнение функции - указатель на открытую сессию сохраняется в переменной hFTPConnect. При этом обратите внимание на использование 7-го параметра: FTP соединения бывают 2-х видов: пассивные и активные, в данной функции в качестве 7-го параметра надо использовать флаг INTERNET_FLAG_PASSIVE,      то есть включить пассивное соединение, по тестам оно работает на активных и пассивных клиентах; если же не использовать флаг (поставить 0 в качестве параметра), будет использовано активное соединение, которое работает только на активных клиентах.
Обратите внимание: hFTPInternet и hFTPConnect являются глобальными объектами класса.

Для закрытия FTP-соединения используется метод closeFTPConnect:

void CFtpConnect::closeFTPConnect()
{
   InternetCloseHandle(hFTPConnect);
   InternetCloseHandle(hFTPInternet);
}

Для создания HTTP-соединения используется метод createHTTPConnect:

void CFtpConnect::createHTTPConnect()
{
   _TotalSkinProgress = nullptr;
   _percentText = nullptr;
   _speedText = nullptr;
   lastTime = 0;
   lastDownload = 0;
  
   wstring stemp = wide_string(_appName, loc);
   LPCWSTR sw = stemp.c_str();
   hHTTPInternet = InternetOpen(sw, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
   if (!hHTTPInternet) {
   //InternetErrorOut(hWin->GetSafeHwnd(), GetLastError(), //TEXT("Connect Failed"));
      return;
   }
   if (hHTTPInternet) {
      wstring sip = wide_string(_domen_name, loc);
      LPCWSTR ip = sip.c_str();
      wstring sun = wide_string(_userName, loc);
      LPCWSTR un = sun.c_str();
      wstring sup = wide_string(_userPassword, loc);
      LPCWSTR up = sup.c_str();
      hHTTPConnect = InternetConnect(hHTTPInternet, ip, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 1u);
      if (!hHTTPConnect) {
         //InternetErrorOut(hWin->GetSafeHwnd(), GetLastError(),
 //TEXT("FTP Failed"));
         return;
      }
   }

   return;
}

Методы различаются только параметрами функций:
hHTTPInternet = InternetOpen(sw, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
Вторым параметром передается INTERNET_OPEN_TYPE_PRECONFIG, он означает, что информация о конфигурации берется непосредственно из реестра.
hHTTPConnect = InternetConnect(hHTTPInternet, ip, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 1u);
Поменялись 3-й и 6-й параметры: INTERNET_DEFAULT_HTTP_PORT - использование стандартного порта для http, INTERNET_SERVICE_HTTP - сервис (протокол) http.

Закрывается http-подключение подобным FTP образом:

void CFtpConnect::closeHTTPConnect()
{
   InternetCloseHandle(hHTTPConnect);
   InternetCloseHandle(hHTTPInternet);
}

Для проверки подключения к интернету, как было сказано выше, используется метод checkInternetConnect:

BOOL CFtpConnect::checkInternetConnect()
{
   BOOL res = InternetCheckConnection(_T("http://www.microsoft.com"), FLAG_ICC_FORCE_CONNECTION, 0);
   return res;
}

Внутри метода, с помощью функции InternetCheckConnection, приложение пытается осуществить связь с сайтом, указанном в первом параметре, и, если подключение успешно, тогда возвращается true, иначе - false.

Следующий метод, который мы рассмотрим - это GetFtpFileToCreate, используемый для скачивания текстового файла с сервера:

bool WINAPI CFtpConnect::GetFtpFileToCreate(LPCWSTR file, DWORDLONG fileSize, DWORDLONG totalSize, bool isTxt)
{
   LPCWSTR mainFile = file;

   string s = "\\";
   string t = "/";
   string str = narrow(file);

   string fn = narrow(file);
   string dn;
   getFileNameAndDirName(fn, dn);
   wstring wfn = wide_string(fn, loc);
   file = wfn.c_str();

   replace(str.begin(), str.end(), '\\', '/');
   wstring wstr = wide_string(str, loc);
   LPCWSTR            rfile = wstr.c_str();

   wstring wstr2 = getFileNameWithoutDomen(file, _ftp_folder);
   str = narrow(wstr2);
   replace(str.begin(), str.end(), '/', '\\');
   wstr2 = wide_string(str, loc);
   file = wstr2.c_str();

   //используется для прокачивания загрузки!
   unsigned long iBytesRead;
   unsigned long currentFileSize = 0;//скачивание текущего файла

   HINTERNET FTPRequestDownload = FtpOpenFile(hFTPConnect, rfile, GENERIC_READ, INTERNET_FLAG_TRANSFER_BINARY | INTERNET_FLAG_DONT_CACHE | INTERNET_FLAG_RELOAD, 0);

   if (FTPRequestDownload == NULL) {
//MessageBox(hWin->GetSafeHwnd(), file, TEXT("Файл не найден"), //0);
//InternetErrorOut(hWin->GetSafeHwnd(), GetLastError(), 
//TEXT("FtpOpenFile"));
     InternetCloseHandle(FTPRequestDownload);
     return false;
   }

   if (FTPRequestDownload)
   {
      DWORD dwContentLength;
     //unsigned long iBytesRead; --- объявление перенесено выше!
      BYTE      Buffer[8192];
      ZeroMemory(Buffer, sizeof(Buffer));
      dwContentLength = 4 * 1024;

      FILE *pFile = _tfopen(file, TEXT("wb"));
      if (pFile == NULL)
        return false;
      do
        {
          if (InternetReadFile(FTPRequestDownload, Buffer, dwContentLength, &iBytesRead)) {
               currentFileSize += iBytesRead;
              //выводим шкалу общего скачивания (new)
              if (_TotalSkinProgress && totalSize > 0) {
              showPercentTotalStr(iBytesRead, totalSize);
            }
            fwrite(Buffer, iBytesRead, 1, pFile);
            ZeroMemory(Buffer, sizeof(Buffer));
           }
         } while (iBytesRead != 0);
         fclose(pFile);
     }

   InternetCloseHandle(FTPRequestDownload);

   bool result = false;
   if ((fileSize == 0 && totalSize == 0) || isTxt)
       result = true;
   else
       result = (currentFileSize == fileSize);

   return result;
}

Как я уже говорил, метод получает: имя файла для скачивания, размер файла и общий объем скачивания, плюс, необязательный булевый флаг, показывающий: текстовый это файл или нет.
В начале метода происходит преобразование пути к файлу, а так же извлечение из него имени папки и файла (с помощью функции getFileNameAndDirName). Кроме того, с помощью функции getFileNameWithoutDomen мы получаем путь к файлу на FTP-сервере и имя файла.
Далее, посредством функции FtpOpenFile на сервере для чтения или записи (в зависимости от параметров) открывается файл. Функция принимает следующие параметры: указатель на FTP-соединение, открытое ранее, имя открываемого файла на стороне сервера, флаг режима доступа к файлу: чтение, запись, набор флагов условий передачи данных: INTERNET_FLAG_TRANSFER_BINARY - передача двоичных данных, INTERNET_FLAG_DONT_CACHE - не кэшировать данные, сразу записывать на диск, INTERNET_FLAG_RELOAD - сразу сбрасывать данные на диск - не кэшировать. В случае успеха, функция возвращает не нулевой хэндл.
Следующим действием, в случае, если файл успешно открыт, мы читаем его и скачиваем на клиентский компьютер. Для этого, выделяем буфер размером 8192 байт и очищаем его. Далее, создаем на клиенте файл: FILE *pFile = _tfopen(file, TEXT("wb")); и в цикле начинаем порциями читать файл на сервере и записывать его на клиенте:

do
  {
    if (InternetReadFile(FTPRequestDownload, Buffer, dwContentLength, &iBytesRead))
     {
        currentFileSize += iBytesRead;
      //выводим шкалу общего скачивания (new)
        if (_TotalSkinProgress && totalSize > 0) {
          showPercentTotalStr(iBytesRead, totalSize);
        }
        fwrite(Buffer, iBytesRead, 1, pFile);
        ZeroMemory(Buffer, sizeof(Buffer));
     }
   } while (iBytesRead != 0);
 fclose(pFile);

Функция InternetReadFile получает: ссылку на открытый файл, указатель на буфер для получения данных, количество байт для чтения, указатель на переменную, в которую будет записано количество считанных данных. Если эта функция возвращает true, тогда чтение указанного количества байт завершено успешно, и мы попадаем в тело условного оператора. Здесь, мы увеличиваем переменную currentFileSize на количество скаченных на предыдущем шаге байт, в этой переменной накапливается число байт. Затем, происходит обновление шкалы прогресса. После этого, полученные байты записываются на диск, а буфер очищается. Это в цикле повторяется до тех пор, пока весь файл не будет получен, после чего, он закрывается, а так же закрывается файловый поток. Если размер скачанного файла равен размеру параметра fileSize или, если качался текстовый файл, то возвращается истина, так как в разных системах (Windows, Unix) из-за различия в символах конца строки, размер одного и того же текстового файла будет различный.

Файловая подсистема


После того, как текстовый файл со списком файлов клиента будет успешно закачан, управление возвращается в метод OnBnClickedDownloadButton, откуда, в отдельном потоке для объекта reader класса CFileReader вызывается метод readLocalFile, которому передается ссылка на объект connect класса CFtpConnect:

void CFileReader::readLocalFile(CFtpConnect& connect)
{
   bool reCheck = false;//надо перезапустить скачивание файла
   wstring lpath;
   string s = _infoFileName;
   if (!readTotalFilesSize(s))
      return;
   if (_TotalSkinProgress != nullptr && g_totalSize > 0) {
      int sizeMB = int(g_totalSize / 1024 / 1024);
      if (sizeMB > 0)
      _TotalSkinProgress->SetRange(0, sizeMB, 1);
   }
   ifstream f;
   f.open(s, ios::in);
   if (s != "" && f.is_open()) {
      while (!f.eof()) {
        SetCurrentDirectory(_CurrentFolder.c_str());
        string filePath;
        f >> filePath;
        getWholeStr(f, filePath);//результат во втором параметре
        string fsize;
        f >> fsize;
        string fcrc;
        f >> fcrc;
        // обработка файлового пути
        string fileName = filePath;
        string dirName = "";
        getFileNameAndDirName(fileName, dirName);
        if (filePath == "Files_count:") {
           break;
        }
        //обработка файлового размера
        DWORDLONG dwlsize = stringToDWORDLONG(fsize);
        //обработка контрольной суммы
        DWORDLONG dwlcrc = stringToDWORDLONG(fcrc);
        //обработка каталога и файла
        wstring stemp = wide_string(dirName, loc);
        LPCWSTR dir = stemp.c_str();
        wstring stemp1 = wide_string(fileName, loc);
        LPCWSTR file = stemp1.c_str();
        wstring stemp2 = wide_string(filePath, loc);
        LPCWSTR path = stemp2.c_str();
       if (!fileSearch(dir, file, path, connect, dwlcrc, dwlsize)) {
           reCheck = true;
           break;
        }
       lpath = path;
       //MessageBox(hWin->GetSafeHwnd(), lpath, _T(""), MB_OK);
      }
      f.close();
      if (g_totalSize > connect.getDownSize()) {
// - вывод сообщения: количество байт которое надо скачать и количество скаченных байт
// ULONGLONG global_TotalSize = g_totalSize;
// ULONGLONG global_DownSize = connect.getDownSize();
// string s = to_string(global_TotalSize) + " == " + //to_string(global_DownSize);
// wstring ws = wide_string(s, loc);
// LPCWSTR lp = ws.c_str();
// MessageBox(hWin->GetSafeHwnd(), lpath.c_str(), lp, MB_OK);
//                           
// global_Download = true;//передаем выполнение для другого потока
// }
   if (reCheck) {
     global_Download = true;//передаем выполнение для другого потока
   } else
   if (_TotalSkinProgress != nullptr){// && g_totalSize == connect.getDownSize()) {
     HideLabelsShowButton();
    }
   }
}

В начале функции мы получаем размер всех файлов, эта запись находится в конце текстового файла, скачанного с сервера, вызвав последовательно readTotalFilesSize:

bool CFileReader::readTotalFilesSize(string file)
{
   g_totalSize = getTotalSize(file);
   if (g_totalSize == 0) return false;
   return true;
}

которая вызывает getTotalSize:

DWORDLONG CFileReader::getTotalSize(string file)
{
   DWORDLONG totalSizeNum = 0;
   ifstream f;
   f.open(file, ios::in);

   if (file != "" && f.is_open()) {
      while (!f.eof()) {
        string filePath;
        f >> filePath;
        getWholeStr(f, filePath);//результат во втором параметре
        string fms;
        f >> fms;
        string fsize;
        f >> fsize;
        if (filePath == "Files_count:") {
          string totalSize;
          f >> totalSize;
          totalSizeNum = stringToDWORDLONG(totalSize);
          break;
        }
      }
   }
   else {
      f.close();
      return 0;
   }

   f.close();
   return totalSizeNum;
}

Последняя — просматривает весь файл, пока не наткнется на строчку: “Files_count:”, после которой следует искомое значение. Оно может быть очень большое, ибо исчисляется размер клиента в байтах, поэтому для преобразования строки в число используется функция stringToDWORDLONG:

//функция преобразования string в ULONGLONG
static DWORDLONG stringToDWORDLONG(const string cnum)
{
   return _strtoui64(cnum.c_str(), NULL, 0);
}

Когда размер всех файлов клиента будет получен, управление возвращается в метод readLocalFile, где на основе этого числа вычисляется и устанавливается диапазон для шкалы прогресса скачивания. Затем, текстовый файл открывается и читается по 3 строки: путь к файлу, размер файла и его контрольная сумма. Потом, файловый путь преобразуется, обрабатываются файловый размер и контрольная сумма - они преобразуются из строк в числа с помощью уже рассмотренной функции stringToDWORDLONG. После этого, вызывается метод fileSearch, на основе результата его выполнения (если результат false), скачивание данного файла планируется повторно, так как, в таком случае, в процессе скачивания файла произошла ошибка. Если же закачка файла завершилась успешно, то итерация цикла повторяется: считываются новые данные, закачивается следующий файл. Когда цикл завершается, значит, все файлы закачены, вызывается функция HideLabelsShowButton, которая скрывает текстовые метки, в том числе, шкалу прогресса и показывает кнопку для запуска игры.

Рассмотрим метод fileSearch. Он является центральным местом программы, поскольку на нем лежит нагрузка по поиску и скачиванию файлов. Итак, метод принимает 6 параметров, из которых 2 являются не обязательными: каталог, файл, полный путь к файлу, ссылка на объект соединения (connect), контрольная сумма файла, файловый размер.

bool CFileReader::fileSearch(LPCWSTR dir, LPCWSTR file, LPCWSTR path, CFtpConnect& connect, DWORDLONG fileCRC /*= 0*/, DWORDLONG fileSize /*= 0*/)
{
   WIN32_FIND_DATA FindFileData;
   HANDLE hf;

   hf = FindFirstFile(path, &FindFileData);
   if (hf != INVALID_HANDLE_VALUE) {
   do {
     // использование структуры ULARGE_INTEGER для нахождения 64-х //битных значений, состоящих из двух 32-х битных частей
                                                                           ULARGE_INTEGER us;
       us.HighPart = FindFileData.nFileSizeHigh;
       us.LowPart = FindFileData.nFileSizeLow;
       ULONGLONG lfileSize = us.QuadPart;
       UINT32 crc = calcCtrlSum(path);
       //сравнить, удалить и обновить
       if ((crc != fileCRC || lfileSize < fileSize) || (fileCRC == 0 && fileSize == 0)) {
          if (DeleteFile(path)) {
          BOOL boo = SetCurrentDirectory(dir);
          if (!downloadFile(path, connect, fileSize, g_totalSize))
             return false;
          }
                                                                              //else MessageBox(hWin->GetSafeHwnd(), _T("Не удалось 
//удалить файл"), _T("Error"), 0);
      }
       else
      if (fileSize > 0) {
         connect.showPercentTotalStr((long)fileSize, g_totalSize);
      }
    } while (FindNextFile(hf, &FindFileData) != 0);
                               FindClose(hf);
                               return true;
  }
  else {//если объект не найден
        if (dir != _T(""))
        createDirSeq(dir);
        if (!downloadFile(path, connect, fileSize, g_totalSize))
          return false;
       }
    return true;
}

В начале метода, на клиентской машине, с помощью функции FindFirstFile ищется файл по пути path, результаты поиска помещаются в переменную FindFileData типа WIN32_FIND_DATA. Если поиск успешен, запускается цикл dowhile. В его теле из свойств найденного файла извлекаются размер файла, подсчитывается контрольная сумма файла. Далее, происходит сравнение данных: подсчитанная контрольная сумма сравнивается с контрольной суммой, полученной в параметре (считанная из скачанного текстового файла), изъятый из свойств файловый размер так же сравнивается с размером, полученным в параметре; если есть какие-то различия, то локальный файл удаляется (с помощью win32-функции DeleteFile)  и вызывается метод downloadFile, который скачивает файл (на самом деле, он распределяет работы между другими методами для скачивания файла, но об этом позднее). В случае если скачивание не удается, метод прерывается и возвращает false, на уровне выше, как мы помним, в таком случае, метод fileSearch тоже возвращает false, тогда метод readLocalFile перезапускает попытку скачивания данного файла. Если же файл успешно скачен, тогда осуществляется поиск следующего файла. Если он не находится, тогда поиск закрывается и возвращается true. Если в первоначальном поиске с помощью FindFirstFile файл не найден, тогда посредством метода createDirSeq создается последовательность вложенных каталогов, переданный в параметре dir, затем, с помощью метода downloadFile с сервера скачивается файл.

Контрольная сумма подсчитывается с помощью метода calcCtrlSum, который подробно рассмотрен в первой части обзора.

Метод createDirSeq служит для создания последовательности вложенных папок.

bool CFileReader::createDirSeq(LPCWSTR directory)//создать 
//последовательность директорий
{
   string s = "\\";
   string str = narrow(directory);
   string::size_type start_pos = 0;
   string::size_type pos = 0;
   bool dirCreated = false;//была ли создана папка
   bool firstCall = true;//первый вызов
   while ((pos = str.find(s, start_pos)) != string::npos)
   {
      if (!firstCall) {
         pos++;
         start_pos = pos;
         pos = str.find(s, start_pos);
      }
      if (pos == string::npos)
         pos = str.length();
      else
         pos = pos - start_pos;

         string dir = str.substr(start_pos, pos);
         wstring ws = wide_string(dir, loc);
         createDir(ws.c_str());
         dirCreated = true;
         firstCall = false;
   }
   if (!dirCreated) {
       createDir(directory);
   }

   return true;
}

Оп получает путь, просматривает его, извлекает отдельные имена, разделенные символом  ‘\’ - обратным слэшем и последовательно вызывает метод createDir. Последний получает имя директории, проверяет ее существование, в случае отсутствия, создает ее и переходит в нее, а в случае наличия этой директории, заходит в нее.

bool CFileReader::createDir(LPCWSTR directory)//создать каталог
{
   WIN32_FIND_DATA FindFileData;
   HANDLE hf;

   hf = FindFirstFile(directory, &FindFileData);
   if (hf != INVALID_HANDLE_VALUE) {
   if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)        {
      SetCurrentDirectory(directory);
   }
   }
   else {
       CreateDirectory(directory, NULL);
       SetCurrentDirectory(directory);
   }

   return true;
}

Рассмотрим метод downloadFile. Он служит для выбора способа скачивания файла.

bool CFileReader::downloadFile(LPCWSTR path, CFtpConnect& connect, DWORDLONG fileSize, DWORDLONG totalSize)
{
   wstring ppath = path;
   bool isTXT = isThisTXT(ppath);
   bool isLauncherDir = (fileSize == 0 && totalSize == 0);
   bool res = false;
   if (_isWin10) {
     if (!isTXT) {
     ppath = insertFolderName(ppath, isLauncherDir);
     path = ppath.c_str();
     res = connect.GetFtpFileToCreate(path, fileSize, g_totalSize, isTXT);
     }
     else
     {
      ppath = insertFolderName(ppath, isLauncherDir);
      path = ppath.c_str();
      res = connect.GetFtpFileToCreate(path, fileSize, totalSize, isTXT);
     }
   }
   else
   {
     if (!isTXT) {
       ppath = insertDomenName(ppath, isLauncherDir);
       path = ppath.c_str();
       res = connect.GetHttpFileToCreate(path, fileSize, g_totalSize);
     }
     else
     {
        ppath = insertFolderName(ppath, isLauncherDir);
        path = ppath.c_str();
        res = connect.GetFtpFileToCreate(path, fileSize, totalSize);
     }
   }
   if (!res) {
     //MessageBox(hWin->GetSafeHwnd(), path, _T("Неудача"), NULL);
   }
   return res;
}

Для разных способов соединения с сервером, метод форматирует строку - путь к файлу: если подключение происходит по ftp, тогда путь к файлу представляет файловый путь:

//добавить в начало пути к файлу серверную папку (для FTP 
//подключения)
static wstring insertFolderName(wstring path, const bool bLauncher)
{
   string str = narrow(path);
   if (!bLauncher)
       str = _ftp_folder + _dirName + "/" + str;
   else
       str = _ftp_folder + "Launcher" + "/" + str;
   path = wide_string(str, loc);
   return path;
}

Если же подключение по http, то путь представляет доменное имя:

//добавить в начало пути к файлу доменное имя с папкой (для 
//HTTP подключения)
static wstring insertDomenName(wstring path, const bool bLauncher)
{
   string str = narrow(path);
   if (!bLauncher)
       str = _domen + _dirName + "/" + str;
   else
      str = _domen + "Launcher" + "/" + str;
   path = wide_string(str, loc);
   return path;
}

К слову, так как по ftp происходит авторизированное подключение, то клиент имеет права для свободной закачки из любого предоставленного ему каталога. В то же время, по ftp можно выделить определенный каталог, для которого предоставить анонимный доступ, то есть — для всех. Тем не менее, при подключении по http, авторизация отсутствует, однако, есть каталог, доступный для чтения любому пользователю, в этом каталоге размещается сайт, к которому, как мы прекрасно знаем, подключается браузер, который имеет права читать, выполнять скрипты и веб-приложения. При разработке лаунчера, примерно 7 месяцев назад, yurembo, вообще, ничего не знал про веб-сервера, это сейчас он прокачался в вебе, а в то время со сведениями об открытом на веб-сервере каталоге ему подсказала Ирина Чернова. Общедоступным каталогом на веб-сервере является http. Закинув туда игрового клиента, и настроив пути доступа в лаунчере, пошла закачка, подробнее об этом ниже.

Как вы уже знаете, текстовые файлы качаются по протоколу ftp, тогда, как все остальные - по протоколу http. Однако когда я разрабатывал лаунчер и тестировал его на разных операционных системах, в Windows 10 при скачивании по протоколу http были проблемы, поэтому, если лаунчер выполняется в Windows 10, тогда все файлы качаются по протоколу ftp. Метод скачивания данных по этому протоколу мы уже разобрали.

Посмотрим, как определяется версия операционной системы. В методе создания окна, вызывается статическая функция getWindowsVersion:

static DWORD getWindowsVersion()
{
   RTL_OSVERSIONINFOEXW *pk_OsVer = new RTL_OSVERSIONINFOEXW;
   typedef LONG(WINAPI* tRtlGetVersion)(RTL_OSVERSIONINFOEXW*);

   memset(pk_OsVer, 0, sizeof(RTL_OSVERSIONINFOEXW));
   pk_OsVer->dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);

   HMODULE h_NtDll = GetModuleHandleW(L"ntdll.dll");
   tRtlGetVersion f_RtlGetVersion = (tRtlGetVersion)GetProcAddress(h_NtDll, "RtlGetVersion");

   if (!f_RtlGetVersion)
     return FALSE; // This will never happen (all processes load //ntdll.dll)

   LONG Status = f_RtlGetVersion(pk_OsVer);

   int ret = 0;

   if (Status == 0)
      ret = pk_OsVer->dwMajorVersion;

   delete pk_OsVer;

   return ret;
}

Так как, в разных версиях системы Windows NT,  функции для получения версии системы разные, и в следующей версии системы, функции, актуальные для предыдущей, считаются устаревшими, то одно из самых верных решений, считать номер версии из файла ntdll.dll, который присутствует во всех системах класса NT. Системами Windows 9x можно пренебречь, они давно устаревшие, на них все равно ничего уже не работает. Когда версия считана, она возвращается в вызвавший код. Где, на основе результата, глобальной переменной _isWin10 присваивается булево значение: _isWin10 = (getWindowsVersion() >= 10); Таким образом, лаунчер узнает систему, в которой выполняется.

Глобальные переменные




Обратите внимание, каким образом в лаунчере объявлены глобальные переменные — в начале файла functions.h. Так как, этот файл подключается много раз, поскольку его функциональность широко используется, именно поэтому она выделена в отдельный файл, объявленные в этом файле объекты создаются много раз, при каждом подключении, следовательно, образуется много одноименных объектом. Это вызывает проблемы, и линковщик сообщает об ошибке. Таким образом, надо предотвратить множественное создание одних и тех же объектов. Для этого в стандартном C++ присутствует ключевое слово static, с помощью которого можно объявить общие вспомогательные функции, не относящиеся к какому-либо определенному классу, а используемые объектами каждого. Однако, на переменную это ключевое слово действует по другому, в таком случае, статичная переменная будет доступна только в пределах одного файла исходного кода, где была объявлена. Это идет в разрез с нашими планами, поэтому для объявления глобальных переменных я воспользовался ключевым словом __delspec. Оно платформо-зависимо, не входит в стандарт ISO C++ и является фичей компилятора Microsoft C++. Однако, я не собираюсь билдить мой лаунчер на другом компиляторе и операционной системе. Объявление глобальной переменной выглядит следующим образом:

__declspec(selectany) bool _isWin10 = false;

Модификатор selectancy сообщает компилятору, что объявленный глобальный элемент данных (переменная или объект) - это запись COMDAT (упакованная функция) режима pick-any (выбрать-любой). Во время компоновки, если видны несколько определений COMDAT, то компоновщик выбирает одно и отменяет остальные. Для динамически инициализированных, глобальных объектов, selectany также отбрасывает весь код инициализации объекта, на который нет ссылок. Глобальный элемент данных может быть нормально инициализирован только один раз в проекте EXE или DLL. selectany может использоваться при инициализации глобальных данных, указанных в заголовках, если такой же заголовок появляется в более чем одном файле-исходнике.

http-соединение


Однако вернемся в метод downloadFile. Вопрос остается открытым: почему я качаю файлы по двум протоколам, а не только по ftp? Во время разработки и тестирования, у меня в системе все файлы клиента прекрасно качались по ftp. Кроме того, когда мы стали проводить тестирование среди разработчиков Генома, у большинства из них закачка проходила успешно. Тем не менее, нашелся моделер, у которого закачка обрывалась (привет Свете). После пары ночей тестирования и построения догадок, выяснилось, что обрыв закачки происходил на скачивании большого файла — более гигабайта. Это было странно, и в моих условиях не тестировалось, так как у меня прекрасно качались любые данные. Проблема была очень существенной, ведь, далеко не у всех игроков Генома будет стабильный и хороший коннект, как у большинства разработчиков. И, это было хорошо, что на начальной стадии тестирования нашелся пользователь со слабым коннектом.
Я перепробовал несколько решений, в том числе, я испытал сетевую библиотеку libcurl, но ни одно из них не дало нужного результата, скачивание по-прежнему обрывалось. Тогда, я решил воспользоваться протоколом http. Забегая вперед, он не только дал нужный результат, когда у вышеназванного пользователя исчезла проблема со скачиванием, так же у остальных повысилась скорость закачки.
Скачивание по протоколу http в лаунчере выполняется с помощью метода GetHttpFileToCreate, который принимает 3 параметра: file - путь и имя файла для закачки, fileSize - размер файла, totalSize - общий объем для скачивания.

bool WINAPI CFtpConnect::GetHttpFileToCreate(LPCWSTR file, DWORDLONG fileSize, DWORDLONG totalSize)
{
   LPCWSTR mainFile = file;
               
   string s = "\\";
   string t = "/";
   string str = narrow(file);

   string fn = narrow(file);
   string dn;
   getFileNameAndDirName(fn, dn);
   wstring wfn = wide_string(fn, loc);
   file = wfn.c_str();

   replace(str.begin(), str.end(), '\\', '/');
   wstring wstr = wide_string(str, loc);
   LPCWSTR rfile = wstr.c_str();

   //используется для прокачивания загрузки!
   unsigned long iBytesRead;
   unsigned long currentFileSize = 0;//скачивание текущенго файла

   HINTERNET HttpRequestDownload = HttpOpenRequest(hHTTPConnect, TEXT("GET"), rfile, NULL, NULL, 0, INTERNET_FLAG_KEEP_CONNECTION, 1);
               
   if (HttpRequestDownload == NULL) {
       //MessageBox(hWin->GetSafeHwnd(), file, TEXT("Файл не 
//найден"), 0);
       //InternetErrorOut(hWin->GetSafeHwnd(), GetLastError(),
 //TEXT("FtpOpenFile"));
        InternetCloseHandle(HttpRequestDownload);
        return false;
   }
               
   wstring strHeader = _T("Accept: */*");
   BOOL bSend = HttpAddRequestHeaders(HttpRequestDownload, strHeader.c_str(), strHeader.length(), HTTP_ADDREQ_FLAG_ADD);
   if (!bSend) {
     //MessageBox(hWin->GetSafeHwnd(), strHeader.c_str(), 
//TEXT("http-заголовок"), 0);
     return false;
   }
   strHeader = _T("Content-Type: application/x-www-form-urlencoded");
   bSend = HttpAddRequestHeaders(HttpRequestDownload, strHeader.c_str(), strHeader.length(), HTTP_ADDREQ_FLAG_ADD);
   if (!bSend) {
     //MessageBox(hWin->GetSafeHwnd(), strHeader.c_str(), 
//TEXT("http-заголовок"), 0);
     return false;
   }
   bSend = HttpSendRequest(HttpRequestDownload, NULL, 0, NULL, 0);
   if (!bSend) {
     //MessageBox(hWin->GetSafeHwnd(), strHeader.c_str(), 
//TEXT("http-заголовок"), 0);
     return false;
   }
   if (HttpRequestDownload) {
     DWORD dwContentLength;
     //unsigned long iBytesRead; --- объявление перенесено выше!
     BYTE Buffer[8192];
     ZeroMemory(Buffer, sizeof(Buffer));
     dwContentLength = 4 * 1024;
     wstring wideFile = getFileNameWithoutDomen(wfn, _domen);
     LPCWSTR writeFile = wideFile.c_str();
     FILE *pFile = _tfopen(writeFile, TEXT("wb"));
     if (pFile == NULL) {
        //MessageBox(hWin->GetSafeHwnd(), writeFile, TEXT("Cannot //create a file"), 0);
        return false;
     }
     do
       {
         if (InternetReadFile(HttpRequestDownload, Buffer, dwContentLength, &iBytesRead))
         {
           currentFileSize += iBytesRead;
           //выводим шкалу общего скачивания (new)
           if (_TotalSkinProgress && totalSize > 0) {
              showPercentTotalStr(iBytesRead, totalSize);
           }
           fwrite(Buffer, iBytesRead, 1, pFile);
           ZeroMemory(Buffer, sizeof(Buffer));
         }
       } while (iBytesRead != 0);
       fclose(pFile);
     }

     InternetCloseHandle(HttpRequestDownload);

     bool result = false;
     if (fileSize == 0 && totalSize == 0)
        result = true;
     else
        result = (currentFileSize == fileSize);

     return result;
}

В начале метода выполняется преобразование пути: сужение символов, замена обратного слэша прямым слэшем. После чего, создается объект HttpRequestDownload типа HINTERNET, он указывает на открытый файл через http подключение. Этот объект создается посредством функции HttpOpenRequest, она принимает 8 параметров: указатель на объект, созданный с помощью InternetConnect внутри метода createHTTPConnect; указатель на строку, содержащую http-операцию (GET, PUT, POST); указатель на строку, включающую путь к файлу на веб-сервере, к которому надо получить доступ; номер версии http-протокола (NULL означает использовать версию 1.0 или 1.1, в зависимости от настроек Internet Explorer); ссылка на URL-документ, если NULL, используется значение 3-го параметра; указатель на массив значений способа доступа, в нашем случае - 0; дополнительные параметры, в нашем случае использован флаг INTERNET_FLAG_KEEP_CONNECTION, который означает использовать семантику «живого соединения», если оно доступно; последний параметр - указатель на дополнительные опции, в нашем случае, не используется, поставлен 0.
Затем, мы создаем http-запрос, формируя заголовок, вызывая для этого функцию HttpAddRequestHeaders, которая получает следующие 4 параметра: указатель на объект, возвращенный функцией HttpOpenRequest, полученный на прошлом шаге; указатель на строку, которую надо добавить в заголовок; размер этой строки; набор модификаторов, определяющий семантику функции, мы использовали модификатор HTTP_ADDREQ_FLAG_ADD, он позволяет, в случае отсутствия, добавить заголовок. Сначала, мы добавляем в заголовок строку:
wstring strHeader = _T("Accept: */*");
вторым вызовом указанной функции, мы добавляем строку:
strHeader = _T("Content-Type: application/x-www-form-urlencoded");
Функция HttpAddRequestHeaders возвращает булево значение, означающее успех или неудачу ее выполнения.
После этого, с помощью функции HttpSendRequest отправляем запрос на сервер. Эта функция получает 4 параметра: указатель на объект HttpRequestDownload; указатель на дополнительные заголовки, может быть NULL, если заголовки записаны в объекте HttpRequestDownload, как в нашем случае; размер дополнительных заголовков; указатель на буфер дополнительных параметров, отправляемые после отправки заголовков, используется с операциями POST и PUT, поэтому в нашем случае игнорируется; размер буфера дополнительных параметров, так как в данном случае этот буфер не используется, то и размер нулевой. Рассматриваемая функция возвращает логическое значение, соответствующее успеху или провалу своего выполнения.

После этого, начинается непосредственная закачка запрошенного файла: выделяется и очищается буфер, открывается для чтения файл, затем он читается посредством функции InternetReadFile. Чтение удаленного файла мы уже проходили, поэтому я не буду повторяться, для освежения памяти, обратись к обзору метода GetFtpFileToCreate.
В конце функции, закрывается хэндл открытого файлы, сравнивается размер скачанного файла с действительным объемом, на основе чего возвращается результат.
Затем, в цикле все описанные операции повторяются снова, как мы это видели выше, и так происходит, пока не будут скачены все файлы, находящиеся в списке. Когда все файлы скачены и все они в целом состоянии, лаунчер показывает кнопку для запуска игры. Таким образом, работает лаунчер и так он качает файлы.

Заключение


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



Статья получилась несколько объемнее, чем я планировал. Есть идея написать 3-ю статью, в которой рассмотреть еще один важный вопрос лаунчера. Поживем, увидим, время покажет. До встречи!