Сообщений 10    Оценка 721 [+1/-0]         Оценить  
Система Orphus

Asynchronous Pluggable Protocol

Автор: Вадим Макутин
Источник: RSDN Magazine #5-2003
Опубликовано: 12.06.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Что-то вроде вступления
Постановка задачи
Пишем протокол
Регистрация протокола
Заключение

Исходные тексты

Что-то вроде вступления

Разработчики, использующие ActiveX-объект Microsoft WebBrowser Control или класс CHTMLView из Microsoft Foundation Classes, рано или поздно задаются вопросом: «А можно ли загрузить страницу из базы данных не создавая временной копии на диске?». Если страница содержит текстовую информацию, можно воспользоваться методом Write интерфейса IHTMLDocument2, который можно получить из самого control-а. А файлы с картинками? Тут приходит на помощь Asynchronous Pluggable Protocol.


Постановка задачи

В данной статье будет продемонстрирована работа Asynchronous Pluggable Protocol. Нашей целью будет написать протокол, который мог бы подгружать данные, тем способом, которым мы захотим, и оттуда, откуда мы захотим, и показывать их в WebBrowser Control.

Итак, воспользовавшись методом Navigate, определенным в интерфейсе IWebBrowser2, либо аналогичной функцией-членом класса CHTMLView, приводим в действие механизм запуска Asynchronous Pluggable Protocol. Параметр Urlданного метода будет выглядеть следующим образом:

myprot://path_key/object_key

Здесь:

Обязательной частью такого Url является myprot://, дальше вы должны указать свои параметры, так как только таким способом можно сообщить протоколу, что именно нужно подгружать в данный момент. Вы также можете передавать SQL-запрос. В общем, чем сложнее будет Url, тем сложнее его будет разбирать. Вы можете использовать подобный Url и для загрузки различных объектов (например, изображений img).

ПРИМЕЧАНИЕ

Протокол начинает свою работу, когда WebBrowser встречает Url с соответствующим строковым идентификатором протокола, независимо от того, что control загружает в данный момент, страницу, картинку и т.п. Просто создается еще один поток, в котором выполняется подгрузка нужного объекта. Таким образом, WebBrowser создает столько потоков (следовательно, столько же экземпляров протокола), сколько необходимо. Каждый экземпляр протокола ничего не знает о других экземплярах, хотя все они выполняются в пределах одного процесса.

Основные этапы, которые придется пройти при разработке протокола:

  1. Создать класс, родителями которого являются интерфейсы IInternetProtocol и IInternetProtocolRoot (необходимо перекрыть некоторые методы этих интерфейсов, чтобы обеспечить работу протокола).
  2. Произвести разбор передаваемого протоколу Url и загрузить данные в память.
  3. Произвести загрузку данных в WebBrowser Control.
  4. Последним, что необходимо сделать, это разработать тестовое приложение, которое произведет регистрацию протокола и продемонстрирует его работу.

Пишем протокол

С помощью Wizard создаем ATL-проект – dll с названием MyInternetProtocol. Добавляем в проект ATL Simple Object. Для примера назовем его MyProtocol.

Добавим реализацию методов интерфейсов IInternetProtocol и IInternetProtocolRoot. В результате мы получим файл, похожий на следующий листинг:

      #ifndef __MYPROTOCOL_H_
#define __MYPROTOCOL_H_

#include"resource.h"/////////////////////////////////////////////////////////////////////////////// CMyProtocolclass ATL_NO_VTABLE CMyProtocol : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CMyProtocol, &CLSID_MyProtocol>,
  public IInternetProtocol,
  public IInternetProtocolRoot
{
public:
  CMyProtocol()
  {
    m_lPosition = 0;
    m_lSize = 0;
  }

DECLARE_REGISTRY_RESOURCEID(IDR_MYPROTOCOL)
DECLARE_NOT_AGGREGATABLE(CMyProtocol)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CMyProtocol)
  COM_INTERFACE_ENTRY_IID (IID_IInternetProtocol, CMyProtocol)
  COM_INTERFACE_ENTRY_IID (IID_IInternetProtocolRoot, CMyProtocol)
END_COM_MAP()

public:
  // IInternetProtocolRoot
  HRESULT STDMETHODCALLTYPE Start(
    /* [in] */ LPCWSTR szUrl,
    /* [in] */ IInternetProtocolSink __RPC_FAR *pOIProtSink,
    /* [in] */ IInternetBindInfo __RPC_FAR *pOIBindInfo,
    /* [in] */ DWORD grfPI,
    /* [in] */ DWORD dwReserved);
    
  HRESULT STDMETHODCALLTYPE Continue(
    /* [in] */ PROTOCOLDATA __RPC_FAR *pProtocolData);
    
  HRESULT STDMETHODCALLTYPE Abort(
    /* [in] */ HRESULT hrReason,
    /* [in] */ DWORD dwOptions);
    
  HRESULT STDMETHODCALLTYPE Terminate(
    /* [in] */ DWORD dwOptions);
    
  HRESULT STDMETHODCALLTYPE Suspend(void) {return E_NOTIMPL;}
  HRESULT STDMETHODCALLTYPE Resume(void) {return E_NOTIMPL;} 
  // IInternetProtocol
  HRESULT STDMETHODCALLTYPE Read(
    /* [length_is][size_is][out][in] */void __RPC_FAR *pv,
    /* [in] */ ULONG cb,
    /* [out] */ ULONG __RPC_FAR *pcbRead);
    
  HRESULT STDMETHODCALLTYPE Seek(
    /* [in] */ LARGE_INTEGER dlibMove,
    /* [in] */ DWORD dwOrigin,
    /* [out] */ ULARGE_INTEGER __RPC_FAR *plibNewPosition);
  
  HRESULT STDMETHODCALLTYPE LockRequest(
    /* [in] */ DWORD dwOptions);
    
  HRESULT STDMETHODCALLTYPE UnlockRequest(void);

private:

  CComPtr<IInternetProtocolSink> m_pIProtSink;
  CComPtr<IStream> m_pIStream;
  ULONG m_lPosition;
  ULONG m_lSize;

  //загружаем данные в m_pIStream из ***
  HRESULT LoadDataToIStream(/* [in] */ LPCWSTR wstrUrl);
};

#endif//__MYPROTOCOL_H_

Вы, наверное, заметили метод LoadDataToIStream и переменные-члены класса m_pIStream - m_lPosition, m_lSize, m_pIProtSink. Этот метод будет отвечать за загрузку данных в поток, ссылка на который находится в m_pIStream, а m_lPosition и m_lSize помогут позиционироваться в m_pIStream при выгрузке данных в WebBrowser, но об этом будет рассказано ниже. При помощи интерфейса IInternetProtocolSink (m_pIProtSink) мы будем сообщать браузеру о состоянии загрузки данных.

С чего начинает работу протокол? С вызова метода Start интерфейса IInternetProtocolRoot. В этом методе после проверки аргументов вызывается функция LoadDataToIStream. Реализация этой функции в данной статье рассматриваться не будет, хотя с ней можно ознакомиться, изучив исходный код, прилагаемый к статье. Следует заметить, что именно в этой функции осуществляется разбор Url и загрузка данных в m_pIStream.

Важно также отметить, что именно в методе Start нужно оповестить WebBrowser о том, что данные для загрузки подготовлены, и control может их подгружать. Для этого понадобится интерфейс IInternetProtocolSink, ссылка на который передается в параметре pOIProtSink. Сохраняем его в переменной члене класса, т.к. в дальнейшем нам еще понадобится информировать браузер о событиях, которые происходят в нашем протоколе. Вызывая метод ReportData с первым параметром, равным BSCF_FIRSTDATANOTIFICATION, сообщаем браузеру, что данные есть, и что производится первое обращение к этим данным.

Исходный код метода Start имеет следующий вид:

HRESULT STDMETHODCALLTYPE CMyProtocol::Start(
  /* [in] */ LPCWSTR szUrl, 
  /* [in] */ IInternetProtocolSink __RPC_FAR  *pOIProtSink,
  /* [in] */ IInternetBindInfo __RPC_FAR *pOIBindInfo,
  /* [in] */ DWORD grfPI,
  /* [in] */ DWORD dwReserved)
{
  ATLTRACE(_T("CMyProtocol::Start ()\n"));

  //проверяем аргументы, которые нам понадобятсяif ((szUrl == NULL) || (pOIProtSink == NULL))
    return E_INVALIDARG;

  //загружаем данныеif (FAILED(LoadDataToIStream(szUrl)))
    return INET_E_DOWNLOAD_FAILURE;

  m_pIProtSink = pOIProtSink;
  //информируем о том что есть что отображать
  pOIProtSink->ReportData(BSCF_FIRSTDATANOTIFICATION, 0, m_lSize);
  return S_OK;
}

