Partager via


Step 4: Creating a Web Chat Application

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 combines the components from the previous steps to builds a simple but fully functional agent-based online chat.

This topic contains the following sections.

  • Implementing a Chat Server
  • Adding Web-Based User Interface
  • 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.

Implementing a Chat Server

A basic aspect of agents is that the body of an agent runs as a single logical thread. It may switch between actual .NET threads and it doesn't use any threads when performing nonblocking waiting, but the body never runs concurrently.

An efficient web server needs to handle multiple concurrent requests. It cannot run all of the processing in the body of the HttpAgent as in the previous "Hello world" example. This article uses a different approach. When the agent receives an HTTP request, it starts a new asynchronous workflow to handle the request and then continues waiting for the next request. This introduces concurrency, but there is no need to worry about race conditions. The ChatRoom instance, which is going to be accessed by multiple threads, is implemented using Agent and is thread safe.

The following section implements the asynchronous workflow that handles an HTTP request. The spawning of the workflow for every incoming request is shown later.

Handling Requests

When handling a request, the server checks for two special commands. When the client requests the /chat path, server replies with the content of a chat room. The /post path is used for sending new messages to the chat room. When the client requests any other path, the server treats it as a request for a file from some special directory because the chat application also consists of a simple HTML file and CSS style sheet.

The following listing shows global declarations that are used when handling requests:

let room = new ChatRoom()
let root = @"C:\wwwroot\"
let contentTypes = dict [ ".css", "text/css"; ".html", "text/html" ]

The application is quite simple and has only a single chat room so it is created as a global declaration. The snippet also stores a path that contains static files and a simple dictionary for getting HTTP content type of a file using the file extension. Using these three values, the next listing implements a function that asynchronously handles an incoming HTTP request:

let handleRequest (context:HttpListenerContext) = async { 
    match context.Request.Url.LocalPath with 
    | "/post" -> 
        // Send message to the chat room
        room.SendMessage(context.Request.InputString)
        context.Response.Reply("OK")
    | "/chat" -> 
        // Get messages from the chat room (asynchronously!)
        let! text = room.AsyncGetContent()
        context.Response.Reply(text)
    | s ->
        // Handle an ordinary file request
        let file = root + (if s = "/" then "chat.html" else s)
        if File.Exists(file) then 
          let ext = Path.GetExtension(file).ToLower()
          let typ = contentTypes.[ext]
          context.Response.Reply(typ, File.ReadAllBytes(file))
        else 
          context.Response.Reply(sprintf "File not found: %s" file) }

The argument of the function is an HttpListenerContext, which contains information about the request and provides functionality for sending a response. This is made a bit simpler using extensions that were defined in the previous step of the tutorial. The entire function is wrapped in an async block, so that it can execute operations in a non-blocking way.

When the user requests the /post path, the function gets the text of the message from the request using InputString extension and send it as a new comment to the chat room. To handle a /chat request, the function asynchronously gets the content of a chat room and sends it back as an HTTP response.

Finally, all other requests are treated as attempts to read a file. The file chat.html is used as a default file. To serve a file, the function first checks if the file exists, then gets an associated MIME content type (e.g., text/css for the .css extension) and then sends the contents of the file as a response.

The next section completes the F# part of the online chat. All that remains to do is to create an HTTP server that handles requests using the handleRequest function.

Starting the Server

As already mentioned, an efficient server needs to handle multiple requests concurrently. To implement this behavior using HttpAgent, the agent waits for incoming requests in a loop. When a request is received, the agent starts the asynchronous workflow constructed by handleRequest without waiting until it completes:

let url = "https://localhost:8082/"
let server = HttpAgent.Start(url, fun mbox -> async {
    while true do 
      let! ctx = mbox.Receive()
      ctx |> handleRequest |> Async.Start })

The agent has the same structure as the basic example from the previous step. It uses a while loop to wait for requests until the server is stopped. When it receives a request, it passes the information to handleRequest, which then returns an asynchronous workflow. The workflow is started in the thread pool using the Async.Start method. This means that the body of the agent finishes almost immediately and is ready to handle the next request. Another way to call the handleRequest function in the body of the agent would be to write:

