Click here to Skip to main content
Click here to Skip to main content

Crafting a Simple Chat Room Based on HTTP Multipart Streaming

, 28 May 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
HTTP multipart streaming can do many things.

Introduction

This is a short tip regarding a client-server chat room system based on full HTTP. The idea is simple: a client application sends text to a server using an HTTP POST request, and the server pushes text to clients with an HTTP multipart streaming, which the clients receive and "split".

The Basics

So let's take a quick look at HTTP multipart streaming. Here's a response example from an HTTP server:

HTTP/1.1 200 OK 
Content-Type: multipart/x-mixed-replace; boundary=--4646a1160058e09bed25;
Server: Microsoft-HTTPAPI/2.0
Date: Fri, 17 May 2013 08:20:42 GMT
 
--4646a1160058e09bed25 
Content-Type: text/plain
Content-Length: 28
 
Fortune doesn't favor fools.
--4646a1160058e09bed25 
Content-Type: text/plain
Content-Length: 65
 
Watch your mouth kid, or you'll find yourself respawning at home!
--4646a1160058e09bed25 

As you can see, the most important thing is the boundary. It declares where the data starts and where it ends for each part. The server must tell the client at the very beginning, within the Content-Type header at first. Then clients are able to "split" the stream with the specified boundary string.

Using the Code

There are several classes I've done so you can easily use them as below. Note that all classes named after ClassNameElement are inherited from ServiceElement, controlled by ServiceElementController. For running and stopping the element service, use the Start and Stop methods.

At the client side, HttpMultipartTriggerElement will send a request to the specified URL at first. After getting a response from the server, it will start an asynchronous splitting operation. And once the new part arrives, the operation will raise the Notify event which includes data and other properties.

HttpMultipartTriggerElement trigger = new HttpMultipartTriggerElement(ServerResponseUri);
 
trigger.Notify += trigger_Notify;
trigger.Error += trigger_Error;
trigger.StatusChanged += trigger_StatusChanged;
 
ServiceElementController<HttpMultipartTriggerElement> controller = 
new ServiceElementController<HttpMultipartTriggerElement>(trigger);
 
controller.Start();

In this sample, the client application sends messages with the WebClient class for posting a string.

Console.WriteLine("Enter message or 'exit' to leave...");
 
while (true)
{
    string message = Console.ReadLine();
 
    if (message.ToLowerInvariant() == "exit")
        break;
 
    try
    {
        using (WebClient client = new WebClient())
        {
            client.Headers[HttpRequestHeader.ContentType] = MediaTypeNames.Text.Plain;
            client.UploadString(ServerRequestUri, message);
        }
    }
    catch (Exception error)
    {
        Console.WriteLine(error);
    }
}   

Now we have the entire client application:

class Program
{
    internal static readonly Uri ServerRequestUri;
    internal static readonly Uri ServerResponseUri;

    static Program()
    {
        UriBuilder uri = new UriBuilder(Uri.UriSchemeHttp, "localhost", 80);

        uri.Path = "svc-bin/chatroom/request/"; 
        ServerRequestUri = uri.Uri;
        uri.Path = "svc-bin/chatroom/response/";
        ServerResponseUri = uri.Uri;
    }

    static void Main(string[] args)
    {
        HttpMultipartTriggerElement trigger = 
            new HttpMultipartTriggerElement(ServerResponseUri);

        trigger.Notify += trigger_Notify;
        trigger.Error += trigger_Error;
        trigger.StatusChanged += trigger_StatusChanged;

        ServiceElementController<HttpMultipartTriggerElement> controller = 
            new ServiceElementController<HttpMultipartTriggerElement>(trigger);

        controller.Start();

        Console.WriteLine("Enter message or 'exit' to leave...");

        while (true)
        {
            string message = Console.ReadLine();

            if (message.ToLowerInvariant() == "exit")
                break;

            try
            {
                using (WebClient client = new WebClient())
                {
                    client.Headers[HttpRequestHeader.ContentType] = 
                                        MediaTypeNames.Text.Plain;
                    client.UploadString(ServerRequestUri, message);
                }
            }
            catch (Exception error)
            {
                Console.WriteLine(error);
            }
        }

        controller.Stop();
    }

    static void trigger_StatusChanged(object sender, StatusChangedEventArgs e)
    {
        Console.WriteLine("Chat Room Client Status: {0} -> {1}", e.OldStatus, e.NewStatus);
    }

    static void trigger_Error(object sender, ExceptionsEventArgs e)
    {
        Console.WriteLine("Chat Room Client Error(s): ");
        Console.WriteLine(e);
    }

    static void trigger_Notify(object sender, FireNotificationEventArgs e)
    {
        using (StreamReader reader = new StreamReader(e.Notification.CreateReadOnlyStream()))
            Console.WriteLine("Incoming message: {0}", reader.ReadToEnd());
    }
}   

