Решение системных задач для предприятий малого бизнеса.

Игры от YuWik

Дозвон с использованием WinAPI(RAS-функции) (Borland Builder C++)

Константин аватар

Дозвон к провайдеру в Windows реализуется через набор так называемых RAS(Remoute Access System)-функций. Как понятно из названия данная подсистема АПИ (API) реализует набор возможностей по удаленyому доступу на сервер. Дозвон и установка соединения - это малая часть функциональности - более полное описание вы можете посмотреть в МСДН. В данной статье будет рассмотрено исключительно дозвон и установка соединения.

Для начала вам необходимо подключить заголовочные файлы windows.h,ras.h и raserror.h
Все функции реализованы в RASAPI32.DLL, т.е. что бы использовать функции необходимо или загрузить библиотеку динамически, либо подключить .lib-файл в проект. Последнее гораздо проще и быстрее - с некоторыми оговорками. При динамической загрузке ВЫ управляете процессом и должны обрабатывать внештатные ситуации. Если по каким либо причинам библиотека отсутствует, изменена или еще что-нибудь не так - в случае динамической загрузки вы можете вывести осмысленное сообщение, при статической линковке приложение просто не запустится. В общем во всех ситуациях, когда скорость не особо критична я использую динамическую загрузку - но это не значит, что это правильно - просто мне так больше нравится :)
Т.е. следующим вашим шагом будет объявление указателей на функции:

typedef DWORD (__stdcall *pRasGetErrorString)(UINT, LPTSTR, DWORD);
typedef DWORD (__stdcall *pRasDial)(LPRASDIALEXTENSIONS, LPTSTR,
  LPRASDIALPARAMS, DWORD, LPVOID, LPHRASCONN);
typedef DWORD (__stdcall *pRasEnumConnections)
  (LPRASCONN, LPDWORD, LPDWORD);
typedef DWORD (__stdcall *pRasGetConnectStatus)
  (HRASCONN, LPRASCONNSTATUS);
typedef DWORD (__stdcall *pRasEnumEntries)(LPTSTR, LPTSTR,
  LPRASENTRYNAME, LPDWORD, LPDWORD);
typedef DWORD (__stdcall *pRasGetEntryDialParams)(LPTSTR,
  LPRASDIALPARAMS,LPBOOL);
typedef DWORD (__stdcall *pRasHangUp)(HRASCONN);

pRasGetErrorString fRasGetErrorString;
pRasDial fRasDial;
pRasEnumConnections fRasEnumConnections;
pRasGetConnectStatus fRasGetConnectStatus;
pRasEnumEntries fRasEnumEntries;
pRasGetEntryDialParams fRasGetEntryDialParams;
pRasHangUp fRasHangUp;
HINSTANCE hRasInstance;
HRASCONN hRas;

Надеюсь здесь понятно все - назначение функций я поясню далее.
Для работы приложения мне необходимы 7 функций - но это в моем случае. Если вы предполагаете, что пользователь будет вводить имя и пароль в приложении, вместо использования соединения по умолчанию - то часть функций вам не пригодится, зато понадобятся другие :). Как уже сказал - более подробное пояснение смотрите в МСДН - ну а я продолжу.
Теперь необходимо реализовать функцию, которая будет выводить всякую фигню на экран - типа "Набор номера...","Установка соединения....", и т.п.
Выглядит эта функция так(имя может быть любым):

