Introduction
>This article is my attempt at developing an extremely simple whiteboard application to be used simultaneously
by two users. Although I started my attempt with support for more than two users, I dropped it to two to reduce
the complexity of the project.
Here are some of the features this whiteboard supports:
- It allows two users to connect to each other, with either one running as a
listener with a valid IP on a valid port, and the other one running as a client
that connects to the listener�s IP/Port.
- It allows 4 drawing primitives, namely scribbling via a pencil tool, drawing rectangles with
a rubberbanding like effect in Paintbrush, drawing ellipses, clearing the screen.
System Requirements
To compile the solution you need to have Microsoft Visual Studio .NET installed. To run any of the client
executable you need to have the .NET framework installed.
Here is a snapshot that allows the client and server both running on the same machine, but make sure the correct
IP is specified in the "ConnectTo" box.
Starting the application:
To start the application, do the following:
Fire up an instance of the application on one machine.
Select the mode this instance would run in, i.e. client mode or server mode. To make it run as a server
listener, select the "Start as Listener" option as shown and accept the default 8888 as the listening port,
unless you have another application on your system listening on it, then click 'Start listening'
Note: Unfortunately this application is unable to accept requests from another Whiteboard client
if it's behind a firewall, to make it work you need to poke a hole in the firewall for port 8888.(eeeeks !)
Fire up another instance of the Whiteboard application on another machine. This time select the Connect to
option as shown:
- The ConnectedPeers title on the top right would add a node with the peer's IP.
A user drawing on one end is reflected on the other, i.e. shown below at the listener end.
Sorry, Chris, that's the best I could get with my limited drawing skills as well as the limited no of
drawing tools provided in this app.
Explaining the implementation
The following diagram illustrates the internal architecture of the app./p>
The application consists of a WinForms based client which contains a couple of UI controls for user input,
like textboxes for IP address/port and treeview control, toolbar, buttons, radio buttons, etc.
It contains a custom control which takes care of all the drawing related code, see DrawAreaCtrl.cs.
To persist the scribbled data on the whiteboard, the custom control has code that makes EXACT changes as
that made on the whiteboard drawing area on a Bitmap object. This ensures that the changes made aren�t lost.
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
g.DrawImage(m_bmpSaved, 0,0,m_bmpSaved.Width, m_bmpSaved.Height);
}
The UI has two modes of operation:
- Server mode (Listener)
- Client mode (Connects to a listener)
If you select the first mode as shown above the application starts listening on the machine�s IP on the
port specified as shown in the following snippet.
m_enNetMgrMode = NETWORK_MANAGER_MODE.enServerMode;
string StrHostName = Dns.GetHostName();
m_SockListener = new Socket(0, SocketType.Stream, ProtocolType.Tcp);
IPHostEntry IpEntry = Dns.GetHostByName (StrHostName);
IPAddress [] IpAddress = IpEntry.AddressList;
IPEndPoint LocEndpoint = new IPEndPoint(IpAddress[0], m_iPort);
m_SockListener.Bind(LocEndpoint);
m_SockListener.Blocking = true;
m_SockListener.Listen(-1);
m_SockListener.BeginAccept(m_AsyncCallbackAccept, m_SockListener);
If you select the second option AFTER SPECIFYING the correct IP/Port of another listening whiteboard, the
app connects to the server as shown.
IPAddress hostadd = Dns.Resolve(StrHostIp).AddressList[0];
int iHostPort = Convert.ToInt32(StrHostPort);
IPEndPoint EPhost = new IPEndPoint(hostadd,iHostPort);
m_StrHostIp = StrHostIp;
m_StrHostPort = StrHostPort;
m_SockServer = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp
);
try
{
m_SockServer.Connect(EPhost);
m_Creator.EnableDisableConnectModeControls(true);
string msg = "Connected to remote Whiteboard on: " + StrHostIp ;
msg += " listening on port: " + StrHostPort;
m_Creator.SetMusicalStatus(msg, "ding.wav");
m_HndlrForListeningWB = new ClientHandler(m_SockServer, m_Creator);
m_HndlrForListeningWB.ProcessClientRequest();
}
catch(NullReferenceException exception)
{
m_Creator.SetStatus ("Connect failed. " + exception.Message);
m_SockServer = null;
}
Messaging architecture
Once the socket is established between the two users, each peer communicates with the other peer through
something called WhiteBoard Messages.(see WhiteBoardMessages.cs). This file contains an abstract
class called WBMessage
that every other message like WBMsgDrawBegin
, WBMsgDrawEnd
,
WBMsgDrawLine
, WBMsgDrawRectangle
and WBMsgDrawEllipse
inherits from.
For example:
[Serializable]
public class WBMsgDrawBegin : WBMessage
{
private const WHITEBOARD_MESSAGE_TYPE m_enMsgType =
WHITEBOARD_MESSAGE_TYPE.enWBBegin;
public override WHITEBOARD_MESSAGE_TYPE MessageType
{
get
{
return m_enMsgType;
}
}
public Point m_PtBegin;
public bool m_bMouseDown;
}
Notice the Serializable
attribute before each of these classes.
The static methods of the class WBMessageHelper (Serialize
, Deserialize
,
DeserializeL
) are the heart of the message encoding/decoding infrastructure
of the application.
.NET provides object serialization support through the use of Formatter
classes like the BinaryFormatter
and SoapFormatter
.
This application keeps transporting mouse messages (mousedown, mousemove and
mouseup coordinates) from one user to another remote user. Using the
SoapFormatter
would have meant transporting a LOT more data (since SOAP is
an XML based serialization mechanism) than in a Binary Format.
Without BinaryFormatters
I could have sent raw structs with longs, ints and
floats within them and manipulated byte pointers in unsafe code. But I
decided to use what .NET already provides without delving into the unsafe
world.
BinaryFormatter
s serializes and deserializes an object, or an entire graph
of connected objects, in binary format. For example, the following function
serializes a long
into a memory stream object whose raw buffer contents
could be passed on the wire through the opened socket and then unpacked with
a similar Deserialize routine:
public static MemoryStream Serialize(long lObject)
{
MemoryStream ms = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;
formatter.TypeFormat = FormatterTypeStyle.TypesWhenNeeded;
formatter.Serialize(ms, lObject);
return ms;
}
The weird thing about BinaryFormatters
is that even a single long of 8 bytes when serialized into
a memory stream takes up 56 bytes of buffer space, which is seven times bigger than just sending a raw long
of 8 bytes.
Lets take a use case on how the Scribble line tool works (assuming 2 peers connected to each other with
one as listener and other connecting to it) When someone clicks on the scribble button on the toolbar.
The DrawAreaCtrl
custom control shifts to the WHITEBOARD_DRAW_MODE
of enModeLine
Once this draw mode is enabled, all mouse events are interpreted for drawing
related to scribbling lines.
MouseDown
On a mousedown an instance of the serializable class
WBMsgDrawBegin
is
created. This class is then serialized to a byte buffer using the
BinaryFormatter
class and sent across the socket. But every serialized
object's buffer is preceded with a serialized long that specifies the length
of the buffer that is going to follow it. This way the client on the other
end knows how much data to parse and deserialize.
public void SendWBMessage(WBMessage msg)
{
if(m_NS != null)
{
MemoryStream ms = WBMessageHelper.Serialize(msg);
MemoryStream msLength = WBMessageHelper.Serialize(ms.Length);
m_NS.BeginWrite(msLength.GetBuffer(), 0, (int)msLength.Length,
m_AsyncCallbackWrite, null);
m_NS.BeginWrite(ms.GetBuffer(), 0, (int)ms.Length, m_AsyncCallbackWrite,
null);
ms.Close();
msLength.Close();
}
}
These bytes, when sent across, look like this in memory:
public void ParseData(byte [] ByteBuff, int iCbRead)
{
int offset = 0;
m_iByteCacheLen = 0;
while(iCbRead >= m_iHdrSize)
{
if(m_bOnHeader)
{
m_iDataPackLen = (int)WBMessageHelper.DeserializeL(ByteBuff, offset,
m_iHdrSize);
offset += m_iHdrSize;
m_bOnHeader = false;
iCbRead -= m_iHdrSize;
}
else
{
if(m_iDataPackLen == 0) return;
if(m_iDataPackLen > iCbRead)
{
Array.Clear(m_ByteCache, 0, 2048);
Array.Copy(ByteBuff, offset, m_ByteCache, 0, iCbRead);
m_iByteCacheLen = iCbRead;
iCbRead -= iCbRead;
}
else
{
WBMessage msg = WBMessageHelper.Deserialize(ByteBuff, offset,
m_iDataPackLen);
RouteRequest(msg);
offset += m_iDataPackLen;
m_iByteCacheLen = 0;
iCbRead -= m_iDataPackLen;
m_iDataPackLen = 0;
m_bOnHeader = true;
}
}
}
if(iCbRead > 0)
{
Array.Clear(m_ByteCache, 0, 2048);
Array.Copy(ByteBuff, offset, m_ByteCache, 0, iCbRead);
m_iByteCacheLen = iCbRead;
}
}
>Some limitations, bugs and of course a disclaimer
- Doesn�t support more than 2 clients (for now, but the code could be
extended);
- Concurrency checks for concurrent drawings not present, so behaviour
unpredictable. Right now its implemented by disabling mouse input from the
user over the whiteboard if another client is in the process of
updating/drawing on it;
- Pen size, brush type, and background color are all fixed for now, which
could be easily incorporated through the UI and some additional messages;
- Doesnt work with a firewall restricting access between the two clients.
(hint: see personal firewall settings to poke a hole for port 8888 to make it to
work);
- Better mouse tracking required on the client;
- I have assumed that a
BinaryFormatter
serializes a long data type to a
memory stream object in the form of 56 byes of memory (try it yourself to see);
- This is BY NO MEANS an article that explains the best software
development practises or explains excellent C# coding skills. This is just a
proof of concept application that I wanted to write out as soon as I can and
see how much time it takes for me to develop.
References
- Programming Microsoft Windows with C# - Charles Petzold.
- MSDN