- The Network
- Multithreading
- A Note About IM Applications
- Let's Build It!
- Conclusion
Let's Build It!
First, we need the basic frameworka template for a standard Windows Forms application. This is shown in Listing 1.
Listing 1The Framework
1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: using System.Net.Sockets; 5: using System.Threading; 6: using System.IO; 7: using System.ComponentModel; 8: 9: namespace MyNamespace { 10: public class Chat : Form { 11: private Button btSend = new Button(); 12: private RichTextBox rtbMessage = new RichTextBox(); 13: private RichTextBox rtbType = new RichTextBox(); 14: 15: public Chat() { 16: rtbMessage.Dock = DockStyle.Top; 17: rtbMessage.Size = new Size(300,200); 18: 19: rtbType.Location = new Point(0,205); 20: rtbType.Size = new Size(240,65); 21: 22: btSend.Text = "Send"; 23: btSend.Click += new EventHandler(this.SendText); 24: btSend.Size = new Size(50,50); 25: btSend.Location = new Point(240,205); 26: 27: this.Text = ".NET IM"; 28: this.Size = new Size(300,300); 29: this.Closing += new CancelEventHandler(this.CloseMe); 30: this.Controls.Add(btSend); 31: this.Controls.Add(rtbMessage); 32: this.Controls.Add(rtbType); 33: } 34: 35: private void SendText(Object Sender, EventArgs e) { 36: } 37: 38: private void CloseMe(Object Sender, CancelEventArgs e) { 39: } 40: 41: public static void Main() { 42: Application.Run(new Chat()); 43: } 44: } 45: } |
This very simple code produces Figure 1. Briefly, you first have the namespaces you'll use, taking into account the threading and net classes you'll need. Next, you have the namespace, class, and several Windows Forms controls to provide the user interface. These are all initialized in the constructor, Chat, and a few event handlers are addedspecifically, to handle the "Send" button click, and to perform some processing when the form closes. Those empty event handlers are shown next, followed by the Main method, which starts the application.
Figure 1 Your basic IM window
First thing's first. You need to create a mechanism to send messages. That's simple enough: declare a new TcpClient object somewhere between lines 11 and 14:
private TcpClient objClient;
The only time you actually need to use this object is when the user sends a messagethat is, when they click the "Send" button. So let's modify the SendText method:
private void SendText(Object Sender, EventArgs e) { rtbMessage.Text += strMe + ": " + rtbType.Text + "\n"; objClient = new TcpClient("127.0.0.1", 1000); StreamWriter w = new StreamWriter(objClient.GetStream()); w.Write(rtbType.Text + "\n"); w.Flush(); objClient.Close(); rtbType.Text = ""; } |
The second line displays the entered text in the RichTextBox. This allows you to keep a record of the conversation. The next line instantiates the TcpClient object. The first parameter of the constructor is the IP address of the person you're going to talk to. In this case, we're going to talk to ourselves (and before you ask, yes, I do have friends), so use the standard IP address that points to your local computer. You could also use "localhost" here, or use any other DNS name that works for you. The second parameter is the port number you want to send to. This number is relatively arbitraryjust choose something in the thousands.
Next, you need to actually send the message. If you're familiar with the .NET method of network messaging, you'll know that this is done by retrieving a Stream on the network that you can write to. Essentially, the GetStream method grabs the ear of the receiver, and prepares to yak into it. We use a StreamWriter object to do the actual writing. The Write method writes the contents from the RichTextBox (followed by a new line character), and Flush ensures that the data is sent immediately (rather than waiting for any network buffering).
Finally, you close the TcpClient. (This is very important; otherwise, you'd maintain a connection to the receiver, and that's not necessary.) Erase the contents of the "rtbType" box, so the user can start anew.
So now your little application can send messages to itself, but no one's listening yet. To do this, you need another thread and a TcpListener object. Add the following code before line 14:
private Thread thdListener; private TcpListener objListener;
Somewhere in the constructor, add the following code:
thdListener = new Thread(new ThreadStart(this.Listen)); thdListener.Start();
The first line instantiates your new thread. ThreadStart is actually a delegate (like EventHandler) that points the application to an event-handling methodthe Listen method in this case. Essentially, this first line tells the application that when the new thread starts, the first thing it should do is execute the Listen method. The second line actually starts the new thread off and running. Next, we need the Listen method:
private void Listen() { string strTemp = ""; objListener = new TcpListener(1000); objListener.Start(); do { TcpClient objClient = objListener.AcceptTcpClient(); StreamReader objReader = new StreamReader(objClient.GetStream()); while (objReader.Peek() != -1) { strTemp += Convert.ToChar(objReader.Read()).ToString(); } object[] objParams = new object[] {strTemp}; strTemp = ""; this.Invoke(new Invoker(this.ShowMessage), objParams); } while(true != false); } |
This method starts off by instantiating the TcpListener object; the third line tells it to listen on port 1000 (this should probably be the same as the port you're sending on; otherwise, you won't receive the communications). The Start method, naturally, starts the listener listening.
Recall that the listener needs to alert the main thread when it hears something, and then goes back to waiting. Thus, we introduce the infinite do loop (because true will never equal false, this loop executes indefinitely). Inside this loop is where the receiving and notifying will be done.
Now that the TcpListener is listening, you need to tell it what to listen for. The AcceptTcpClient method causes the TcpListener to sit there until a TcpClient (actually, any application that communicates via TCP) contacts it. If no one ever sends you an instant message, the execution will never get past the AcceptTcpClient line (which, if this weren't on a different thread, would cause your application to freeze). When you do finally receive some communication, you use the GetStream method to grab the raw contents of the message, wrapping it in a StreamReader to return the contents in a human readable format. Next, you go into a loop to read the message, character by character. Note that the Read method of the StreamReader returns everything in numbers. Thus, you need to use the Convert.ToChar method to change those numbers back into the text characters they represent. Peek tells you when you reach the end of the message.
NOTE
You can't use the StreamReader's ReadToEnd method here because, given the network communication, the StreamReader can't determine the end. It simply waits and waits and waits for more information, and your IM application isn't very useful.
Let's skip down to the third-to-last line for a moment. Communication between threads is often a risky procedure if not done properly. You need a way to alert the main thread safely, and the Invoke method is your savior. Invoke takes two parameters: a delegate that points to a method that the main thread should execute once it receives the alert, and any parameters that should be sent to that method, in an array. The Invoker delegate here is a custom one. To create it, simply add the following code somewhere before line 14:
public delegate void Invoker(String t);
This delegate will pass a string (the received message) to the event handler. This string is the one you built from the StreamReader, but you need to put it in an array to properly send it using Invoke; so this is done a few lines above.
In a nutshell, here's the plan of execution: First, the new thread starts and executes the Listen method; then it instantiates a TcpListener, and waits for someone to contact it. When a submission is received, it retrieves the message into a string using a StreamReader; next, it alerts the main thread, telling it to execute the ShowMessage function, passing in the newly built string; then it goes back to waiting for a message. Rinse and repeat.
Let's look at the ShowMessage function. It's very simple:
private void ShowMessage(String t) { rtbMessage.Text += t + "\n"; } |
This method simply takes the string passed into it, and displays it in the RichTextBox.
There's only one thing left to do: When your application quits, you need to break the infinite loop in the Listen method and stop the TcpListener from listening. If Listen had been executed on the main thread, it would stop when your application closed. However, because it's in another thread, it will continue on, regardless of what the main thread does. So, in the event handler for the form's Closing event, you stop the TcpListener:
private void CloseMe(Object Sender, CancelEventArgs e) { objListener.Stop(); } |
And you're done! As you can see, it doesn't take much to build a chat application. You can compile this code from the command line using the command (assuming, of course, that you saved this code as Chat.cs):
csc /t:winexe /r:system.dll,system.drawing.dll,system.windows.forms.dll Chat.cs
Listing 2 shows the code in its entirety, with some minor modifications to make it more user-friendly. Figure 2 shows the output after some nice chatting with myself.
Listing 2A complete Instant Messaging Application
using System; using System.Windows.Forms; using System.Drawing; using System.Net.Sockets; using System.Threading; using System.IO; using System.ComponentModel; namespace MyNamespace { public class Chat : Form { public delegate void Invoker(String t); private Thread thdListener; private TcpListener objListener; private TcpClient objClient; private Button btSend = new Button(); private RichTextBox rtbMessage = new RichTextBox(); private RichTextBox rtbType = new RichTextBox(); private string strFriend; private string strMe; public Chat() { strFriend = "127.0.0.1"; strMe = "Chris"; thdListener = new Thread(new ThreadStart(this.Listen)); thdListener.Start(); rtbMessage.Dock = DockStyle.Top; rtbMessage.Size = new Size(300,200); rtbType.Location = new Point(0,205); rtbType.Size = new Size(240,65); btSend.Text = "Send"; btSend.Click += new EventHandler(this.SendText); btSend.Size = new Size(50,50); btSend.Location = new Point(240,205); this.Text = ".NET IM"; this.Size = new Size(300,300); this.Closing += new CancelEventHandler(this.CloseMe); this.Controls.Add(btSend); this.Controls.Add(rtbMessage); this.Controls.Add(rtbType); } private void SendText(Object Sender, EventArgs e) { rtbMessage.Text += strMe + ": " + rtbType.Text + "\n"; objClient = new TcpClient(strFriend, 1000); StreamWriter w = new StreamWriter(objClient.GetStream()); w.Write(rtbType.Text + "\n"); w.Flush(); objClient.Close(); rtbType.Text = ""; } private void Listen() { string strTemp = ""; objListener = new TcpListener(1000); objListener.Start(); do { TcpClient objClient = objListener.AcceptTcpClient(); StreamReader objReader = new StreamReader(objClient.GetStream()); while (objReader.Peek() != -1) { strTemp += Convert.ToChar(objReader.Read()).ToString(); } object[] objParams = new object[] {strTemp}; strTemp = ""; this.Invoke(new Invoker(this.ShowMessage), objParams); } while(true != false); } private void ShowMessage(String t) { rtbMessage.Text += strFriend + ": " + t + "\n"; } private void CloseMe(Object Sender, CancelEventArgs e) { objListener.Stop(); } public static void Main() { Application.Run(new Chat()); } } } |
Figure 2 Talking to yourself is fun.