VOID WINAPI RasCallback(HRASCONN hrasconn, UINT unMsg,
  RASCONNSTATE rascs, DWORD dwError, DWORD dwExtendedError)
{
  String S = "";
  if (dwError) {
    // Error occurred, show the error string.
    char buff[256];
    fRasGetErrorString(dwError, buff, sizeof(buff));
    fmMainFtp->LogEvents(buff);
    String MsgError = "При подключении произошла следующая ошибка:\n"+String(buff);
    if(Application->MessageBoxA(MsgError.c_str(),"Ошибка",MB_ICONQUESTION+MB_OKCANCEL)==IDOK){
      fRasHangUp(hrasconn);
     fmMainFtp->ttTimer->Enabled = true;
     return;
    }
    else fmMainFtp->Close();
    return;
  }
  switch (rascs) {
    // Build a status string based on the
    // status message.
    case RASCS_PortOpened :
      S = "Открытие порта..."; break;
    case RASCS_DeviceConnected :
      S = "Соединение..."; break;
    case RASCS_Authenticate :
      S = "Вход в сеть..."; break;
    case RASCS_Authenticated :
      S = "Проверка пользователя и пароля"; break;
    case RASCS_Connected : {
      S = "Вход в сеть";
     // fmMainFtp->bbSend->Enabled = true;
        fmMainFtp->ttTimerClick->Enabled = true;
      break;
    }
    case RASCS_Disconnected :
      S = "Disconnected"; break;
  }
  // Show the status message in the memo.
  if (S != "")
    fmMainFtp->LogEvents(S);
}

обратите внимание на важный момент - функция объявлена как WINAPI - и она объявлена вне класса формы. Если возникла ошибка, она обрабатывается и если нужно, устанавливается соединение или форма закрывается. Если все нормально - обрабатываются события соединения. LogEvents - функция отвечающая за вывод информации на форму и запись в файл.

void __fastcall TfmMainFtp::LogEvents(String Msg){
String Message = DateTimeToStr(Now())+"\t"+Msg;
  mmLog->Lines->Add(Message);
  Message=Message+"\n";
   Word Year, Month, Day;
TDateTime Current = Now();
  DecodeDate(Current,Year,Month,Day);
  TVarRec arg[3] = {2,Month,Day};
  String M = Format("%2.2d",&arg[1],0);
  String D = Format("%2.2d",&arg[2],0);
  String LogFile = "message"+IntToStr(Year)+M+D+".log";
  String tmp = GetCurrentDir();
  SetCurrentDir(DirProg+"\\log");
  ofstream file(LogFile.c_str(),ios::app);
  file << Message.c_str();
  file.close();
  SetCurrentDir(tmp);
  return;
}

В приложении соответственно необходимо контролировать наличие поддиректории \log. Все остальное я надеюсь особых пояснений не требует. Сообщение формируется, выводится в данном случае в мемо (mmLog) после чего записывается в файл лога с именем "messageYYYYMMDD.log" - где YYYY - год, MM - месяц ну и день понятно.
Следующий шаг - собственно загрузка длл и функций из нее:

MutexFirst = CreateMutex(NULL,true,"MyMutex");
int Res = GetLastError();
if(Res==ERROR_ALREADY_EXISTS){
First = 1;
Close();
}
  hRas = 0;
  hRasInstance = LoadLibrary("RASAPI32.DLL");
  if (!hRasInstance) {
    LogEvents("Unable to load RAS DLL.");
    Close();
    return;
  }
  fRasGetErrorString = (pRasGetErrorString)
    GetProcAddress(hRasInstance, "RasGetErrorStringA");
  fRasDial = (pRasDial)
    GetProcAddress(hRasInstance, "RasDialA");
  fRasEnumConnections = (pRasEnumConnections)
    GetProcAddress(hRasInstance, "RasEnumConnectionsA");
  fRasGetConnectStatus = (pRasGetConnectStatus)
    GetProcAddress(hRasInstance, "RasGetConnectStatusA");
  fRasEnumEntries = (pRasEnumEntries)
    GetProcAddress(hRasInstance, "RasEnumEntriesA");
   fRasGetEntryDialParams  = (pRasGetEntryDialParams)GetProcAddress(hRasInstance, "RasGetEntryDialParamsA");
  fRasHangUp = (pRasHangUp)
    GetProcAddress(hRasInstance, "RasHangUpA");
  if (!fRasGetErrorString || !fRasDial || !fRasEnumConnections ||
      !fRasGetConnectStatus || !fRasEnumEntries || !fRasHangUp) {
    LogEvents("Ошибка загрузки функций дозвона");
     Close();
     return;
  }

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

