Windows CE 5.0 HTTPD <-> .NET Application



将Windows CE 5.0设备的HTTPD Web服务器耦合到在同一Windows CE设备上运行的.NET应用程序的最实用方法是什么?

我的第一个想法是构建一个ISAPI扩展,将传入的http请求转发到.NET应用程序。不知道怎么做!可能是共享内存、COM、TCP/IP套接字?

另一种方法是,在.NET应用程序本身中实现一个独立的HTTP服务器,并避免使用HTTPD。

有什么经验或想法吗?

谢谢Chris

在CE web服务器中运行.NET代码的关键是将服务器dll加载到.NET进程中。几年前,我做了一个概念验证来证明这一点。

乍一看,这个设计可能有点复杂,但有几个优点:

  • .NET代码和非托管ISAPI扩展可以在web服务器中并行运行
  • Web服务器的加密和身份验证等功能仍然有效
  • 资源继续由web服务器管理,包括线程池
  • 性能可能胜过任何基于单独流程和IPC的解决方案

首先,我们需要防止Windows CE自动启动web服务器。将此添加到注册表:

[HKEY_LOCAL_MACHINEServicesHTTPD]
    "Flags"=dword:4  ; DEVFLAGS_NOLOAD

当我们在做的时候,添加另一个键来将"/dotnet"映射到我们的自定义ISAPI处理程序:

[HKEY_LOCAL_MACHINECommHTTPDVROOTS/dotnet]
    @="\Windows\HttpdHostUnmanaged.dll"

现在从以下源代码创建一个名为HttpdHostProc.exe的.NET exe:

using System;
using System.Runtime.InteropServices;
using System.Text;
class HttpdHostProc
{
    static void Main(string[] args)
    {
        GetExtensionVersionDelegate pInit =
            new GetExtensionVersionDelegate(GetExtensionVersion);
        TerminateExtensionDelegate pDeinit
            = new TerminateExtensionDelegate(TerminateExtension);
        HttpExtensionProcDelegate pProc =
            new HttpExtensionProcDelegate(HttpExtensionProc);
        Init(pInit, pDeinit, pProc);
        int state = SERVICE_INIT_STOPPED | SERVICE_NET_ADDR_CHANGE_THREAD;
        int context = HTP_Init(state);
        HTP_IOControl(context, IOCTL_SERVICE_REGISTER_SOCKADDR,
             IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero);
        HTP_IOControl(context, IOCTL_SERVICE_STARTED,
             IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero);
        RunHttpd(context, 80);
    }
    static int GetExtensionVersion(IntPtr pVer)
    {
        OutputDebugString("GetExtensionVersion from .NETrn");
        return 1;
    }
    static int TerminateExtension(int dwFlags)
    {
        OutputDebugString("TerminateExtension from .NETrn");
        return 1;
    }
    static int HttpExtensionProc(IntPtr pECB)
    {
        OutputDebugString("HttpExtensionProc from .NETrn");
        var response = "<html><head></head><body><p>Hello .NET!</p></body></html>";
        var bytes = Encoding.UTF8.GetBytes(response);
        var length = bytes.Length;
        var unmanagedbuffer = Marshal.AllocHGlobal(length);
        Marshal.Copy(bytes, 0, unmanagedbuffer, length);
        var retval = WriteClient(pECB, unmanagedbuffer, ref length);
        Marshal.FreeHGlobal(unmanagedbuffer);
        return retval;
    }
    delegate int GetExtensionVersionDelegate(IntPtr pVer);
    delegate int TerminateExtensionDelegate(int dwFlags);
    delegate int HttpExtensionProcDelegate(IntPtr pECB);
    [DllImport("HttpdHostUnmanaged.dll", SetLastError = true)]
    extern static void Init(
        GetExtensionVersionDelegate pInit,
        TerminateExtensionDelegate pDeinit,
        HttpExtensionProcDelegate pProc
    );
    [DllImport("HttpdHostUnmanaged.dll", SetLastError = true)]
    extern static int RunHttpd(int context, int port);
    [DllImport("HttpdHostUnmanaged.dll", SetLastError = true)]
    extern static int WriteClient(IntPtr pECB, IntPtr Buffer, ref int lpdwBytes);
    [DllImport("coredll.dll")]
    extern static void OutputDebugString(string msg);
    [DllImport("httpd.dll", SetLastError = true)]
    extern static int HTP_Init(int dwData);
    [DllImport("httpd.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    extern static bool HTP_IOControl(int dwData, int dwCode, IntPtr pBufIn,
        int dwLenIn, IntPtr pBufOut, int dwLenOut, IntPtr pdwActualOut);
    const int IOCTL_SERVICE_STARTED = 0x01040038;
    const int IOCTL_SERVICE_REGISTER_SOCKADDR = 0x0104002c;
    const int SERVICE_INIT_STOPPED = 0x00000001;
    const int SERVICE_NET_ADDR_CHANGE_THREAD = 0x00000008;
}

一些评论:

