Step 3: Writing an Agent-Based Web Server

Applies to: Functional Programming

Authors: Tomas Petricek and Jon Skeet

Referenced Image

Get this book in Print, PDF, ePub and Kindle at manning.com. Use code “MSDN37b” to save 37%.

Summary: This step shows how to use the HttpListener type to create a type that can be used for developing an agent-based HTTP web server.

This topic contains the following sections.

  • Developing an HTTP Server Agent
  • Simplifying Types for Web Programming
  • Creating a Hello World Server
  • Summary
  • Additional Resources
  • See Also

This article is associated with Real World Functional Programming: With Examples in F# and C# by Tomas Petricek with Jon Skeet from Manning Publications (ISBN 9781933988924, copyright Manning Publications 2009, all rights reserved). No part of these chapters may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

Developing an HTTP Server Agent

The F# library doesn't include any special support for using agents for creating web servers or for creating agent-based distributed systems. The programming model based on agents fits this problem domain extremely well and it is possible to use rich .NET libraries to create an agent that behaves as an HTTP web server.

The first part of this article shows how to create a type HttpAgent that behaves similarly to Agent<'T> but listens to HTTP requests and generates web pages in response. The next section starts by wrapping some asynchronous functionality in .NET, so that it can be easily used from F#.

Wrapping an Asynchronous .NET API

Standard .NET libraries include asynchronous versions of many I/O operations. These operations are usually exposed as pairs of methods such as BeginRead and EndRead. To use these operations directly, one would call BeginRead and give it a delegate as an argument. The delegate would then call EndRead to get the result.

Methods that implement this pattern aren't typically useddirectly from F#. Instead, they can be wrapped into an asynchronous workflow that works with the usual let! construct. The following snippet shows how to do that using the BeginGetContext and EndGetContext methods in an HttpListener class that will be used shortly:

[<AutoOpen>]
module HttpExtensions = 
  type System.Net.HttpListener with
    member x.AsyncGetContext() = 
      Async.FromBeginEnd(x.BeginGetContext, x.EndGetContext)

The snippet declares an extension method called AsyncGetContext and adds it to the HttpListener type. The declaration of an extension method has to be placed inside a module. The snippet uses the AutoOpen attribute to specify that the extension should be immediately available.

Wrapping a Begin/End method pair into an asynchronous workflow is very easy because the F# library provides a method that does exactly that. In the above example, the operation doesn't have any parameters, so it can be wrapped just by calling Async.FromBeginEnd with two methods as arguments. However, the method is overloaded and can wrap operations that take arguments too.

Now that you're equipped with the wrapper, the following section will show you how to implement an HTTP server agent.

Implementing the Agent Type

The aim of this section is to create an HttpAgent type that behaves almost like the usual Agent<'T>. The only difference is that users of HttpAgent do not send messages to the agent explicitly. Instead, it starts listening on a specified port. When a client connects to the server, the agent will generate a new message carrying the information about the connection. The code that uses the HttpAgent can then specify how to react to the message (usually by sending a web page back).

The agent type has a static method Start that starts the HTTP server and returns an instance of the agent. The method takes a function representing the body of the agent as an argument. The function is used to implement a local Agent that is created for the buffering of incoming HTTP requests:

open System.Net
open System.Threading

/// HttpAgent that listens for HTTP requests and handles
/// them using the function provided to the Start method
type HttpAgent private (url, f) as this =
  let tokenSource = new CancellationTokenSource()
  let agent = Agent.Start((fun _ -> f this), tokenSource.Token)
  let server = async { 
    use listener = new HttpListener()
    listener.Prefixes.Add(url)
    listener.Start()
    while true do 
      let! context = listener.AsyncGetContext()
      agent.Post(context) }
  do Async.Start(server, cancellationToken = tokenSource.Token)

  /// Asynchronously waits for the next incomming HTTP request
  /// The method should only be used from the body of the agent
  member x.Receive(?timeout) = agent.Receive(?timeout = timeout)

  /// Stops the HTTP server and releases the TCP connection
  member x.Stop() = tokenSource.Cancel()

  /// Starts new HTTP server on the specified URL. The specified
  /// function represents computation running inside the agent.
  static member Start(url, f) = 
    new HttpAgent(url, f)

The agent defines a private constructor that is called from the static Start method. The body of the constructor initializes a new CancellationTokenSource that is later used for stopping the server. Then, it creates an agent that will handle HTTP requests. The body of this agent is the asynchronous workflow created by the function provided as an argument. When starting the agent, it is given a cancellation token so that it can be later stopped.

As a next step, the constructor creates an asynchronous workflow that starts listening on the given URL address using .NET HttpListener type. The workflow repeatedly calls AsyncGetContext (created in the previous section) to get the next incoming request. It posts the request to the agent so that it can be processed by the code specified by the user of HttpAgent. The workflow is again provided with the cancellation token.

The members of the agent are quite simple. The Receive method is called from the body of the agent and receives the next message from the local agent; the Stop method uses the cancellation token source to stop both the agent and the workflow that listens for the HTTP requests. Thanks to the use keyword that was used when creating HttpListener, stopping the workflow will automatically stop the HTTP server.