HRASCONN TfmMainFtp::CheckForConnections()
{
if(GetInternetConnection())return (void*)(1);
  char buff[256];
  RASCONN rc;
  rc.dwSize = sizeof(RASCONN);
  DWORD numConns;
  DWORD size = rc.dwSize;
  // Enumerate the connections.
  DWORD res = fRasEnumConnections(&rc, &size, &numConns);
  if (!res && numConns == 0)
    // В системе не создано нет соединений - нечем работать
    return 0;
  if (res) {
    // Ошибка!!! Пишем в лог
    fRasGetErrorString(res, buff, sizeof(buff));
    LogEvents(buff);
    String MsgError = "При подключении произошла следующая ошибка:\n"+String(buff);
//Если пользователь выбирает - перезвонить - запускаем таймер и пытаемся снова    if(Application->MessageBoxA(MsgError.c_str(),"Ошибка",MB_ICONQUESTION+MB_OKCANCEL)==IDOK){
    fRasHangUp(rc.hrasconn);
     ttTimer->Enabled = true;
     return 0;
    }
    else Close();
    return 0;
  }
else {
    // Получение текущего статуса.
    RASCONNSTATUS status;
    status.dwSize = sizeof(status);
    res = fRasGetConnectStatus(rc.hrasconn, &status);
    if (res) {
      // Ошибка!!!!
      fRasGetErrorString(res, buff, sizeof(buff));
      LogEvents(buff);
      String MsgError = "При подключении произошла следующая ошибка:\n"+String(buff);
    if(Application->MessageBoxA(MsgError.c_str(),"Ошибка",MB_ICONQUESTION+MB_OKCANCEL)==IDOK){
    fRasHangUp(rc.hrasconn);
     ttTimer->Enabled = true;
     return 0;
    }
    else Close();
      return 0;
    } else {
      // Если есть соединение - показываем его статус.
      if (status.rasconnstate == RASCS_Connected) {
        LogEvents("Установлено соединение:");
        LogEvents("Тип устройства:\t" +
          String(rc.szDeviceType));
        LogEvents("Название устройства: " +
          String(rc.szDeviceName));
        LogEvents("Соединение: " +
          String(rc.szEntryName));
        return rc.hrasconn;
      } else {
        // Соединение найдено но оно уже завершено
        // status is RASCS_Disconnected.
//Если надо - кладем трубку и перезваниваем
        LogEvents("Connection Error");
        String MsgError = "При подключении произошла следующая ошибка:\n"+String(buff);
         if(Application->MessageBoxA(MsgError.c_str(),"Ошибка",MB_ICONQUESTION+MB_OKCANCEL)==IDOK){
         fRasHangUp(rc.hrasconn);
         ttTimer->Enabled = true;
         return 0;
        }
        else Close();
        return 0;
      }
    }
  }
  return 0;
}

А эта функция собственно все и начинает - у меня она привязана к таймеру, который активизируется при создании формы