  • Main函数加载并初始化我们的非托管dll,它将充当托管代码和非托管代码之间的垫脚石
  • 然后,它通过调用HTP_Init函数初始化并启动web服务器,然后调用几个ioctl
  • 最后,它在我们的非托管dll中调用RunHttpd,它将接受传入的请求并将其转发到web服务器

如果你曾经做过任何ISAPI编程,那么下面的三个函数——GetExtensionVersion、TerminateExtension和HttpExtensionProc——看起来应该很熟悉。如果不是,那么您真正需要知道的就是传入请求由HttpExtensionProc处理。

转到非托管dll,HttpdHostUnmanaged.dll:

#include <winsock2.h>
#include <httpext.h>
typedef BOOL (* pfHTP_IOControl)(DWORD dwData, DWORD dwCode, PBYTE pBufIn,
    DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD pdwActualOut);
typedef BOOL (* PFN_WRITE_CLIENT)(HCONN ConnID, LPVOID Buffer,
    LPDWORD lpdwBytes, DWORD dwReserved);
static PFN_GETEXTENSIONVERSION g_pInit;
static PFN_TERMINATEEXTENSION g_pDeinit;
static PFN_HTTPEXTENSIONPROC g_pProc;
BOOL APIENTRY DllMain(HANDLE, DWORD, LPVOID)
{
    return TRUE;
}
extern "C" void Init(
    PFN_GETEXTENSIONVERSION pInit,
    PFN_TERMINATEEXTENSION pDeinit,
    PFN_HTTPEXTENSIONPROC pProc)
{
    // Store pointers to .NET delegates
    g_pInit = pInit;
    g_pDeinit = pDeinit;
    g_pProc = pProc;
}
extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
    pVer->dwExtensionVersion = HSE_VERSION;
    strncpy(pVer->lpszExtensionDesc, "HttpdHostUnmanaged",
        HSE_MAX_EXT_DLL_NAME_LEN);
    // Call .NET GetExtensionVersion
    return g_pInit(pVer);
}
extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags)
{
    // Call .NET TerminateExtension
    return g_pDeinit(dwFlags);
}
extern "C" DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
    // Call .NET HttpExtensionProc
    return g_pProc(pECB);
}
extern "C" DWORD WINAPI WriteClient(EXTENSION_CONTROL_BLOCK *pECB,
    LPVOID Buffer, LPDWORD lpdwBytes)
{
    return pECB->WriteClient(pECB->ConnID, Buffer, lpdwBytes, 0);
}
extern "C" int WINAPI RunHttpd(DWORD context, int port)
{
    // Load web server and start accepting connections.
    // When a connection comes in,
    // pass it to httpd using IOCTL_SERVICE_CONNECTION.
    HMODULE hDll = LoadLibrary(L"httpd.dll");
    if(!hDll)
    {
        return -1;
    }
    pfHTP_IOControl Ioctl =
        (pfHTP_IOControl)GetProcAddress(hDll, L"HTP_IOControl");
    if(!Ioctl)
    {
        FreeLibrary(hDll);
        return -2;
    }
    WSADATA Data;
    int status = WSAStartup(MAKEWORD(1, 1), &Data);
    if(status != 0)
    {
        FreeLibrary(hDll);
        return status;
    }
    SOCKET s = socket(PF_INET, SOCK_STREAM, 0);
    if(s == INVALID_SOCKET)
    {
        status = WSAGetLastError();
        goto exit;
    }
    SOCKADDR_IN sAddr;
    memset(&sAddr, 0, sizeof(sAddr));
    sAddr.sin_port = htons(port);
    sAddr.sin_family = AF_INET;
    sAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    if(bind(s, (LPSOCKADDR)&sAddr, sizeof(sAddr)) == SOCKET_ERROR)
    {
        status = WSAGetLastError();
        goto exit;
    }
    if(listen(s, SOMAXCONN) == SOCKET_ERROR)
    {
        status = WSAGetLastError();
        goto exit;
    }
    for(;;)
    {
        SOCKADDR_IN addr;
        int cbAddr = sizeof(addr);
        SOCKET conn = accept(s, (LPSOCKADDR)&addr, &cbAddr);
        if(conn == INVALID_SOCKET)
        {
            status = WSAGetLastError();
            goto exit;
        }
        DWORD IOCTL_SERVICE_CONNECTION = 0x01040034;
        Ioctl(context, IOCTL_SERVICE_CONNECTION,
            (PBYTE)&conn, sizeof(conn), NULL, 0, NULL);
    }
exit:
    FreeLibrary(hDll);
    if(s != INVALID_SOCKET)
    {
        closesocket(s);
    }
    WSACleanup();
    return status;
}