The return type of the Receive method is HttpListenerContext, which represents information about HTTP request and contains an object for generating responses. Before creating a simple web server, the next section defines a few extensions that make working with this type easier.

Simplifying Types for Web Programming

The HttpListenerContext type has two properties that represent a HTTP request and a HTTP response respectively. The following listing extends the class representing the HTTP request with a member InputString that returns the data sent as part of the request as text. It also extends the HTTP response class with an overloaded member Reply that can be used for sending strings or files to the client:

open System.IO
open System.Text

[<AutoOpen>]
module HttpExtensions = 
  type System.Net.HttpListenerRequest with
    member request.InputString =
      use sr = new StreamReader(request.InputStream)
      sr.ReadToEnd()

  type System.Net.HttpListenerResponse with
    member response.Reply(s:string) = 
      let buffer = Encoding.UTF8.GetBytes(s)
      response.ContentLength64 <- int64 buffer.Length
      response.OutputStream.Write(buffer,0,buffer.Length)
      response.OutputStream.Close()
    member response.Reply(typ, buffer:byte[]) = 
      response.ContentLength64 <- int64 buffer.Length
      response.ContentType <- typ
      response.OutputStream.Write(buffer,0,buffer.Length)
      response.OutputStream.Close()

Again, the implementation of the extension members needs to be placed in a module. The listing above uses the HttpExtensions module that was used in the earlier listing. It is not allowed to have two modules of the same name, so the two modules either need to be merged or the new module needs to be renamed.

The implementation of InputString is just two lines of code. It uses a StreamReader to read the content as a string and ensures that the StreamReader is properly disposed of using the use keyword. The two Reply methods are slightly more complex. The first one converts a string to a byte array using UTF8 encoding and then writes the array to the OutputStream. The second overload writes the bytes given as an argument to the output stream and it also sets the ContentType of the response (e.g., "binary/image" for GIF files).

Using the HttpAgent and these three helpers, a very simple HTTP server can be implemented in just five lines of F# code.

Creating a Hello World Server

The next step of this tutorial uses HttpAgent to implement a realistic online chat application. This section starts with something simple. The following listing creates an HTTP server that responds to any incoming request with just a "Hello world!" string.

let url = "https://localhost:8082/"
let server = HttpAgent.Start(url, fun server -> async {
    while true do 
        let! ctx = server.Receive()
        ctx.Response.Reply("Hello world!") })

// Stop the HTTP server and release the port 8082
server.Stop()

To create an HTTP server, the caller needs to specify the URL and a function as the body of the agent. The body of the function can wait for clients using the Receive method and then send a reply using the Reply helper.

The structure of the listing is very similar to the simple agent demonstrated in the first step of the tutorial. This is exactly the goal of the HttpAgent type. The agent keeps the same familiar programming model. It is not difficult to imagine other types of agents, for example, an agent that communicates via TCP or some other communication protocol.

Summary

This step of the tutorial implemented an HttpAgent type that can be used for creating web servers. The type is designed to provide a programming interface that is very similar to the Agent<'T> type and so it fits very well with the agent-based programming model. When implementing the type, the article also introduced several helpers that enable creating a simple Hello World server with just a few lines of code.

The next step puts together the chat room agent created in the previous step and the HttpAgent type developed in this step. It creates a simple but fully functional online chat.

Additional Resources

This article is a part of a tutorial that demonstrates how to use agents to create a web-based chat server. The best way to read the tutorial is to follow the steps in order, so it is recommended to continue by reading Step 4:

This tutorial demonstrates how to use agents to develop one particular server-side application. More information about server-side programming with agents in general and for various design patterns that involve agents can be found in the following articles:

To download the code snippets shown in this article, go to https://code.msdn.microsoft.com/Chapter-2-Concurrent-645370c3

See Also

This article is based on Real World Functional Programming: With Examples in F# and C#. Book chapters related to the content of this article are:

  • Book Chapter 13: “Asynchronous and data-driven programming” explains how asynchronous workflows work and uses them to write an interactive script that downloads a large dataset from the Internet.

  • Book Chapter 14: “Writing parallel functional programs” explains how to use the Task Parallel Library to write data-parallel and task-based parallel programs. This approach complements agent-based parallelism in F#.

  • Book Chapter 16: “Developing reactive functional programs” discusses how to write reactive user interfaces using asynchronous workflows and events. This approach is related to agents but more suitable for creating user interfaces.

The following MSDN documents are related to the topic of this article:

  • Asynchronous Workflows (F#) explains how asynchronous workflows work and uses them to write an interactive script that downloads a large dataset from the Internet.

  • Async.FromBeginEnd<'T> Method (F#) includes numerous examples of using the FromBeginEnd method for wrapping asynchronous .NET operations as F# asynchronous workflows.

  • HttpListener Class explains how to use the Task Parallel Library to write data-parallel and task-based parallel programs. This approach complements agent-based parallelism in F#.

Previous article: Step 2: Encapsulating Agents into Objects

Next article: Step 4: Creating a Web Chat Application