void __fastcall TfmMainFtp::acConnectWithInetExecute(TObject *Sender)
{
ttTimer->Enabled = false;
hRas = CheckForConnections();
String PBEntry;
  if (hRas) {
   LogEvents("Используем существующее соединение...");
   //LogEvents("Кликните на кнопку \"Отправить/принять\" или ожидайте автоматического старта соединения.");
   // bbSend->Enabled = true;
    ttTimerClick->Enabled = true;
    AlreadyConnected = true;
    // Ничего не нужно делать - соединение установлено.
    return;
  }
  else {
    // Выбираем список соединений.
  PBEntry = GetPhoneBookEntry();
    if (PBEntry == "") {
      LogEvents("Нет соединений");
      Close();
    }
  }

  LogEvents("Соединение отсутствует. Дозваниваемся...");
  // Проверяем - вдруг мы работаем под - 98винд.
  CheckRnaApp();
  // Установка параметров соединения.
  RASDIALPARAMS params;
  params.dwSize = sizeof(params);
  strcpy(params.szEntryName, PBEntry.c_str());
//Юзаем сохраненный пароль
   BOOL  PwdDetected = FALSE;
//Заполняем структуру
   DWORD Result = fRasGetEntryDialParams(NULL,&params,&PwdDetected);
//Дозваниваемся
  DWORD res = fRasDial(0, 0, &params, 1, RasCallback, &hRas);
//Чегото не то :) 
if (res) {
    char buff[256];
    fRasGetErrorString(res, buff, sizeof(buff));
    LogEvents(buff);
    String MsgError = "При подключении произошла следующая ошибка:\n"+String(buff);
    if(Application->MessageBoxA(MsgError.c_str(),"Ошибка",MB_ICONQUESTION+MB_OKCANCEL)==IDOK){
    fRasHangUp(hRas);
     ttTimer->Enabled = true;
     return;
    }
    else Close();
  }

}

здесь нам понадобилось еще две функции - для получения имени соединения по умолчанию и для проверки версии операционнки - вот они:

String TfmMainFtp::GetPhoneBookEntry()
{
LogEvents("Инициализация соединения");
String S;
TRegistry *Reg = new TRegistry;
Reg->RootKey = HKEY_LOCAL_MACHINE;
Reg->OpenKeyReadOnly("\\SOFTWARE\\Microsoft\\RAS AutoDial\\Default");
LogEvents("Получение соединения по умолчению");
//Если есть соединение по умолчанию
if(Reg->ValueExists("DefaultInternet")){
  S = Reg->ReadString("DefaultInternet");

  delete Reg;
  return S;
  }
else{
  RASENTRYNAME* entries = new RASENTRYNAME;
  entries->dwSize = sizeof(RASENTRYNAME);
  DWORD numEntries;
  DWORD size = entries->dwSize;
  DWORD res = fRasEnumEntries(0, 0, entries, &size, &numEntries);
  if (numEntries == 1) {
    String entryName = entries->szEntryName;
    delete[] entries;
    delete Reg;
    return entryName;
  }
//если соединений больше одного и нет по умолчанию - берем первое
  if (res == ERROR_BUFFER_TOO_SMALL) {
    // памяти йок
    delete[] entries;
    entries = new RASENTRYNAME[numEntries];
    entries[0].dwSize = sizeof(RASENTRYNAME);
    res = fRasEnumEntries(0, 0, entries, &size, &numEntries);
    if (res) {
      char buff[256];
      fRasGetErrorString(res, buff, sizeof(buff));
      LogEvents(buff);
    }
  S = entries[0].szEntryName;
  }
  delete[] entries;
  delete Reg;
  return S;
}
}

void TfmMainFtp::CheckRnaApp()
{
  if (Win32Platform == VER_PLATFORM_WIN32_NT)
    // RNAAPP is only Win9x
    return;
  HWND hWnd = FindWindow("RnaEngClass", 0);
  if (hWnd) {
    // Found it, kill it.
    PostMessage(hWnd, WM_CLOSE, 0, 0);
    // Delay a bit for good measure.
    Sleep(2000);
  }
}

Ну естественно, по завершении работы надо закрыть соединение - если его открывали мы:

void __fastcall TfmMainFtp::CloseConnect()
{
LogEvents("Завершаем соединение...");
  if (!hRas)
    return;
  // Hang up.
  fRasHangUp(hRas);
  // Очищаем состояние.
  DWORD res = 0;
  while (res != ERROR_INVALID_HANDLE) {
    RASCONNSTATUS status;
    status.dwSize = sizeof(status);
    res = fRasGetConnectStatus(hRas, &status);
    ::Sleep(0);
  }
  AlreadyConnected = true;
  hRas = 0;
  LogEvents("Соединение завершено");

}

Вот и все.