let! ctx = mbox.Receive()
do! handleRequest ctx

This compiles fine, but it has a very different behavior. Instead of starting the processing function in background and then continuing to do other work (wait for the next request), it starts the processing and waits until the processing of the request completes. Only after that can the next iteration of the loop be done. This means that this version would only process one request at a time! In this example, the processing finishes relatively quickly, but the change could seriously affect the performance of a real system.

To finish the example, the next section shows a simple HTML document with a little bit of JavaScript for sending messages to the chat and refreshing the room content.

Adding Web-Based User Interface

The focus of this document is on the server-side F# code, so it shows only a minimalistic implementation of the chat room. The full source code (available at the end of the document) also configures the visual aspects of the web application. The full version of the application can be seen in Figure 1.

Figure 1. Web chat created using F# agents

Referenced Image

The web page that is sent to the web browser is a simple static page that contains some HTML markup and JavaScript. When the page loads, the JavaScript code creates a timer to refresh the content.

The HTML markup contains a <div> element-named output that is used for displaying the messages in the room, an <input> element where the user can enter text, and a <button> that sends the text to the chat room:

<h1>Agent-based F# chat</h1>
<div id="output"></div>
<div id="input">
    <input type="text" id="inputBox" />
    <button onclick="send();">Send</button>
</div>

To keep things simple, the listing only shows the body of the page. The markup is just plain HTML. The only thing that's worth noting is that the button uses a send function, a handler for the onclick event. It also sets the value of the id attribute for all elements that need to be accessed programmatically from the JavaScript.

The JavaScript functionality consists of just two functions. The send function creates an HTTP POST request to the /post page and sends the text from the inputBox element as data. The second function is called repeatedly via timer. It sends a request to the /chat page. When it gets a response, it displays the received room content in the output element. The following snippet implements the client-side script using the jQuery library:

function send() {
    $.ajax({ type: "POST", url: "/post", dataType: "text", 
             data: $("#inputBox").val(), success: refresh });
    $("#inputBox").val("");
}

function refresh() {
    $.ajax( { 
        type: "POST", url: "/chat", dataType: “text”,
        success: function (data) {
            $("#output").html(data);
        }
    });
};

setInterval(function () { refresh(); return true; }, 5000);

Both of the functions call the server using the $.ajax function from jQuery. When sending a new message in send, the request needs to contain the message. To specify the data, the snippet sets the dataType parameter (to send the value as plain text instead of XML or JSON) and specifies the text as a value of the data parameter. The call also specifies that jQuery should call our refresh function on successful completion so that the chat room content is immediately updated. After initiating the request, the function also clears the content of the inputBox element.

The refresh function creates a request to the /chat path. It doesn't send any data to the server but it needs to process the received response. This is done in the anonymous function specified as the success parameter. The function gets called with the HTML markup generated by the F# server and sets the markup as the HTML content of the output element. Finally, the last line creates a timer that regularly calls the refresh function every 5 seconds.

Summary

The last step of this tutorial completes the featured web-based chat application. It used two components discussed in the previous steps. The chat room is represented using a type developed using an agent (in Step 2). As a result, the server can safely access the object concurrently and retrieve the content of the chat room without blocking a thread.

To expose the chat as a web application, the sample used an HttpAgent type (from Step 3). The type provides a way for creating an HTTP server using the same pattern that is used when creating standard F# agents. Finally, the tutorial showed how to add a basic web-based user interface that calls the server using jQuery.

The complete source code of the tutorial can be downloaded from the following page:

Additional Resources

This article is a part of a tutorial that demonstrates how to use agents to create a web-based chat server. The earlier steps of the tutorial were combined in this last step. They can be found below:

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:

Previous article: Step 3: Writing an Agent-Based Web Server

Next article: Tutorial: Creating Windows Services in F#