Friday, February 05, 2010

In the last article we looked at setting up Padarn for using custom HttpHandlers.  In this article we'll look at exactly how we use a custom handler to get data.

The GET Service

Probably the most common HTTP method is the GET method.  It's what your browser used to pull the content of this page, and it's what our REST service will use to get back a list of all Books in our database.

First, I create an entity that defines a Book.  This code file will actually get shared between the device and desktop projects to make things simple.

public class Book
{
  public int ID { get; set; }
  public string Title { get; set; }
  public string Author { get; set; }
  public int? Pages { get; set; }

  public static Book FromXml(string xml)
  {
    // omitted for brevity
  }

  public string AsXml()
  {
    // omitted for brevity
  }
}

I've left out the XML serialization/deserialization routines because they take up a lot of space and aren't really that interesting.  They're in the example source (I'll post it when I get these articles up - be patient) if you want to see them.

So next I need a way to get a list of these from my Database.  To encapsulate all of this logic, I created a DatabaseConnector singleton class that resides in the same assembly as the HttpHandlers.  Remember, these handlers are stateless and get created and called every time a URL is requested, so it would be pointless to put a connector instance in the handler itself.

The DatabaseConnector exposes a GetAllBooks method:

public Book[] GetAllBooks()
{
  string sql = "SELECT BookID, Title, Author, Pages FROM Books";
  List<Book> books = new List<Book>();

  using(SqlCeCommand cmd = new SqlCeCommand(sql, Connection))
  using(var resultset = cmd.ExecuteResultSet(ResultSetOptions.Scrollable))
  {
    if (resultset.HasRows)
    {
      while (resultset.Read())
      {
        books.Add(new Book
        {
          ID = resultset.GetInt32(0),
          Title = resultset.GetString(1),
          Author = resultset.GetString(2),
          Pages = resultset.IsDBNull(3) ? null : (int?)resultset.GetInt32(3)
        });
      }
    }
  }

  return books.ToArray();
}

Now all that I need to do is call this from my GetHandler like so:

