Windows CE 5.0 HTTPD <-> .NET Application

4

我想知道在Windows CE 5.0设备上如何将HTTPD Web服务器与运行在同一设备上的.NET应用程序进行最实用的耦合方式是什么?

我的第一个想法是构建一个ISAPI扩展,将传入的http请求转发到.NET应用程序。不确定如何实现!可能需要使用共享内存、COM、TCP/IP套接字?

另一种方法可能是,在.NET应用程序内部实现一个独立的HTTP服务器,避免使用HTTPD。

有任何经验或想法吗?

谢谢 Chris

2个回答

3
在CE web服务器中运行.NET代码的关键是将服务器DLL加载到.NET进程中。几年前,我进行了一个概念验证来证明这一点。
设计可能乍一看有些复杂,但具有以下几个优点:
- .NET代码和非托管ISAPI扩展可以在Web服务器中并行运行 - Web服务器功能(如加密和身份验证)仍然有效 - 资源继续由Web服务器管理,包括线程池 - 在性能方面,可能比基于单独进程和IPC的任何解决方案都要好
首先,我们需要防止Windows CE自动启动Web服务器。将此添加到注册表中:
[HKEY_LOCAL_MACHINE\Services\HTTPD]
    "Flags"=dword:4  ; DEVFLAGS_NOLOAD

顺便说一下,将另一个键映射到我们自定义的ISAPI处理程序以映射'/dotnet':

[HKEY_LOCAL_MACHINE\Comm\HTTPD\VROOTS\/dotnet]
    @="\\Windows\\HttpdHostUnmanaged.dll"

现在从以下源代码创建一个名为HttpdHostProc.exe的.NET可执行文件:
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 .NET\r\n");
        return 1;
    }

    static int TerminateExtension(int dwFlags)
    {
        OutputDebugString("TerminateExtension from .NET\r\n");
        return 1;
    }

    static int HttpExtensionProc(IntPtr pECB)
    {
        OutputDebugString("HttpExtensionProc from .NET\r\n");

        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服务器,并进行了一些ioctls操作
  • 最后,它调用我们的非托管dll中的RunHttpd函数,该函数将接受传入请求并将其转发到Web服务器。

下面的三个函数 - GetExtensionVersion、TerminateExtension、HttpExtensionProc - 如果您曾经进行过任何ISAPI编程,应该会很熟悉。如果没有,请知道传入的请求将由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之间进行调用。

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

要测试它,请在设备上启动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函数,但请注意,这可能存在一些潜在问题:

  • 在我的测试中,.NET CF套接字上的Handle属性返回一种伪句柄,无法与本机套接字API一起使用
  • 套接字的生命周期将由.NET运行时管理,使得将套接字所有权传递给Web服务器变得特别困难

如果您不介意使用/unsafe编译,则通过向WriteClient传递固定缓冲区,而不是将所有响应数据复制到HttpExtensionProc中的未经管理的缓冲区中,可能可以略微提高性能。

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

编辑

仅澄清如何处理请求:

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

非常感谢您分享您的天才。您简直无法想象这对今天仍然非常有用!绝对应该得到奖励。由于我被迫使用httpd提供一些服务,我的唯一选择是放弃紧凑框架或在第二个端口上进行监听,而这正是完美的解决方案,设计非常出色,并且只有少量未经管理的内容。现在我已经将httpd服务与我的自定义.net扩展全部放在了80端口上。我会尽快添加一些可能对他人有用的详细信息。 - matpop
@matpop,非常感谢。在连微软似乎都放弃了WinCE的时候,知道还有一个地方可以容纳这样的创意真是太棒了。 - Carsten Hansen

2
在我看来,基于之前尝试使用内置的HTTPD服务器的经验,内置服务器对于想要做任何有用的事情来说都非常糟糕。调试任何问题和与任何设备硬件/系统的互操作性都是一个巨大的头痛。
由于CF不支持EE Hosting,因此Web服务器无法加载托管程序集(来自ISAPI或其他任何地方)。这意味着您的托管代码必须在单独的进程中,并且要进行通信,您必须使用IPC - 类似于P2PMessageQueue、MemoryMappedFile、socket等。
编写自己的HTTPD服务器也是一种选择,但这并不是一项轻松的任务。听起来很简单,但一旦你深入其中,就会发现有很多需要考虑的东西。我知道这一点,因为几年前我们在一个项目上做出了这个决定,最终创建的服务器支持了ASP.NET的子集,并且我们将其转化为了商业产品,因为a)它非常有用,b)因为实际编写它需要花费大量的工作。不过,它确实具有托管代码和能够在Studio中进行调试的好处,而不是像托管开发人员所期望的那样使用printf。

嗨,克里斯,感谢您的快速回复。我猜原生代码到托管代码的桥接可能会引起一些真正的麻烦。我还阅读了您在Windows CE上关于RESTful服务的博客文章 - 非常好! - Chris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接