At the server side, we use an HttpElement which represents an HTTP server and add two modules: an HttpMultipartResponseModule derived instance for multipart streaming response, and an instance directly derived from HttpModule with the IHttpRequestModule interface for handling incoming HTTP POST requests from clients. Each module corresponds to a combination of URL path and HTTP method. 

ServiceElementController<HttpElement> controller = 
    new ServiceElementController<HttpElement>(httpElement);
 
httpElement.Modules.Add(new CustomMultipartResponseModule());
httpElement.Modules.Add(new CustomRequestModule());  

Here's the detail of the multipart response module.

class CustomMultipartResponseModule : HttpMultipartResponseModule
{
    public CustomMultipartResponseModule()
        : base("svc-bin/chatroom/response/")
    { 
    }
 
    protected override void ContextRegistered(HttpResponseContext response)
    {
        Console.WriteLine("{0} Online.", response.Description.RemoteEndPoint); 
    }
 
    protected override void ContextUnregistered(HttpResponseContext response)
    {
        Console.WriteLine("{0} Offline.", response.Description.RemoteEndPoint); 
    }
}    

Next is the HTTP POST request handler. In the IHttpRequestModule.Read method, we check the MIME type of each incoming request from the clients at first. If it is text/plain, copy the data and return a BufferedNotification instance, otherwise return null to ignore it. Notice the first parameter of the base constructor as well.

class CustomRequestModule : HttpModule, IHttpRequestModule
{ 
    #region Constructor
 
    public CustomRequestModule()
        : base(WebRequestMethods.Http.Post, "svc-bin/chatroom/request/")
    {
    }
 
    #endregion
 
    #region HttpModule Implementation
 
    public override bool Validate(string username, string password)
    {
        return true;
    }
 
    public override void Dispose()
    { 
    }
 
    #endregion
 
    #region IHttpRequestModule Method
 
    public IEnumerable<Notification> Read(HttpRequestContext request)
    {
        if (request.ContentType != MediaTypeNames.Text.Plain)
            return null;
 
        using (Stream stream = request.GetRequestStream())
        using (MemoryStream data = new MemoryStream((int)request.ContentLength))
        {
            stream.CopyTo(data, 65536);
 
            return new[] { new BufferedNotification(
                                 NotificationLevel.Info, data.ToArray()) };
        }
    }
 
    #endregion
}

That's all the stuff we need at the server side, therefore we can finally complete the server application:

class Program
{
    static readonly HttpElement httpElement = new HttpElement();

    static void Main(string[] args)
    {
        ServiceElementController<HttpElement> controller = 
            new ServiceElementController<HttpElement>(httpElement);

        httpElement.Modules.Add(new CustomMultipartResponseModule());
        httpElement.Modules.Add(new CustomRequestModule());
        httpElement.Notify += HttpElement_Notify;
        httpElement.Error += HttpElement_Error;
        httpElement.StatusChanged += HttpElement_StatusChanged;

        controller.Start();

        do
            Console.WriteLine("Enter 'exit' to leave...");
        while (Console.ReadLine().ToLowerInvariant() != "exit");

        controller.Stop();
    }

    static void HttpElement_Notify(object sender, FireNotificationEventArgs e)
    {   
        httpElement.Publish(e.Notification);
    }

    static void HttpElement_StatusChanged(object sender, StatusChangedEventArgs e)
    {
        Console.WriteLine("Chat Room Server Status: 
        {0} -> {1}", e.OldStatus, e.NewStatus);
    }

    static void HttpElement_Error(object sender, ExceptionsEventArgs e)
    {
        Console.WriteLine("Chat Room Server Error(s): ");
        Console.WriteLine(e);
    }
}

You can view and download the whole source code here.

Advanced Topic

Because HTTP multipart streaming can load everything, you can create a chat room system of text, image, or even video messages. Just remember to send the message with the correct MIME type so other clients can recognize and display its content.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Robert Vandenberg Huang
Software Developer
Taiwan Taiwan
Software developer, video game enthusiast, drummer and huge Jazz music fan. Been working on software development since early age. Mainly using C, C++, C# and JavaScript (in Node.js). Mostly interesting in HTTP(S), cryptographic hash algorithms, OAUTH 2.0 and game AI. Believe that crafting a system is just like playing drums - both requires the view of art and logic.
Follow on   Twitter   LinkedIn

Comments and Discussions

 
QuestionWhy? PinmemberCorey Fournier28-May-13 11:51 
AnswerRe: Why? PinprofessionalRobert Vandenberg Huang28-May-13 14:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141216.1 | Last Updated 28 May 2013
Article Copyright 2013 by Robert Vandenberg Huang
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid