Click here to Skip to main content
15,884,629 members
Articles / Programming Languages / C#
Article

Fully functional Asynchronous Mailslot Control in C#

Rate me:
Please Sign up or sign in to vote.
4.76/5 (32 votes)
17 Jun 20078 min read 62.4K   1.1K   34   11
An asynchronous C# control implementation of Win32 Mailslot communication.

Introduction

For a recent project I needed to create a local LAN chat application that was secure and connectionless. From prior projects I knew a simple way to accomplish this was by using Windows Mailslots. Unfortunately, .NET does not have built-in support for using them and one is forced to P/Invoke API calls directly. After searching many newsgroups and sites such as CP, I found some suggestions and random P/Invoke signatures but nothing usable so I had to create it from scratch. And now I offer my findings to you.

One important thing to note is that this project will throw an exception if run from the debugger. This is because the OnReceivedData event is fired from a different thread than the UI is running in. There are easy ways around this using this.InvokeRequired which you will find if you search, however this article does not go into that issue. For an example look here, specifically the post by Brian Gideon.

Background

If you're new to programming, you probably aren't familiar with what a Mailslot is. In simplest terms, a "slot" is a virtual file. In order to pass data between two points, two things must happen at each point. Both ends must open the slot in read mode locally, and in write mode remotely. Remember we are treating the slot as a file. Client A writes a message to the slot \\ClientB\Slotname and Client B reads it from \\.\Slotname. Alternatively, if the remote slot is opened as \\*\Slotname, then the message will be sent to Slotname on all computers on the local domain or workgroup. If a computer receives a message to a slot that it does not have opened, Windows will ignore it by design.

Mailslot communication is done over UDP port 137. The useful thing about this method is that it requires no actual connection to be established, and no central server is required.

Using the control

Because I have written this as a control, it requires very little code to be used. One thing to note is that I have not figured out how to make the control invisible on the Form so be sure to set its visible property to false or you will have a picture of an envelope.

Quick start guide

  1. Add vMailslot.cs to your project
  2. Drop a vMailslot control on your Form
  3. Set the slotname property
  4. Assign an event handler for OnReceivedData
  5. Call the .Connect method with the desired scope

Behind the scenes

For those who want to know how it works, here we go.

The most frequent newsgroup / forum posting I see is how to correctly declare the API calls that are needed to implement Mailslots. It took a lot of trial and error to get the right combination of types between all calls. So let's get those out of the way as some of you will be here purely for this information. We have five API functions to declare: CreateMailslot, GetMailslotInfo, CreateFile, ReadFile and WriteFile.

CreateMailSlot

C#
[DllImport("kernel32.dll")]
static extern IntPtr CreateMailslot(string lpName,
                                    uint nMaxMessageSize,
                                    uint lReadTimeout,
                                    IntPtr lpSecurityAttributes);

GetMailslotInfo

C#
[DllImport("kernel32.dll")]
static extern bool GetMailslotInfo(IntPtr hMailslot,
                                   int lpMaxMessageSize,
                                   ref int lpNextSize,
                                   IntPtr lpMessageCount,
                                   IntPtr lpReadTimeout);

CreateFile

C#
[DllImport("Kernel32.dll", SetLastError = true,
                       CharSet = CharSet.Auto)]
