Windows CE 5.0 HTTPD <-> .NET Application
I'm what is the most practical way of coupling a HTTPD Web server of a Windows CE 5.0 Device to a .NET application that runs on开发者_Go百科 the same Windows CE device?
My first idea was to build an ISAPI extension that would forward incoming http requests to a .NET application. Not sure how to do it! May be shared memory, COM, TCP/IP Sockets?
Another way could be, implementing a standalone HTTP server within the .NET application itself and avoid using the HTTPD.
Any experience or ideas?
Thanks Chris
The key to running .NET code in the CE web server is to load the server dll into a .NET process. I did a proof of concept a few years back to demonstrate this.
The design may look a bit convoluted at first sight, but has several advantages:
- .NET code and unmanaged ISAPI extensions can run side by side in the web server
- Web server features like encryption and authentication still work
- Resources continue to be managed by the web server, including the thread pool
- Performance-wise probably beats any solution based on separate processes and IPC
First off, we'll need to prevent Windows CE from starting the web server automatically. Add this to the registry:
[HKEY_LOCAL_MACHINE\Services\HTTPD]
"Flags"=dword:4 ; DEVFLAGS_NOLOAD
While we're at it, add another key to map '/dotnet' to our custom ISAPI handler:
[HKEY_LOCAL_MACHINE\Comm\HTTPD\VROOTS\/dotnet]
@="\\Windows\\HttpdHostUnmanaged.dll"
Now create a .NET exe called HttpdHostProc.exe from the following source code:
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;
}
A few comments:
- The Main function loads and initialises our unmanaged dll, which will act as a stepping stone between managed and unmanaged code
- It then initialises and starts the web server by calling its HTP_Init function, followed by a couple of ioctls
- Finally, it calls RunHttpd in our unmanaged dll, which will accept incoming requests and forward them to the web server.
The three functions further down - GetExtensionVersion, TerminateExtension, HttpExtensionProc - should look familiar if you've ever done any ISAPI programming. If not, all you really need to know is that incoming requests are handled by HttpExtensionProc.
Moving on to the unmanaged 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;
}
There are a number of not terribly interesting functions in there that forward calls to and from .NET.
As mentioned above, the RunHttpd function just accepts incoming connections and passes them to the web server for processing by means of another ioctl.
To test it all, launch HttpdHostProc.exe on the device, then open http://<ipaddr>/dotnet
in a browser. The CE device should respond with a bit of HTML containing the message "Hello .NET!".
This code runs on Windows Embedded Compact 7.0 with .NET Compact Framework 3.5, but would probably work on other versions as well.
I built the unmanaged dll against the Pocket PC 2003 SDK, since that's what I happened to have installed. Probably any Windows CE SDK would do, but you might have to adjust compiler settings, for instance I had to build with /GS- (buffer security checks disabled) for PPC2003.
It is tempting to implement the RunHttpd function in .NET as well, but be warned there are a couple of potential issues with that:
- In my testing, the Handle property on a .NET CF socket returned a sort of pseudo-handle, which did not work with native socket APIs
- The lifetime of the socket would be managed by the .NET runtime, making it particularly difficult to pass ownership of the socket to the web server
If you don't mind compiling with /unsafe, performance could probably be improved slightly by passing fixed buffers to WriteClient, rather than copying all the response data to an unmanaged buffer in HttpExtensionProc.
The EXTENSION_CONTROL_BLOCK structure contains a number of useful fields and functions that obviously would need to be included in a full implementation.
EDIT
Just to clarify how requests are handled:
- Incoming requests are accepted in RunHttpd, which forwards them to the web server using an ioctl
- As per the vroot entry in the registry that we set up earlier, the web server calls into HttpdHostUnmanaged.dll to handle requests for /dotnet
- If this is the first request for /dotnet, the web server starts by calling the unmanaged version of GetExtensionVersion in HttpdHostUnmanaged.dll. The unmanaged GetExtensionVersion calls back into the .NET version of GetExtensionVersion. GetExtensionVersion is a conveninent place to initialise any resources required, as it is only called once (the corresponding clean-up function is TerminateExtension, which is called when/if the web server decides to unload HttpdHostUnmanaged.dll)
- Next, the web server calls the unmanaged HttpExtensionProc
- The unmanaged HttpExtensionProc calls back into the .NET version of HttpExtensionProc
- The managed HttpExtensionProc generates the response, and calls the unmanaged WriteClient to deliver it to the client
In my opinion, based on trying to use the built-in HTTPD server in the past, is that the built-in server absolutely sucks for trying to do anything useful. It's a major headache to debug anything and interop with any device hardware/system is painful.
Since there is no EE Hosting support in the CF, the web server cannot load managed assemblies (from ISAPI or anything else). That means your managed code has to be in a separate process and to communicate you have to use IPC - something like a P2PMessageQueue, MemoryMappedFile, socket, etc.
Writing your own HTTPD server is also an option, but it's not a trivial task. It sounds simple, but there's a lot involved once you dive into it. I know that because we made that decision several years ago on a project, and the server we ended up creating supported a subset of ASP.NET and we actually turned it into a commercial product because a) it was really useful and b) becasue it took a lot of work to actually write. It does, however, give that benefit of hosting managed code and being able to debug in Studio instead of printf as a managed developer expects.
精彩评论