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.