public class GetHandler : BaseHandler
{
public override void ProcessRequest(HttpContext context)
{
  string entity = GetEntityName(context.Request.Path);

  // the only entity we support is "Books"
  if (string.Compare(entity, "books", true) != 0)
  {
    throw new HttpException(HttpErrorCode.NotFound, string.Format("Entity '{0}' not supported", entity));
  }

  Book[] books = DataConnector.GetInstance().GetAllBooks();

  StringBuilder sb = new StringBuilder(XML_HEADER);
  sb.Append("<books>");

  foreach (var b in books)
  {
    sb.Append(b.AsXml());
  }

  sb.Append("</books>");

  context.Response.Write(sb.ToString());
  context.Response.Flush();
}

That's all there is to it.  We now have implemented a REST service to get all Books from our Windows CE device.  When the Padarn server sees a GET request at http://<ip address>/books/ it will return an XML stream of all of the books in the database.  Here's an example of the output:

<?xml version="1.0" encoding="UTF-8"?>
<books>
  <book>
    <id>1</id>
    <title>The Count of Monte Cristo</title>
    <author>Alexandre Dumas</author>
    <pages>1573</pages>
  </book>
  <book>
    <id>2</id>
    <title>Programming WCF Services</title>
    <author>Juval Loewy</author>
    <pages>610</pages>
  </book>
</books>

The GET Client

Now let's look at how a non-browser client (i.e. a desktop application) might make use of this service.

First, since I like abstraction and encapsulation, I created a class named RestConnector that handles all communications for REST services and a class named BookClient who's job is to handle nothing but interactions with the Books service.  RestConnector handles any GET requirements through the following methods:

public string Get(string directory)
{
  string page = string.Format("http://{0}/{1}", DeviceAddress, directory);

  StringBuilder sb = new StringBuilder();

  byte[] buf = new byte[8192];

  HttpWebRequest request = (HttpWebRequest)WebRequest.Create(page);

  HttpWebResponse response = (HttpWebResponse)request.GetResponse();

  return GetResponseData(response);
}

private string GetResponseData(HttpWebResponse response)
{
  StringBuilder sb = new StringBuilder();

  byte[] buf = new byte[8192];

  Stream stream = response.GetResponseStream();

  string result = null;
  int count = 0;

  do
  {
    count = stream.Read(buf, 0, buf.Length);

    if (count != 0)
    {
      // look for a UTF8 header
      if ((buf[0] == 0xEF) && (buf[1] == 0xBB) && (buf[2] == 0xBF))
      {
        result = Encoding.UTF8.GetString(buf, 3, count - 3);
      }
      else
      {
        result = Encoding.UTF8.GetString(buf, 0, count);
      }
      sb.Append(result);
    }
  } while (count > 0);

  return sb.ToString();
}

The BookClient uses the Get method to retrieve the XML for a list of books from the service and then deserializes that XML like this:

public Book[] GetAllBooks()
{
  List<Book> books = new List<Book>();

  string xml = Connector.Get(ENTITY_NAME);

    XmlDocument doc = new XmlDocument();
  doc.LoadXml(xml);

  foreach (XmlNode node in doc.SelectNodes("books/book"))
  {
    books.Add(Book.FromXml(node.OuterXml));
  }

  return books.ToArray();
}

It's really that simple.  At this point we have a simple array of Book instances passed from our service to our desktop client so we can do whatever we'd like with them (I'm just displaying them).

Up next: Using POST to add a new entity

2/5/2010 5:51:01 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 

I've spent a bit of time lately adding HttpHandler support to our Padarn Web Server for Windows CE, and today I just wrapped up the code for a pretty in-depth example for using it to host RESTful Web Services.  I'm going to give some highlights on how it works here in my blog as well, because while it's remarkably easy to get working, there's still a lot going on.

Background

The general idea is to have a database on a Windows CE device and a client application on the desktop that could access that database via a Web Service hosted on the device.  I want to be able to do all of the CRUD operations through the service, and nicely these to the HTTP methods:

POST - Create a new entity in the database
PUT - Update an existing entity in the database
GET - Read an entity (or list of entities) from the database
DELETE - Delete an entity from the database

To make things simple, I created a very simple database that contains only one table.  The simple data model isn't real-world, but if you can make it work for one table, making it work for your entire database is really just an extension of the code base.

Setting Up Padarn

I'm going to use custom HttpHandlers to act as the service entry points.  HttpHandlers in Padarn look and work just like they do under IIS, the only difference is how they get "installed:. For Padarn, the handlers are configured, like everything else, in Padarn's configuration file.  To make my life simpler, and the code easier to read, I'll create a separate handler for each separate HTTP method, so one for GET, one for POST and so on.  This is how they get registered with the server:

<httpHandlers>
  <assembly>SampleSite.dll</assembly>
  <add verb="GET" path="/*" type="SampleSite.Handlers.GetHandler, SampleSite"/>
  <add verb="PUT" path="/*" type="SampleSite.Handlers.PutHandler, SampleSite"/>
  <add verb="POST" path="/*" type="SampleSite.Handlers.PostHandler, SampleSite"/>
  <add verb="DELETE" path="/*" type="SampleSite.Handlers.DeleteHandler, SampleSite"/>
</httpHandlers>

Pretty simple. the "add" nodes simply say what method/verb in any given server path maps to a given class.  Padarn uses regex to parse the path, so the first add line basically says "for GET methods to any path on the server, call into an instance of a GetHandler class".  Since the GetHandler is in a separate assembly from my hosting executable, Padarn needs to know about that assembly so it can load it at run time.  The assembly node simply tells Padarn to load up that assembly before it tries creating any custom handler class instances.

Once this is configured, the only thing left to do is to implement the handlers.  An HttpHandler is really simple - it contains a single overriden method that gets called when a page is requested.  Here's what the GetHandler stub looks like:

namespace SampleSite.Handlers
{
  public class GetHandler : BaseHandler
  {
    public override void ProcessRequest(HttpContext context)
    {
      // do stuff here
    }
  }
}

I get in an HttpContext, which has both the HttpRequest and HttpResponse objects. Note how we're trying very hard to mirror the object model Microsoft uses to make the learning curve much easier and to make your code more portable.  The HttpRequest contains, among other things, the incoming path and data.  The HttpResponse is where we write back out our data. 

SImple enough so far, right?  Next up: getting data from our RESTful service.
 

2/5/2010 5:18:58 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Wednesday, July 01, 2009

It's now easier than ever to evaluate Padarn, our ASP.NET Web Server for Windows CE.  We now have available an Evaluation Kit that comes with:

  • A 600MHz fanless Windows CE 6.0 device
  • 13 hands-on labs with full source code
  • A full sample Padarn web site with source code
  • Fully-functional Padarn evaluation binaries

The kit is being offered at an introductory price of $300.00, and if that isn't enough to convince you, we'll even credit that against any purchase of Padarn you make.

For more information, see the Padarn web site.

7/1/2009 4:06:15 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Monday, June 29, 2009

The latest version of our Padarn Mobile Server supports VirtualPathProviders.  This means that now it is easy to create REST Web Services that run on your Windows CE or Windows Mobile device.  Download the latest hands-on lab "HOL P303 - Using VirtualPathProviders in Padarn to provide REST Web Services" from the web site here.

6/29/2009 1:31:39 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Wednesday, May 13, 2009

We've been doing a ton of internal work lately on our Padarn ASP.NET Web Server for Windows CE - both on features as well as just the general stuff you have to do to have a product.  Part of this work was to create a series of hands-on labs to demonstrate how to use Padarn effectively.  The PDFs (a dozen in all so far), along with the latest Developer's Guide are available for public viewing on the Padarn Downloads page.

5/13/2009 7:47:53 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Thursday, May 29, 2008

We're just about to release a new version of Padarn, out ASP.NET Web Server for Windows CE.  This version brings SSL support as well as Basic and Digest authentication.  Another part of this release was trying to keep the footprint reasonably small.  Here's a screen shot of all of the assemblies needed for the entire engine implementation:

padarnfootprint.PNG

So yes, we have an ASP.NET web server with SSL and authentication support and it's just 305KB - and half of that is for SSL alone.

 

5/29/2008 12:26:56 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0]  | 
 Monday, January 21, 2008
A customer asked us to integrate a new camera with Padarn recently (the D-Link DCS-900) to try to do a similar thing that I had done in my PTZ camera demo.  Well this camera was a little different and a whole lot easier to get "streaming" data from.  I ended up writing a small wrapper class that takes the MJPG data coming from the camera, parses it into in-memory Image classes, and then those are available for use.

Once I had that I figured it would be interesting to serve up a Padarn page that did nothing but return the latest frame data from that camera - after all the camera doesn't support PTZ and just a button that takes a picture didn't seem terribly new or informative. 

The problem was that the version of Padarn I was using (1.0.5020) didn't support writing a stream of bytes to the Response.  Well as of 1.0.5030 Padarn now supports WriteBytes (and a few other things like CacheBehavior) and I created a page that dynamically loads the images into an on-page IFrame and allows you to adjust the refresh rate dynamically without ever having to send a full page request in again.

I can see that this could be useful for dynamic updates to a page for something like a control system gauge or for animating building automation status items.

Check out the new sample here.

The code for the DCS-900 looks like this (notice that the ICamera interface changed a little, so the PTZ camera also had to be altered.  I simply stubbed out the new methods).

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Threading;
using System.IO;
using System.Net;
using System.Net.Sockets;
using OpenNETCF.Peripherals.Camera;

namespace OpenNETCF.Peripherals
{
  public class DCS900 : ICamera
  {
    private Image m_lastImage = null;
    private volatile bool m_stopThread = true;
    Thread m_grabberThread;
    private IPAddress m_ip;