Сразу после того, как WebBrowser узнал, что данные для загрузки подготовлены, он вызывает метод Read интерфейса IInternetProtocol. Метод будет вызываться до тех пор, пока не будет загружен весь объект. Это обусловлено тем, что браузер подгружает данные порциями, размер которых он указывает в параметре cb этого метода. Нам остается загрузить нужное количество байт в буфер pv и записать фактическое количество прочитанных байтов по адресу, переданному при помощи переменной pcbRead.

Когда метод Read вызывается первый раз, m_lPosition равен 0. Эта переменная указывает на позицию в m_pIStream, откуда будет загружаться объект. С каждым вызовом метода значение в этой переменной будет увеличиваться на количество загруженных байтов. После загрузки очередной партии данных передаем состояние загрузки WebBrowser при помощи уже известного метода ReportData с флагом BSCF_INTERMEDIATEDATANOTIFICATION и возвращаем S_OK. Как только m_lPosition станет равным m_lSize, следует поставить в известность WebBrowser, что объект загружен. Для этого вызываем уже известный нам метод ReportData с параметром BSCF_LASTDATANOTIFICATION. Для корректного завершения загрузки вызываем метод ReportResult с первым параметром S_OK и возвращаем S_FALSE. После вызова метода ReportResult будут вызваны методы IInternetProtocol::LockRequest и IInternetProtocolRoot::Terminate.

ПРЕДУПРЕЖДЕНИЕ

Нужно быть готовым к тому, что метод Read может быть вызван несколько раз после того, как вы закончили загружать объект. В этом случае нужно возвращать S_FALSE

HRESULT STDMETHODCALLTYPE CMyProtocol::Read(
  /* [length_is][size_is][out][in] */void __RPC_FAR *pv,
  /* [in] */ ULONG cb,
  /* [out] */ ULONG __RPC_FAR *pcbRead)
  {
    BOOL Status = FALSE;
    //устанавливает позицию в IStream, с которой будем производить загрузку
    LARGE_INTEGER nLen1;
    nLen1.QuadPart = m_lPosition;
    if(FAILED(m_pIStream->Seek(nLen1, STREAM_SEEK_SET, NULL)))
      return INET_E_DOWNLOAD_FAILURE;
    //загружаем очередную порцию данныхif (SUCCEEDED(m_pIStream->Read(pv, cb, pcbRead)))
    {
      if (*pcbRead != 0)//загрузка произошла успешно
      {
        Status = TRUE;
        //запоминаем позицию, с которой будем загружать данные в//следующий раз
        m_lPosition += *pcbRead;
      }
      else//если произошла ошибка загрузкиreturn S_FALSE;
    }
    ATLTRACE(_T("nBuffer is %ld\n"), *pcbRead);
    if (Status == FALSE) //ошибка загрузкиreturn INET_E_DOWNLOAD_FAILURE;
    elseif (m_lSize - m_lPosition > 0)   //если есть еще что загружать
      {
        if (m_pIProtSink != NULL)
          m_pIProtSink->ReportData(BSCF_INTERMEDIATEDATANOTIFICATION,
            m_lPosition, m_lSize);
        return S_OK;
      }
    if (m_pIProtSink != NULL) //все загружено
    {
      m_pIProtSink->ReportData(BSCF_LASTDATANOTIFICATION, m_lSize, m_lSize);
      m_pIProtSink->ReportResult(S_OK, 0, NULL);
    }
    return S_FALSE;  
}

В завершение работы интерфейс IInternetProtocolRoot вызывает метод Terminate. В этот момент необходимо «освободить» все интерфейсы, ресурсы и т.п., задействованные в процессе работы протокола.

HRESULT STDMETHODCALLTYPE CMyProtocol::Terminate(/* [in] */ DWORD dwOptions)
{
  ATLTRACE(_T("CMyProtocol::Terminate ()\n"));
  m_pIProtSink.Release();
  m_pIStream.Release();
  return S_OK;  
}

Регистрация протокола

К этому моменту мы создали отдельный DLL-файл, с нашим первым интернет-протоколом. Возникает резонное желание проверить, как все это работает. Но не будем спешить. Сначала необходимо рассмотреть механизмы подключения (регистрации имени протокола) и отключения, а для этого нужно создать отдельное приложение. При вызове InitInstance необходимо зарегистрировать протокол, а при вызове ExitInstance - выполнить обратное действие.

Итак:

1. Для регистрации протокола необходимо прибегнуть к методу RegisterNameSpace интерфейcа IInternetSession.