这里有许多不太有趣的函数,它们将调用转发到.NET.和从.NET.转发调用

如上所述,RunHttpd函数只接受传入的连接,并将它们传递给web服务器,以便通过另一个ioctl进行处理。

要测试所有内容,请在设备上启动HttpdHostProc.exe,然后在浏览器中打开http://<ipaddr>/dotnet。CE设备应该用包含消息"Hello.NET!"的HTML进行响应。

此代码在带有.NET Compact Framework 3.5的Windows Embedded Compact 7.0上运行,但可能也适用于其他版本。

我根据Pocket PC 2003 SDK构建了非托管dll,因为我碰巧安装了它。可能任何Windows CE SDK都可以,但您可能必须调整编译器设置,例如,我必须使用/GS-(禁用缓冲区安全检查)为PPC2003构建。

在.NET中实现RunHttpd函数也是很诱人的,但请注意,这有几个潜在的问题:

  • 在我的测试中,.NETCF套接字上的Handle属性返回了一种伪句柄,它不适用于本机套接字API
  • 套接字的生存期将由.NET运行时管理,这使得将套接字的所有权传递给web服务器变得特别困难

如果您不介意使用/unsafe进行编译,那么通过将固定缓冲区传递给WriteClient,而不是将所有响应数据复制到HttpExtensionProc中的非托管缓冲区,性能可能会略有提高。

EXTENSION_CONTROL_BLOCK结构包含许多有用的字段和函数,这些字段和函数显然需要包含在完整的实现中。

编辑

只是为了澄清如何处理请求:

  1. 传入请求在RunHttpd中被接受,后者使用ioctl将它们转发到web服务器
  2. 根据我们之前设置的注册表中的vroot条目,web服务器调用HttpdHostUnmanaged.dll来处理/dotnet的请求
  3. 如果这是对/dotnet的第一个请求,则web服务器将通过调用HttpdHostUnmanaged.dll中的GetExtensionVersion的非托管版本来启动。非托管GetExtensionVersion调用回GetExtensionVersion.NET版本。GetExtensionVersion是初始化任何所需资源的方便位置,因为它只调用一次(相应的清理函数是TerminateExtension,当/如果web服务器决定卸载HttpdHostUnmanaged.dll时调用它)
  4. 接下来,web服务器调用非托管的HttpExtensionProc
  5. 非托管的HttpExtensionProc回调到.NET版本的HttpExtensionProc
  6. 托管HttpExtensionProc生成响应,并调用非托管WriteClient将其传递给客户端

在我看来,基于过去尝试使用内置HTTPD服务器,内置服务器尝试做任何有用的事情都非常糟糕。调试任何东西都是一件令人头疼的事,而与任何设备硬件/系统的互操作都是令人痛苦的。

由于CF中不支持EE宿主,因此web服务器无法加载托管程序集(来自ISAPI或其他任何程序集)。这意味着你的托管代码必须在一个单独的过程中,并且为了进行通信,你必须使用IPC——比如P2PMessageQueue、MemoryMappedFile、socket等。

编写自己的HTTPD服务器也是一种选择,但这不是一项琐碎的任务。这听起来很简单,但一旦你深入研究,就会涉及到很多。我知道这一点,因为我们几年前在一个项目上做出了这个决定,而我们最终创建的服务器支持ASP的一个子集。NET,我们实际上把它变成了一个商业产品,因为a)它真的很有用,b)因为它需要大量的工作来实际编写。然而,它确实提供了托管托管代码的好处,并且能够在Studio中调试,而不是像托管开发人员所期望的那样在printf中调试。

相关内容

  • 没有找到相关文章

最新更新