    public DCS900()
    {
    }

    public void Initialize(IPAddress ipAddress, string userName, string password)
    {
        m_ip = ipAddress;
    }

    public bool SupportsPanTilt
    {
        get { return false; }
    }

    private int FindFrameBoundary(byte[] data, int offset, int maxLength)
    {
        byte[] frameDelimiter = Encoding.ASCII.GetBytes("--video boundary--");
        int n = 0;
        bool match;

        for (; offset < maxLength; offset++)
        {
            match = true;
            if (data[offset] == frameDelimiter[n])
            {
                match &= true;
                n++;
                if (n >= frameDelimiter.Length)
                {
                    // we've found the header
                    return offset + 1;
                }
            }
            else
            {
                match = false;
                n = 0;
            }
        }

        return -1;
    }

    private Image ImageFromFrameData(byte[] data, int frameLength)
    {
        int start = 50;

        // find the data start - it will be between 50 and 54 bytes in from the frame start
        while (data[start] != 0xFF)
        {
            start++;
            if (start > 54) return null;
            if (data[start + 1] == 0xD9) break;
        }

        try
        {
            using (MemoryStream stream = new MemoryStream(data, start, frameLength - start))
            {
                Image image = new Bitmap(stream);
                stream.Close();
                return image;
            }
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.Message);
        }

        return null;
    }

    public void Start()
    {
        m_grabberThread = new Thread(GrabberThreadProc);
        m_grabberThread.IsBackground = true;
        m_grabberThread.Start();
    }

    public void Stop()
    {
        m_stopThread = true;
    }

    public bool IsRunning
    {
        get { return !m_stopThread; }
    }

    private void GrabberThreadProc()
    {
        lock (m_grabberThread)
        {
            m_stopThread = false;

            Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            s.Connect(new IPEndPoint(m_ip, 80));

            string header = "GET /VIDEO.CGI HTTP/1.0\r\nUser-Agent: user\r\nAuthorization: Basic YWRtaW46REVVU1Q=\r\n\r\n";
            byte[] data = Encoding.ASCII.GetBytes(header);
            byte[] image = new byte[32768]; // 32k buffer is more than adequate for this camera

            s.Send(data);

            data = new byte[2048];

            int offsetStart = 0;
            int offsetEnd = 0;
            int ptr = 0;

            int length = s.Receive(data);

            do
            {
                offsetStart = FindFrameBoundary(data, 0, length);
            } while (offsetStart < 0);

            while (!m_stopThread)
            {
                offsetEnd = FindFrameBoundary(data, offsetStart, length);

                while (offsetEnd < 0)
                {
                    int copyLength = length - offsetStart;
                    Buffer.BlockCopy(data, offsetStart, image, ptr, copyLength);
                    offsetStart = 0;
                    ptr += copyLength;

                    length = s.Receive(data);
                    offsetEnd = FindFrameBoundary(data, offsetStart, length);
                }

                if (ptr > 50)
                {
                    Buffer.BlockCopy(data, offsetStart, image, ptr, offsetEnd - 18);
                    ptr += offsetEnd - 18;

                    m_lastImage = ImageFromFrameData(image, ptr);

                }
                offsetStart = offsetEnd;
                ptr = 0;

                Thread.Sleep(200);
            }

            s.Close();
        }
    }

    public Image GetImage()
    {
        return m_lastImage;
    }

    public void Pan(PanDirection direction)
    {
        throw new NotSupportedException();
    }

    public void Tilt(TiltDirection direction)
    {
        throw new NotSupportedException();
    }
  }
}


1/21/2008 7:56:46 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Thursday, December 13, 2007
Today we rolled out an update to Padarn: version 1.0.5010.

"So what's new," you ask? 

  1. We added support for the Page.Request.Browser property.  On the surface it seemed quite simple, but the implementation was actually quite a challenge.
  2. We fixed a bug that Page.Request.Headers always came back null
  3. We updated the SampleSite demo including:
    1. Updates and additions to the OpenNETCF.Web.Html namespace
    2. A new Camera Demo page that controls a camera in our "server closet"
    3. A page showing hosting the WMP control service media off of Padarn
    4. A page that shows client browser capability detection
The docs and live page are updated - you can reach them both from here:  www.opennetcf.com/padarn.ocf


We are also now offering Padarn free of charge for academic use.  Contact us for details.

12/13/2007 7:26:16 PM (Eastern Standard Time, UTC-05:00)  #    Comments [2]  |