static extern IntPtr CreateFile(
    string fileName,
    [MarshalAs(UnmanagedType.U4)] FileAccess fileAccess,
    [MarshalAs(UnmanagedType.U4)] FileShare fileShare,
    int securityAttributes,
    [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
    int flags,
    IntPtr template);

ReadFile

C#
[DllImport("kernel32.dll", SetLastError = true)]
private unsafe static extern bool ReadFile(
    IntPtr hFile,
    void* lpBuffer,
    int nNumberOfBytesToRead,
    int* lpNumberOfBytesRead,
    int overlapped); 

WriteFile

C#
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool WriteFile(
    IntPtr hFile,
    byte[] lpBuffer,
    uint nNumberOfBytesToWrite,
    out uint lpNumberOfBytesWritten,
    [In] ref System.Threading.NativeOverlapped lpOverlapped);

As the title implies, this control monitors the slot asynchronously, or in another thread. With that said, you'll see in the constructor we create this thread. We set the thread priority to BelowNormal to reduce the CPU impact.

C#
public vMailslot()
{
  InitializeComponent();
  readThread = new Thread(new ThreadStart(ThreadReadSlot));
  readThread.Priority = ThreadPriority.BelowNormal;
}

In the Connect method, we open the previously mentioned local and remote slots, and start the thread that will monitor for incoming messages.

C#
public bool Connect(string Scope)
{
  if (_SlotName.Length > 0)
  {
    _ReadHandle = CreateMailslot("\\\\.\\mailslot\\" + _SlotName, 0, 0,
                                  IntPtr.Zero);
    _WriteHandle = CreateFile("\\\\" + Scope + "\\mailslot\\" + _SlotName,
          FileAccess.Write, FileShare.Read, 0, FileMode.Open, 0, IntPtr.Zero);
  }
  if ((_ReadHandle.ToInt32() * _WriteHandle.ToInt32()) > 0)
  {
    readThread.Start();
    return true;
  }

  return false;
}

Once .Connect is called and the slots are created, you can begin using the SendText function to send messages. Let's have a look at that.

C#
public bool SendText(string Username, string Command, string Data)
{
  int iCounter;
  string[] SplitData;
  string PreEncode;
  Byte[] EncVar;

  System.Threading.NativeOverlapped stnOverlap =
                        new System.Threading.NativeOverlapped();

  Data = Data.Trim();
  if (Data.Length > 0)
  {
    SplitData = sbSplit(Username, Command, Data,
       400 - Username.Length - Command.Length - seqNum.ToString().Length - 7);

    for (iCounter = 0; iCounter < SplitData.Length; iCounter++)
      if (SplitData[iCounter] != null)
      {
        EncVar = System.Text.Encoding.Default.GetBytes(SplitData[iCounter]);
        PreEncode = System.Text.Encoding.Default.GetString(EncVar);
        RC4(ref EncVar, EncKey);

        WriteFile(_WriteHandle, EncVar, (uint)EncVar.Length,
                          out bytesWritten, ref stnOverlap);
        if (SentText != null)
          SentText(PreEncode);
      }
  }
  return false;
}

If you look at this function you may have already figured out that the control implements RC4 encryption. The encryption key is an array of bytes which can be altered in the control source. The function is based on the same one that you will find in almost all RC4 examples and will not be examined in this article. With that said, you will see a call to the function sbSplit() above. Windows by design limits broadcast UDP data to 424 bytes. This is only an issue when using * as the scope in Connect() but for simplicity is always assumed. The sbSplit function handles breaking the message up into fragments before sending. Let's have a look at it.

C#
public string[] sbSplit(string Username, string Command,
                        string Text, int MaxLen)
{
  int Index, txtLeft, txtLen, offS, Iteration;
  string tmpStr;
  string[] ReturnText = new string[1];

  txtLen = Text.Length;
  txtLeft = txtLen;
  offS = 0;
  Index = 0;
  Iteration = 0;
  while (txtLeft > 0)
  {
   Iteration++;
   if (ReturnText.Length <= Iteration)
     ReturnText = ResizeArray(ReturnText, Iteration + 1);
   if (txtLeft <= MaxLen)
   {
     tmpStr = Text.Substring(Text.Length - txtLeft, txtLeft);
     seqNum++;
     ReturnText[Iteration] = seqNum.ToString() + "§" + Username + "§" +
                                  "END" + "§" + Command + "§" + tmpStr;

     return ReturnText;
   }

   tmpStr = Text.Substring(offS, MaxLen);
   Index = tmpStr.Length - 1;
   while ((tmpStr[Index] != ' ') & (Index > 0))
   {
     Index--;
   }

   if (Index <= 0)
     Index = MaxLen;

   tmpStr = tmpStr.Substring(0, Index);
   txtLeft = txtLeft - tmpStr.Length;
   seqNum++;
   ReturnText[Iteration] = seqNum.ToString() + "§" + Username + "§" +
                               "FRAG" + "§" + Command + "§" + tmpStr;

   offS += Index;
  }

  return null;
}

When sbSplit is called, it will use a word wrap algorithm of my own to break the sentence if needed at the nearest word boundary to the limit. This is partially left over from before there was a method to defragment the messages and could now be phased out as the messages are pieced together and displayed as one now. As you can tell, the control uses § as a delimiter in the data. There are 5 components:

  1. Sequence Number
    • Incremented on each call. Used to detect duplicate messages
  2. Username
    • The user name to be used for sending messages. Can be "" if not needed
  3. FRAG / END
    • Used to determine if we have more fragments to send
  4. Command
    • Can be any user defined value. Pass "" if not needed
  5. Text
    • The actual text of the message being sent

I have no good segway here, so let's just go right into the thread that monitors the slot for received data.

C#
private void ThreadReadSlot()
{
 string readData = "";
 string[] parsedData = new string[5];

 while (true)
 {
  try
  {
    readData = this.ReadSlot();
    if (readData.Length > 0)
      if (OnReceivedData != null)
      {
        parsedData = readData.Split('§');
        if (parsedData[2] == "FRAG")
        {
          AddFrag(parsedData[1], parsedData[3], parsedData[4]);
        }
        else
          OnReceivedData(parsedData[1], parsedData[3], Defrag(parsedData[1],
                         parsedData[4]));
      }
  }
  catch (Exception ex) { MessageBox.Show(ex.Message + "\r\n" + ex.Source +
                         "\r\n" + ex.StackTrace); }
  Thread.Sleep(200);
 }
}

After creating this function, the CPU was constantly at 100%. Adding the Thread.Sleep(200) line was the key to eliminating that. This function should be pretty straight-forward by now. The local Mailslot is polled every 200ms for data and if found it is read with ReadSlot() which we will get to in a moment. Once the data has been read it is split into a 5 sting array corresponding to the components you just read about. As you can also see, if the FRAG flag is present, the data is passed to AddFrag() which we will also cover in a moment. When the entire message has been received, the OnReceivedData event is fired and it continues to listen for new messages.

Now I'm starting to get a bit ahead of myself so let's check out the ReadSlot function. One thing to note is that this function makes use of a byte* pointer. For that reason it must be declared as unsafe and you must enable unsafe code in the build options for your project.

C#
unsafe public string ReadSlot()
{
  int iMsgSize, iRead;
  byte[] Data = new byte[424];
  bool IsData;
  bool IsDupe = false;
  iMsgSize = 0;
  iRead = 0;
  byte[] RetValue;
  string sRetVal;

  GetMailslotInfo(_ReadHandle, 0, ref iMsgSize, IntPtr.Zero, IntPtr.Zero);
  //Read the current status of the mailslot,
  //notably the size of any waiting messages

  IsData = (iMsgSize > 0);

  if (IsData)
  {
    fixed (byte* p = Data)
    {
      ReadFile(_ReadHandle, p, iMsgSize, &iRead, 0);
      RetValue = new byte[iMsgSize];
      System.Array.Copy(Data, RetValue, iMsgSize);
      RC4(ref RetValue, EncKey);
    }

    sRetVal = System.Text.Encoding.Default.GetString(RetValue);

    foreach (string prevLine in _MessageQue)
    {
      if (sRetVal == prevLine)
        IsDupe = true;
    }

    if (IsDupe == false)
    {
      _MessageQue[0] = _MessageQue[1];
      _MessageQue[1] = _MessageQue[2];
      _MessageQue[2] = _MessageQue[3];
      _MessageQue[3] = sRetVal;
      return sRetVal;
    }
  }
  return "";
}

I believe I briefly mentioned duplicate messages above. Allow me to explain. Mailslots, by design, will send the data using every available transport. This means if you have TCP/IP, IPX, net NETBUI/NETBIOS protocols installed/enabled, you will send the data out 3 times, once over each protocol.

The same is true when receiving data. The receiver would get 3 copies of the same message. For this reason, we have the Sequence Number as previously mentioned. That, combined with the _MessageQue array, ensures we react to each message only once. I decided to track the last 3 messages received in this code, however you can adjust it as you see fit. The only reason to increase it is if there would be a high amount of traffic that could potentially cause 3 or more different messages to be received in between two copies of the same message.

The UserFrag Class and Supporting Functions

Getting back to the fragmenting / defragmenting of long messages, we have the UserFrag Class. We use this to create the object that will hold the pieces of the message until they are all received. For this I use an ArrayList. Here is the Class.

C#
private class UserFrag
{
  private string _UserName;
  private string _Command;
  private string _Text;

  public UserFrag(string Username, string Command, string Text)
  {
    _UserName = Username;
    _Command = Command;
    _Text = Text;
  }

  public string User
  {
    get { return _UserName; }
  }

  public string Defrag(string Text)
  {
    return _Text + Text;
  }

  public void AddFrag(string Text)
  {
    _Text += Text;
  }
}

The UserFrag Class has a Constructor, a Property, and 2 Methods. The Constructor is passed the Username, Command, and Text. This is used to create the object. When another piece is received, it is added to the message via AddFrag. When the final piece is received, it is passed to Defrag which then returns the entire message text.

This looks easy enough, but there is something missing. We need 2 more functions to properly handle the fragments. The naming is somewhat confusing, but these functions will be AddFrag and Defrag. Note that these are not the same functions as you see above. Those are internal to the UserFrag Class, and are called from the following two functions.

C#
private void AddFrag(string User, string Command, string Text)
{
  bool isNewFrag = true;

  foreach (UserFrag frag in fragQ)
  {
    if (frag.User == User)
    {
      frag.AddFrag(Text);
      isNewFrag = false;
    }
  }

  if (isNewFrag == true)
  {
    UserFrag newFrag = new UserFrag(User, Command, Text);
    fragQ.Add(newFrag);
  }
}

When AddFrag is called, it first iterates through any existing fragments stored in the fragQ ArrayList. If a fragment is found, then frag.AddFrag is called to add the received data to the existing fragment. Here, frag.AddFrag is calling the AddFrag function defined above in the UserFrag Class. If an existing fragment is not found, then a new UserFrag object is created (newFrag), passed the required constructor parameters, and added to fragQ.

The final function, Defrag, returns the complete message and removes the corresponding
UserFrag object from fragQ.

C#
private string Defrag(string User, string Text)
{
  string result = "";
  UserFrag rFrag = null;

  foreach (UserFrag frag in fragQ)
  {
    if (frag.User == User)
    {
      result = frag.Defrag(Text);
      rFrag = frag;
    }
  }
  if (result == "")
    result = Text;
  if (rFrag != null)
    fragQ.Remove(rFrag);
  return result;
}

Above you can see where we call frag.Defrag to retrieve the completed message. Here, frag.Defrag is calling the Defrag Method of the UserFrag object.

Points of Interest

There are many possible applications for this control. It would be an easy drop in for a LAN chat program, or for remote control purposes. The Command parameter can be used to avoid any need for parsing on your part. If no user name is required for your application, then there are 3 usable parameters at your disposal with no parsing at all. There are a few minor functions you will see in the demo project that do not appear here. I will post the source and explanation if there is a request for it, otherwise you should be able to figure it out from the demo.

You are free to use this control and source for any personal or educational / non-profit projects. For there is any interest in a commercial application, get in touch with me and I'm sure we can work out something.

History

  • June 17, 2007 - Original Posting

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 4 Pin
H Brick12-Jul-10 23:06
H Brick12-Jul-10 23:06 
GeneralQuite Helpful Pin
rynaskir15-Oct-09 9:00
rynaskir15-Oct-09 9:00 
AnswerRe: Quite Helpful Pin
Jim Weiler15-Oct-09 14:52
Jim Weiler15-Oct-09 14:52 
GeneralThank you Pin
knoami18-Jun-07 6:33
knoami18-Jun-07 6:33 
GeneralRe: Thank you Pin
Jim Weiler18-Jun-07 6:57
Jim Weiler18-Jun-07 6:57 
GeneralQuestion Pin
merlin98118-Jun-07 4:16
professionalmerlin98118-Jun-07 4:16 
AnswerRe: Question Pin
Jim Weiler18-Jun-07 4:32
Jim Weiler18-Jun-07 4:32 
GeneralIf you found this article helpful, please vote! Pin
Jim Weiler17-Jun-07 18:12
Jim Weiler17-Jun-07 18:12 
GeneralRe: If you found this article helpful, please vote! Pin
PrasertHong25-Mar-08 18:31
PrasertHong25-Mar-08 18:31 
GeneralRe: If you found this article helpful, please vote! Pin
gs_virdi4-Sep-08 21:01
gs_virdi4-Sep-08 21:01 
AnswerRe: If you found this article helpful, please vote! Pin
Jim Weiler16-Oct-09 11:07
Jim Weiler16-Oct-09 11:07 

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

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