HRESULT RegisterNameSpace(
  //указатель на интерфейс IClassFactory, которая//создает экземпляр объекта реализующего IInternetProtocol
  IClassFactory *pCF,
  //ни что иное как указатель на CLSID, нашего интерфейса
  REFCLSID rclsid,
  //строка и дентифицирующая протокол
  LPCWSTR pwzProtocol,    
  //количество элементов массива ppwzPatterns
  ULONG cPatterns,
  const LPCWSTR *ppwzPatterns,   
  //зарезервировано, должно быть равно нулю
  DWORD dwReserved      
);

2. Этот метод регистрирует протокол только для текущего процесса. Из других процессов данный протокол не будет доступен, если, конечно, не произвести регистрацию протокола в другом процессе. Вы можете зарегистрировать ваш протокол в любой момент времени, вызвав InternetSession::RegisterNameSpace. Этот метод может быть вызван любое количество раз для различных имен протоколов.

3. Отключение протокола осуществляется при помощи метода UnregisterNameSpace того же интерфейса IInternetSession.

HRESULT UnregisterNameSpace(
IClassFactory *pCF,   //адрес интерфейса IClassFactory, в котором был //реализован наш IInternetProtocol
LPCWSTR pszProtocol   //строка, идентифицирующая протокол
);

После знакомства с методами, подключающими протокол к приложению, напишем функцию, которая подключала бы или отключала наш протокол в зависимости от значения параметра bRegister.

      bool CMyDlgApp::HandleMyInternetProtocol(bool bRegister)
{
  // имя подключаемого Internet-протоколаstatic LPCWSTR pszCBT = L"myprot";

  staticchar chGetClassObjectName[] = "DllGetClassObject";
  typedef HRESULT (STDAPICALLTYPE  *GET_CLASS_OBJECT_PROC)(REFCLSID rclsid,
    REFIID riid, LPVOID* ppv);

  //необходимо получить экземпляр интерфейса IInternetSession
  CComPtr<IInternetSession> spIInetSession;
  if (FAILED(CoInternetGetSession(0, &spIInetSession, 0)))
    returnfalse;
  //загружаем DLL, в которой реализован наш протокол
  HMODULE hModule = LoadLibrary(_T("MyInternetProtocol.dll"));
  if (hModule == NULL)
    returnfalse;
  //вызываем 
  GET_CLASS_OBJECT_PROC pfnDllGetClassObject;
  pfnDllGetClassObject = (GET_CLASS_OBJECT_PROC)GetProcAddress(hModule,
    chGetClassObjectName);
  if (pfnDllGetClassObject == NULL)
    returnfalse;
  
  CComPtr <IClassFactory> spIClassFactory;
  HRESULT hr=(pfnDllGetClassObject)(__uuidof(MyProtocol), IID_IClassFactory,
    (void**)&spIClassFactory);
  if (FAILED(hr))
    returnfalse;
  if (bRegister)
    hr = spIInetSession->RegisterNameSpace(spIClassFactory, 
      __uuidof(MyProtocol), pszCBT, 0, NULL, 0);
  else
    hr = spIInetSession->UnregisterNameSpace(spIClassFactory, pszCBT);
  return SUCCEEDED(hr);
}
ПРИМЕЧАНИЕ

В данном листинге рассмотрен нетривиальный способ получения адреса интерфейса IClassFactory, в котором был реализован наш IInternetProtocol. Предполагается, что о реализации нашего протокола система ничего не знает, т.е. в системном реестре нет никаких записей, и вообще регистрации протокола не происходило. Единственное что мы знаем, – что он реализован в файле MyInternetProtocol.dll. Если этот способ сложен для понимания, то можно воспользоваться функцией CoGetClassObject.

Вызовы этой функции будем производить из InitInstance() для регистрации протокола, т.е. bRegister при этом должен быть равным true; ExitInstance() необходимо вызывать с параметром, равным false.

Заключение

Использование Pluggable Protocol позволяет прочитать данные из базы данных или из другого источника, но не все системы поддерживают рассмотренную в этой статье технологию. Например, Macromedia Flash пока поддерживает только протоколы http, ftp и file. Однако надеемся, что список систем, не поддерживающих Pluggable Protocol, с течением времени будет уменьшаться.


Эта статья опубликована в журнале RSDN Magazine #5-2003. Информацию о журнале можно найти здесь
    Сообщений 10    Оценка 721 [+1/-0]         Оценить