<

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x

A Peer-To-Peer LAN Chat Application in Visual Basic.Net using TcpClient and TcpListener

Published on
63,150 Points
50,650 Views
15 Endorsements
Last Modified:
Awarded
Editor's Choice

YaLanCha - Yet Another LAN Chat Application?

I needed a simple person-to-person chat system for use only in my home network behind my router firewall. My only requirements were that the system not require a central server and that it not require any kind of installation other than "copy and run". My intended use of this chat system would entail only simple text conversations with the ability to send a link to a web site. I did not need or want any fancier features such as chat rooms, emoticons, file transfers, voice chats, group broadcasts, or online statuses. I just wanted a system that was always on and was ready to send/receive text messages to any other computer in the household. A quick search of the web yielded a huge number of possibilities; many of them free and quite capable. Ultimately, though, I found myself not wanting a bloated chat system that I didn't really trust not to be infected with a virus or malware.

With that in mind, I decided to roll my own chat system from scratch. I had written simple chat systems before using the Winsock control in VB6, but I left that language long ago for the newer .Net world. Unfortunately, the .Net framework does not provide a direct replacement for the Winsock control. This seemed a perfect time and opportunity to tackle the TcpClient and TcpListener classes -- something that had been on my "To Do" list for quite some time. Furthermore, I decided that I would use a standard WinForms project targetting the .Net framework 2.0, and only utilize standard, basic controls. With these constraints, the chat system would run on a wide variety of machines and could be modified and/or compiled by anyone using the free "Express" editions of Visual Studio.

For the record, my home network consists of three laptops, all running Windows 7 Professional with a single admin account on each. My laptop sits downstairs, next to the kitchen and television, which as a stay-at-home dad is my "command central". The other two laptops are upstairs, one at a central desk location and the other in my daughter's room. It is this latter location, in my daughter's room, that really prompted the "need" for this system. As a teenager she is often in her room with music playing and the door closed to keep her younger brothers at bay. It is under these conditions that she cannot hear me calling for her, even with an elevated voice level (yelling). By rolling my own chat system, I can quickly get her attention without requiring any other programs to be open on her system. This is quite useful when I'm busy in the kitchen and can't run upstairs (leaving dinner to burn on the stove).

Besides my need to learn TcpListener and TcpClient, this was also a great excuse to write another article.  Let's get started!...

You got served! - The Server via TcpListener

Since the system will be peer-to-peer, each computer must be running its own server that is able to handle multiple concurrent connections. This is surprisingly easy to do with the TcpListener class in only three basic steps. Before that, though, we must decide on which port the server will listen for incoming connection requests on. In a nutshell, just pick a random number greater than 1,024 and less than or equal to 65,535. The port numbers below 1,024 are well established for other services and should not be used. I decided to use 50,000 for my port number. With that chosen, the first step is to create an instance of TcpListener and pass our chosen port number in as the second parameter:
Dim Server As New TcpListener(IPAddress.Any, 50000)

Open in new window

The IPAddress.Any option tells the TcpListener to listen for connections on all network interfaces. The second step is to tell the TcpListener to actually start listening for connections using the aptly named Start() method:
Server.Start()

Open in new window

The third and final step is to tell the TcpListener to accept any incoming connections using the AcceptTcpClient() method:
Dim client As TcpClient = Server.AcceptTcpClient

Open in new window

The AcceptTcpClient() method actually blocks until a connection request is received and only accepts one connection at a time. It returns a TcpClient instance that can be used to send and receive data to and from the client on the other side. Since the method only accepts one connection at a time, we place that call in a loop so that all pending connection requests are serviced:
While True
	Dim client As TcpClient = Server.AcceptTcpClient	
End While

Open in new window

     
Obviously it's not very useful to simply accept the connection and then do nothing with the resulting TcpClient instance. Since AcceptTcpClient() is blocking, we need to hand the resulting TcpClient instances off to another thread so both server and client can be free to do their thing without worrying about what the other is doing. Here is how to spawn a new thread for each connection and pass the TcpClient to it:
While True
	Dim client As TcpClient = Server.AcceptTcpClient	
	Dim T As New Thread(AddressOf StartChatForm)
	T.Start(client)
End While

Open in new window

The StartChatForm() method simply has this signature:
Private Sub StartChatForm(ByVal client As Object)
	' ... do something with "client" in here ...
End Sub

Open in new window

                 
That's it! We now have all the pieces to a working server that listens on port 50,000 and will accept multiple connections. Here is what it looks like put all together:
Imports System.Net
Imports System.Threading
Imports System.Net.Sockets
Public Class ChatServer
	Inherits ApplicationContext
	
	Private Server As TcpListener = Nothing
	Private ServerThread As Thread = Nothing

	Public Sub New()
		Server = New TcpListener(IPAddress.Any, 50000)
		ServerThread = New Thread(AddressOf ConnectionListener)
		ServerThread.IsBackground = True
		ServerThread.Start()
	End Sub

	Private Sub ConnectionListener()
		Server.Start()
		While True
			Dim client As TcpClient = Server.AcceptTcpClient()
			Dim T As New Thread(AddressOf StartChatForm)
			T.Start(client)
		End While
	End Sub

	Private Sub StartChatForm(ByVal client As Object)
		' ... do something with "client" in here ...
	End Sub

End Class

Open in new window


Singles Ads - Find available Computers in your Network with the NetServerEnum() API

So now that we have a working server that can accept multiple connections, how do we connect to it? Since every computer in the network will be running its own server, a better question is, how do we list all computers in our network? Getting a list of computers in the local network is quite easy using the NetServerEnum() API. Here is the signature for it:
Public Declare Unicode Function NetServerEnum Lib "Netapi32.dll" ( _
	ByVal Servername As Integer, ByVal Level As Integer, ByRef Buffer As Integer, ByVal PrefMaxLen As Integer, _
	ByRef EntriesRead As Integer, ByRef TotalEntries As Integer, ByVal ServerType As Integer, _
	ByVal DomainName As String, ByRef ResumeHandle As Integer) As Integer

Open in new window

     
After calling NetServerEnum(), we will get back a pointer to a buffer containing SERVER_INFO_101 structures which has the name of each network computer in the Name() field:
Public Structure SERVER_INFO_101
	Public Platform_ID As Integer
	<MarshalAsAttribute(UnmanagedType.LPWStr)> Public Name As String
	Public Version_Major As Integer
	Public Version_Minor As Integer
	Public Type As Integer
	<MarshalAsAttribute(UnmanagedType.LPWStr)> Public Comment As String
End Structure

Open in new window

After calling NetServerEnum(), we have to manually free the memory used by that buffer with the NetApiBufferFree() API:
Public Declare Function NetApiBufferFree Lib "Netapi32.dll" (ByVal lpBuffer As Integer) As Integer

Open in new window

As these APIs aren't really the focus of the article, though, let's skip the gory details and simply post a utility function which returns a List(Of String) containing available network computers:
Public Shared Function GetNetworkComputers(Optional ByVal DomainName As String = Nothing) As List(Of String)
	Dim level As Integer = 101
	Dim MaxLenPref As Integer = -1
	Dim ResumeHandle As Integer = 0
	Dim ServerInfo As SERVER_INFO_101
	Dim SV_TYPE_ALL As Integer = &HFFFFFFFF
	Dim ret, EntriesRead, TotalEntries, BufPtr, CurPtr As Integer
	Dim ReturnList As New List(Of String)

	Try
		ret = NetServerEnum(0, level, BufPtr, MaxLenPref, EntriesRead, TotalEntries, SV_TYPE_ALL, DomainName, ResumeHandle)
		If ret = 0 Then
			CurPtr = BufPtr
			For i As Integer = 0 To EntriesRead - 1
				ServerInfo = CType(Marshal.PtrToStructure(New IntPtr(CurPtr), GetType(SERVER_INFO_101)), SERVER_INFO_101)
				CurPtr = CurPtr + Len(ServerInfo)
				ReturnList.Add(ServerInfo.Name)
			Next
		End If
		NetApiBufferFree(BufPtr)
	Catch ex As Exception
	End Try

	Return ReturnList
End Function

Open in new window

The list of systems returned by GetNetworkComputers() could be assigned to the DataSource of a ListBox, making it easy to select a computer to connect to:
lbComputers.DataSource = GetNetworkComputers

Open in new window

     
Here is an example of what is typically returned when run on my network:
The NetServerEnum() API in action!      

Missed Connections - Me, wanting to chat. You, on the network, but listing just your computer name.

Making a connection with the server is just as simple as the server accepting a connection. First we create an instance of TcpClient, then we call the Connect() method:
Dim client As New TcpClient()
client.Connect(ConnectTo, 50000)

Open in new window

     
The ConnectTo parameter is simply a string, and can be a computer name such as "MIKE-LAPTOP" returned from the GetNetworkComputers() function outlined earlier. The system will resolve the computer name automatically and attempt to connect to the associated IP address. The second parameter is the port number to connect to and is the same 50,000 value I decided upon earlier when discussing the server code. If successfull, the TcpClient instance returned by Connect() can be used to send and receive messages.

That's it! Really. The above two lines are all that is necessary to connect a client to a server.
                  

Small Talk - Sending and Receiving Messages with TcpClient

The TcpClient instances generated by the server using AcceptTcpClient, or via the Connect() method in the client, will handle both the send and receive streams of the conversation. The streams are simply sequences of bytes, which are handled using byte arrays. Both sending and receiving data can be done in either a synchronous (blocking) or asynchronous (non-blocking) manner. Let's take a look at sending data first, since it's the easier operation of the two. Let's assume that a valid TcpClient has been provided to us and we'll reference it using a variable called "client":
Dim client As TcpClient ' <--- Provided to us by our TcpListener Server

Open in new window

Now let's create a byte array from a string value so we can send it as a message:
Dim msg As String = "Hello Client!"
Dim bytes() As Byte = System.Text.ASCIIEncoding.ASCII.GetBytes(msg)

Open in new window

To send the message, we simply use the GetStream() property of the TcpClient and call its Write() method:
client.GetStream.Write(bytes, 0, bytes.Length)

Open in new window

Using Write() is the synchronous version of sending data. Execution will halt at this line until all the data has been sent. If called directly from within a GUI, this could cause the interface to freeze until the send is complete. One solution would be to place the call to Write() on another thread. Another would be to execute the send as an asynchronous call using the BeginWrite() method. Here is how that would look:
client.GetStream.BeginWrite(bytes, 0, bytes.Length, AddressOf MyWriteCallBack, client.GetStream)

Open in new window

The BeginWrite() call looks the same as Write(), but has two extra parameters. The fourth parameter is a delegate to the callback which will be executed once all the data has been sent. The fifth parameter is the Network stream itself, and is passed so it can be accessed within the callback:
Public Sub MyWriteCallBack(ByVal ar As IAsyncResult)
	CType(ar.AsyncState, NetworkStream).EndWrite(ar)
End Sub

Open in new window

     
In the callback, we use EndWrite() to complete the process started with BeginWrite(). Note that the callback is started immediately in its own thread as soon as BeginWrite() is called, and that the EndWrite() method is actually a blocking call. As such, if you need something to occur after the data is sent, you could trigger it from the callback after the line containing EndWrite(). Don't forget that the callback runs in its own thread, though, so take that into consideration if you need to interact with the GUI. Here's a simple example of wrapping the asynchronous send process in a procedure:
Public Sub SendMessage(ByVal message As String)
	Dim bytes() As Byte = System.Text.ASCIIEncoding.ASCII.GetBytes(message)
	client.GetStream.BeginWrite(bytes, 0, bytes.Length, AddressOf MyWriteCallBack, client.GetStream)
End Sub
	
Private Sub MyWriteCallBack(ByVal ar As IAsyncResult)
	CType(ar.AsyncState, NetworkStream).EndWrite(ar)
End Sub

Open in new window

     
Right, with the write half taken care of, let's explore how to handle the read stream of the client. Unlike in a send operation where we know beforehand how much data is going to be sent, the amount of data received from the client isn't known until it actually comes across the line. Since we can't know beforehand how much data will arrive at any given time, we also cannot declare a byte array of the exact required size. Instead, we simply create a fixed size buffer, and fill it multiple times if necessary until all the received data has been obtained. The buffer below has been declared to handle 1,024 bytes at at time:
Dim bytesRead As Integer
Dim buffer(1024) As Byte

Open in new window

Now we call GetStream() as before, followed by Read();
bytesRead = client.GetStream.Read(buffer, 0, buffer.Length)

Open in new window

     
Just as with Write(), this version of Read() is a blocking synchronous call. The Read() function will just sit there until data has been received and is available. When execution resumes, the number of bytes read and placed into the buffer is returned from the function and stored in our bytesRead variable. If the connection was closed, Read() will return zero bytes so you should always check for that condition:
If bytesRead > 0 Then
	' ... do something with "bytesRead" and "buffer" ...
End If

Open in new window

     
As before with the server and accepting clients, placing the Read() call inside a loop will ensure that all data received is processed:
Dim bytesRead As Integer
Dim buffer(1024) As Byte
While True
	bytesRead = client.GetStream.Read(buffer, 0, buffer.Length)
	If bytesRead > 0 Then
		Debug.Print(System.Text.ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead))
	End If
End While

Open in new window

As Read() is a blocking call, the above loop would need to be in its own thread if used in a GUI application. Note that there is also an alternate BeginRead() that works in a similar fashion to BeginWrite(). Using one or the other really comes down to preference and how you like to style your code. We'll visit this loop again in a little bit.

In summary, sending and receiving data with the TcpClient can be accomplished using byte arrays and is really quite easy. Plain text can be converted to a btye array using System.Text.ASCIIEncoding.ASCII.GetBytes(), while the reverse operation can be accomplished via System.Text.ASCIIEncoding.ASCII.GetString().
      

Stream of Consciousness - He's talking a lot, but not really making any sense!

So the TcpClient neatly encapsulates both the read and write streams in the GetStream() method, which returns the underlying NetworkStream. What do we really mean when we say "stream" though? Think of a stream as a continuous flow of data with no discernible beginning or end. It is this latter part that is quite important and what we shall focus on next. The underlying TCP/IP protocol ensures that data arrives in the correct order, making it very useful, but it does not make any guarantees as to how that data is grouped together when it arrives!

For example, if we sent "halfway", it may arrive in two different chunks as "half" and "way". Another possibility is that different sends arrive as one big chunk. For instance, if we sent "halfway" and "there" as two different, distinct sends, they both may arrive toghether as "halfwaythere". This isn't perceived as a limitation or bug from the perspective of TCP/IP since all the data that was sent is indeed received in the same order! The point here is that the streams have no notion or knowledge of where your data breaks into discrete "messages". To the streams, everything is just an endless flow of bytes.

It is up to the designer, then, to come up with a protocol that will allow the receiver to discern when a complete message, or multiple messages, have been received. This can be accomplished using fixed numbers of bytes, special byte sequences or delimiters, or even a combination of both. The method I've chosen to employ in my chat application is to use special delimiter characters inserted between the codes and the actual messages typed by the users. Both the codes and the user messages are simple plain text so my delimiters can be any of the non-visible control characters available in the standard ASCII set (any of the ASCII values below 32). Specifically, I decided to use Chr(1) to denote the end of a complete message, and Chr(0) to delimit the values within the message. The format for a complete message in my protocol would then simply be:

      SomePlainTextCode Chr(0) MessageTypedByUser Chr(1)
      
The complete message would first be built as a standard string, and then coverted to a byte array:
Dim msg As String = "GREETING" & Chr(0) & "Idle_Mind" & Chr(1)
Dim bytes() As Byte = System.Text.ASCIIEncoding.ASCII.GetBytes(msg)

Open in new window

     
On the receiving end, we convert all arriving bytes back to text and append that to the end of a standard string variable. After each concatenation we can now determine if one or more complete messages have arrived by checking for the presence of our "end of message" character Chr(1). If found, those complete messages are extracted from the string variable and passed on to the GUI for processing. Any partially received messages will remain in the string variable until the rest of the data for those partial messages arrives. When processing a complete message, we simply Split() the string using the field delimiter character of Chr(0) to separate out the different field values. This simple scheme allows us to pass different kinds of messages while keeping codes, values and the messages themselves neatly separated.

In the final application, I ended up using eight different codes:
Public Enum MessageCodes ' Enter all codes below in --> UPPER CASE <---
	ACK = 0
	TEXT = 1
	DISCONNECTED = 2
	GREETING = 3
	GREETINGRESPONSE = 4
	PAGE = 5
	TYPING = 6
	TYPINGCANCEL = 7
End Enum

Open in new window

Here is a boiled down version of how the receive code might look using this approach:
Private FieldMarker As String = Chr(0)
Private MessageMarker As String = Chr(1)

Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
	Dim bytesRead As Integer
	Dim buffer(1024) As Byte
	Dim Messages As String = ""
	Dim MessageMarkerIndex As Integer
	While True
		bytesRead = client.GetStream.Read(buffer, 0, buffer.Length) ' <-- Blocks until Data is Received
		If bytesRead > 0 Then ' <-- Zero is returned if Connection is Closed and no more data is available
			Messages = Messages & System.Text.ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead) ' Append the received data to our message queue
			MessageMarkerIndex = Messages.IndexOf(MessageMarker) ' See if the End of Message marker is present
			While MessageMarkerIndex <> -1 ' If we have received at least one complete message
				BackgroundWorker1.ReportProgress(0, Messages.Substring(0, MessageMarkerIndex)) ' Let the GUI handle the complete Message
				Messages = Messages.Remove(0, MessageMarkerIndex + 1) ' Remove the processed message
				MessageMarkerIndex = Messages.IndexOf(MessageMarker) ' See if there are more End of Message markers present
			End While
		End If
	End While
End Sub

Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
	If Not IsNothing(e.UserState) AndAlso TypeOf e.UserState Is String Then
		Dim msg As String = CType(e.UserState, String)
		Dim values() As String = msg.Split(FieldMarker)
		If values.Length >= 2 Then ' Forward compatibility for messages with more than two fields
			Dim strCode As String = values(0)
			Dim value As String = values(1) ' All messages should have at least two fields (even if the second isn't used)
			' ... do something with "strCode" and "value" ...
		End If
	End If
End Sub

Open in new window

     
The "do something" line is replaced with a big Select Case statement that takes different actions based on the received code.

Beauty Pageant - Putting it all together in a complete package!

The finished project starts off in Sub Main and passes an ApplicationContext to Application.Run(). This is the same technique described in my previous article, "Building a Formless WinForms Application with a NotifyIcon in VB.Net 2008":
http://www.experts-exchange.com/Programming/Languages/.NET/Visual_Basic.NET/A_2729-Building-a-Formless-WinForms-Application-with-a-NotifyIcon-in-VB-Net-2008.html

Below is the entirety of Module1 containing my Sub Main:
Module Module1

	Private CS As New ChatServer

	Public Sub Main()
		Application.Run(CS)
	End Sub

End Module

Open in new window

     
The complete ChatServer() class is very similar to the previously posted version, with additional code to display a TrayIcon, and code to start an instance of the ChatForm() form in its own thread:
Imports System.Net
Imports System.Threading
Imports System.Net.Sockets
Public Class ChatServer
	Inherits ApplicationContext

	Private Server As TcpListener = Nothing
	Private ServerThread As Thread = Nothing
	Private WithEvents Tray As New NotifyIcon
	Private Threads As New List(Of Thread)

	Public Sub New()
		Tray.Icon = My.Resources.Chat
		Tray.Visible = True
		Tray.Text = "LAN Chat"

		Server = New TcpListener(IPAddress.Any, 50000) ' <-- Listen on Port 50,000
		ServerThread = New Thread(AddressOf ConnectionListener)
		ServerThread.IsBackground = True
		ServerThread.Start()
	End Sub

	Private Sub ConnectionListener()
		Try
			Server.Start()
			While True
				Dim client As TcpClient = Server.AcceptTcpClient ' Blocks until Connection Request is Received
				Dim T As New Thread(AddressOf StartChatForm)
				Threads.Add(T)
				T.Start(client)
			End While
		Catch ex As Exception
			MessageBox.Show("Unable to Accept Connections", "LAN Chat Server Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
		End Try
		Application.ExitThread()
	End Sub

	Private Sub StartChatForm(ByVal client As Object)
		Application.Run(New ChatForm(CType(client, TcpClient))) ' Start a New ChatForm with the TcpClient Connection
		Threads.Remove(Thread.CurrentThread) ' We don't get here until the ChatForm is closed
	End Sub

	Private Sub Tray_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Tray.Click
		ChatStart.Show()
	End Sub

	Private Sub ChatServer_ThreadExit(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.ThreadExit
		Tray.Visible = False
	End Sub

End Class

Open in new window

     
The StartChatForm() method simply passes the TcpClient returned by the AcceptTcpClient() method off to a new instance of the ChatForm() form.

Clicking the TrayIcon causes the ChatStart() form to show. The ChatStart() form simply lists all computers in the LAN using the previously mentioned NetServerEnum() API. After selecting another computer, that computer's name is passed off to an instance of the ChatForm() form:
Imports System.Runtime.InteropServices
Public Class ChatStart

	Private Sub ChatStart_Shown(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Shown
		btnRefresh.PerformClick()
	End Sub

	Private Sub btnRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRefresh.Click
		btnRefresh.Enabled = False
		lbComputers.DataSource = Nothing
		BackgroundWorker1.RunWorkerAsync()
	End Sub

	Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
		e.Result = ChatStart.GetNetworkComputers()
	End Sub

	Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
		lbComputers.DataSource = CType(e.Result, List(Of String))
		btnRefresh.Enabled = True
	End Sub

	Private Sub lbComputers_DoubleClick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lbComputers.DoubleClick
		btnChat.PerformClick()
	End Sub

	Private Sub btnChat_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnChat.Click
		If lbComputers.SelectedIndex <> -1 Then
			Dim chat As New ChatForm(lbComputers.SelectedItem.ToString)
			chat.Show()
			Me.Close()
		End If
	End Sub

#Region "NetServerEnum_API"

	Public Const SV_TYPE_ALL As Integer = &HFFFFFFFF

	Public Structure SERVER_INFO_101
		Public Platform_ID As Integer
		<MarshalAsAttribute(UnmanagedType.LPWStr)> Public Name As String
		Public Version_Major As Integer
		Public Version_Minor As Integer
		Public Type As Integer
		<MarshalAsAttribute(UnmanagedType.LPWStr)> Public Comment As String
	End Structure

	Public Declare Unicode Function NetServerEnum Lib "Netapi32.dll" ( _
		ByVal Servername As Integer, ByVal Level As Integer, ByRef Buffer As Integer, ByVal PrefMaxLen As Integer, _
		ByRef EntriesRead As Integer, ByRef TotalEntries As Integer, ByVal ServerType As Integer, _
		ByVal DomainName As String, ByRef ResumeHandle As Integer) As Integer

	Public Declare Function NetApiBufferFree Lib "Netapi32.dll" (ByVal lpBuffer As Integer) As Integer

	Public Shared Function GetNetworkComputers(Optional ByVal DomainName As String = Nothing) As List(Of String)
		Dim ServerInfo As SERVER_INFO_101
		Dim MaxLenPref As Integer = -1
		Dim level As Integer = 101
		Dim ResumeHandle As Integer = 0
		Dim ret, EntriesRead, TotalEntries, BufPtr, CurPtr As Integer
		Dim ReturnList As New List(Of String)

		Try
			ret = NetServerEnum(0, level, BufPtr, MaxLenPref, EntriesRead, TotalEntries, SV_TYPE_ALL, DomainName, ResumeHandle)
			If ret = 0 Then
				CurPtr = BufPtr
				For i As Integer = 0 To EntriesRead - 1
					ServerInfo = CType(Marshal.PtrToStructure(New IntPtr(CurPtr), GetType(SERVER_INFO_101)), SERVER_INFO_101)
					CurPtr = CurPtr + Len(ServerInfo)
					ReturnList.Add(ServerInfo.Name)
				Next
			End If
			NetApiBufferFree(BufPtr)
		Catch ex As Exception
		End Try

		Return ReturnList
	End Function

#End Region

End Class

Open in new window

     
This leaves us with only the code for ChatForm() to post. The ChatForm() is created by passing either a TcpClient, which was received from the ChatServer() class, or a string computer name that was received from the StartChatForm() form:
Imports System.Net.Sockets
Public Class ChatForm

	Public Enum Sound
		ChatOpen
		ChatMessage
		ChatPage
		ChatClose
	End Enum

	Public Enum MessageCodes ' Enter all codes below in --> UPPER CASE <---
		ACK = 0
		TEXT = 1
		DISCONNECTED = 2
		GREETING = 3
		GREETINGRESPONSE = 4
		PAGE = 5
		TYPING = 6
		TYPINGCANCEL = 7
	End Enum

	Private ConnectTo As String = ""
	Private ChatClient As TcpClient = Nothing
	Private FieldMarker As String = Chr(0)
	Private MessageMarker As String = Chr(1)
	Private Typing() As String = {"/", "-", "\", "|"}
	Private Const AckIntervalInSeconds As Integer = 60
	Private ContinueProcessingMessages As Boolean = True
	Private SendFinalDisconnectMessage As Boolean = False
	Private Shared Waves As New Dictionary(Of String, EmbeddedWave)

	Public Sub New()
		InitializeComponent()
	End Sub

	Public Sub New(ByVal ConnectTo As String)
		InitializeComponent()
		Me.ConnectTo = ConnectTo
		Me.Text = "Connecting to " & ConnectTo & " ..."
	End Sub

	Public Sub New(ByVal ChatClient As TcpClient)
		InitializeComponent()
		Me.ChatClient = ChatClient
	End Sub

	Private Sub ChatForm_Shown(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Shown
		AckTimer.Interval = TimeSpan.FromSeconds(AckIntervalInSeconds).TotalMilliseconds
		SetChatState(False)
		BackgroundWorker1.RunWorkerAsync()
	End Sub

	Private Sub SetChatState(ByVal state As Boolean)
		cbMute.Enabled = state
		cbFlash.Enabled = state
		AckTimer.Enabled = state
		btnSendPage.Enabled = state
		btnSendMessage.Enabled = state
		tbMessageToSend.Enabled = state
	End Sub

	Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
		If IsNothing(ChatClient) Then ' We are initiating the connection
			Try
				ChatClient = New TcpClient()
				ChatClient.Connect(ConnectTo, 50000) ' Blocks until connection is made
			Catch ex As Exception
				ContinueProcessingMessages = False
				BackgroundWorker1.ReportProgress(-2) ' Connection Failed
			End Try
		Else
			SendMessage(MessageCodes.GREETING, Environment.MachineName) ' We accepted a Connection: Send our name
		End If

		If Not IsNothing(ChatClient) AndAlso ChatClient.Connected Then
			BackgroundWorker1.ReportProgress(1) ' Enable the Chat Interface
			SendFinalDisconnectMessage = True

			Dim bytesRead As Integer
			Dim buffer(1024) As Byte
			Dim Messages As String = ""
			Dim MessageMarkerIndex As Integer
			While ContinueProcessingMessages
				Try
					bytesRead = ChatClient.GetStream.Read(buffer, 0, buffer.Length) ' <-- Blocks until Data is Received
					If bytesRead > 0 Then ' <-- Zero is returned if Connection is Closed and no more data is available
						Messages = Messages & System.Text.ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead) ' Append the received data to our message queue
						MessageMarkerIndex = Messages.IndexOf(MessageMarker) ' See if the End of Message marker is present
						While MessageMarkerIndex <> -1 ' If we have received at least one complete message
							BackgroundWorker1.ReportProgress(0, Messages.Substring(0, MessageMarkerIndex)) ' Let the GUI handle the complete Message
							Messages = Messages.Remove(0, MessageMarkerIndex + 1) ' Remove the processed message
							MessageMarkerIndex = Messages.IndexOf(MessageMarker) ' See if there are more End of Message markers present
						End While
					End If
				Catch ex As Exception
					ContinueProcessingMessages = False
					BackgroundWorker1.ReportProgress(-1)
				End Try
			End While
		End If
	End Sub

	Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
		Select Case e.ProgressPercentage
			Case -1 ' Raised From Exception in Receiving Loop in BackgroundWorker()
				SendFinalDisconnectMessage = False
				SetChatState(False)
				Me.Text = Me.Text & " {Connection Lost}"

			Case -2 ' Initial Connection Failed
				SendFinalDisconnectMessage = False
				Me.Text = "Failed to Connect!"
				MessageBox.Show("No response from " & ConnectTo & " ...", "Connection Failed!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
				Me.Close()

			Case 1 ' Connection Made
				SetChatState(True) ' Enable the Chat Interface

			Case 0 ' Normal Message Received
				If Not IsNothing(e.UserState) AndAlso TypeOf e.UserState Is String Then
					Dim msg As String = CType(e.UserState, String)
					Dim values() As String = msg.Split(FieldMarker)
					If values.Length >= 2 Then ' Forward compatibility for messages with more than two fields
						Dim strCode As String = values(0)
						Dim value As String = values(1) ' All messages should have at least two fields (even if the second isn't used)
						Try
							Dim code As MessageCodes = [Enum].Parse(GetType(MessageCodes), strCode.ToUpper)
							Select Case code
								Case MessageCodes.ACK ' Ack signal received 

								Case MessageCodes.GREETING ' We have received a name from the other side
									Me.Text = value
									DisplayMessage(Color.Red, value & " Connected")
									If Not cbMute.Checked Then
										ChatForm.Play(Sound.ChatOpen)
									End If
									SendMessage(MessageCodes.GREETINGRESPONSE, Environment.MachineName) ' Send our name back...

								Case MessageCodes.GREETINGRESPONSE ' We sent our name and have now received a name back
									Me.Text = value
									DisplayMessage(Color.Red, value & " Connected")
									If Not cbMute.Checked Then
										ChatForm.Play(Sound.ChatOpen)
									End If

								Case MessageCodes.TYPING ' The other side is typing a message...
									Static index As Integer = -1
									index = index + 1
									If index > Typing.GetUpperBound(0) Then
										index = 0
									End If
									lblStatus.Text = value & " " & Typing(index)

								Case MessageCodes.TYPINGCANCEL ' The other side has cleared their message textbox
									lblStatus.Text = ""

								Case MessageCodes.TEXT ' A text message from the other person has arrived
									DisplayMessage(Color.DarkGreen, value)
									If Not cbMute.Checked Then
										ChatForm.Play(Sound.ChatMessage)
									End If
									If cbFlash.Checked Then
										FlashWindow(Me.Handle)
									End If

								Case MessageCodes.PAGE ' We have received a Page request
									If Not cbMute.Checked Then
										ChatForm.Play(Sound.ChatPage)
									End If

								Case MessageCodes.DISCONNECTED ' The other person closed their chat window
									ContinueProcessingMessages = False
									lblStatus.Text = ""
									DisplayMessage(Color.Red, value)
									Me.Text = Me.Text & " {Disconnected}"
									SetChatState(False)
									SendFinalDisconnectMessage = False
									If Not cbMute.Checked Then
										ChatForm.Play(Sound.ChatClose)
									End If

							End Select
						Catch ex As Exception
						End Try
					End If
				End If
		End Select
	End Sub

	Private Function SendMessage(ByVal Code As MessageCodes, ByVal Value As String) As Boolean
		Try ' Async Write so we don't lock up the GUI in the event of dropped connections
			Dim msg() As Byte = System.Text.ASCIIEncoding.ASCII.GetBytes(Code.ToString & FieldMarker & Value & MessageMarker)
			ChatClient.GetStream.BeginWrite(msg, 0, msg.Length, AddressOf MyWriteCallBack, ChatClient.GetStream)
			Return True
		Catch ex As Exception
			SendFinalDisconnectMessage = False
		End Try
		Return False
	End Function

	Public Sub MyWriteCallBack(ByVal ar As IAsyncResult)
		Try
			CType(ar.AsyncState, NetworkStream).EndWrite(ar)
		Catch ex As Exception
		End Try
	End Sub

	Private Sub AckTimer_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles AckTimer.Tick
		SendMessage(MessageCodes.ACK, "Ack")
	End Sub

	Private Sub tbMessageToSend_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tbMessageToSend.TextChanged
		If tbMessageToSend.TextLength > 0 Then
			SendMessage(MessageCodes.TYPING, "Typing...")
		Else
			SendMessage(MessageCodes.TYPINGCANCEL, "Clear")
		End If
	End Sub

	Private Sub btnSendMessage_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSendMessage.Click
		If tbMessageToSend.Text.Trim <> "" Then
			If SendMessage(MessageCodes.TEXT, tbMessageToSend.Text) Then
				DisplayMessage(Color.Black, tbMessageToSend.Text)
				tbMessageToSend.Clear()
			Else
				SetChatState(False)
				Me.Text = Me.Text & " {Connection Lost}"
			End If
		End If
	End Sub

	Private Sub DisplayMessage(ByVal clr As Color, ByVal msg As String)
		Try
			Dim SelStart As Integer = tbMessageToSend.SelectionStart
			Dim SelLength As Integer = tbMessageToSend.SelectionLength

			rtbConversation.SelectionStart = rtbConversation.TextLength
			rtbConversation.SelectionColor = clr
			rtbConversation.SelectedText = "[" & DateTime.Now.ToShortTimeString & "] " & msg & vbCrLf
			rtbConversation.Focus()
			rtbConversation.ScrollToCaret()

			tbMessageToSend.Focus()
			tbMessageToSend.SelectionStart = SelStart
			tbMessageToSend.SelectionLength = SelLength
		Catch ex As Exception
		End Try
	End Sub

	Private Sub rtbConversation_LinkClicked(ByVal sender As System.Object, ByVal e As System.Windows.Forms.LinkClickedEventArgs) Handles rtbConversation.LinkClicked
		Try
			Process.Start(e.LinkText) ' Attemp to Open URL in the default browser
		Catch ex As Exception
			MessageBox.Show("Could not open URL: " & e.LinkText, "Unable to Open URL", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
		End Try
	End Sub

	Private Sub btnSendPage_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSendPage.Click
		SendMessage(MessageCodes.PAGE, "Paging")
	End Sub

	Private Sub ChatForm_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
		If SendFinalDisconnectMessage Then
			SendMessage(MessageCodes.DISCONNECTED, Environment.MachineName & " Disconnected")
		End If
		Try
			If Not IsNothing(ChatClient) AndAlso ChatClient.Connected Then
				ChatClient.Close()
			End If
		Catch ex As Exception
		End Try
	End Sub

	Private Shared Sub Play(ByVal ChatSound As Sound)
		Dim WaveName As String = ChatSound.ToString & ".wav"
		If Not Waves.ContainsKey(WaveName) Then
			Waves.Add(WaveName, New EmbeddedWave(WaveName))
		End If
		If Waves(WaveName).IsValid Then
			Waves(WaveName).Play()
		End If
	End Sub

#Region "FlashWindowEx_API"

	Public Const FLASHW_STOP As UInteger = 0
	Public Const FLASHW_CAPTION As Int32 = &H1
	Public Const FLASHW_TRAY As Int32 = &H2
	Public Const FLASHW_ALL As Int32 = (FLASHW_CAPTION Or FLASHW_TRAY)
	Public Const FLASHW_TIMERNOFG As Int32 = &HC

	Public Structure FLASHWINFO
		Public cbsize As Int32
		Public hwnd As IntPtr
		Public dwFlags As Int32
		Public uCount As Int32
		Public dwTimeout As Int32
	End Structure

	Public Declare Function FlashWindowEx Lib "user32.dll" (ByRef pfwi As FLASHWINFO) As Int32

	Private Sub FlashWindow(ByVal handle As IntPtr)
		Dim flash As New FLASHWINFO
		flash.cbsize = System.Runtime.InteropServices.Marshal.SizeOf(flash)
		flash.hwnd = handle
		flash.dwFlags = FLASHW_ALL Or FLASHW_TIMERNOFG
		FlashWindowEx(flash)
	End Sub

#End Region

#Region "Class EmbeddeWave()"

	Public Class EmbeddedWave

		Private _WaveBytes() As Byte, _WaveLoaded As Boolean = False, _WaveName As String = ""

		Public ReadOnly Property IsValid() As Boolean
			Get
				Return _WaveLoaded
			End Get
		End Property

		Public ReadOnly Property Name() As String
			Get
				Return _WaveName
			End Get
		End Property

		Private Sub New()
		End Sub

		Public Sub New(ByVal EmbeddedWaveName As String)
			_WaveName = EmbeddedWaveName
			_WaveLoaded = LoadEmbeddedWave()
		End Sub

		Private Function LoadEmbeddedWave() As Boolean
			Dim resourceStream As System.IO.Stream = _
				System.Reflection.Assembly.GetExecutingAssembly(). _
				GetManifestResourceStream(Me.GetType.Namespace & "." & _WaveName)
			If Not IsNothing(resourceStream) Then
				Try
					ReDim _WaveBytes(CInt(resourceStream.Length))
					resourceStream.Read(_WaveBytes, 0, CInt(resourceStream.Length))
					Return True
				Catch ex As Exception
					MessageBox.Show(ex.Message, "Load Embedded Wave Resource Failed", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
					Return False
				End Try
			Else
				MessageBox.Show(_WaveName, "Embedded Wave Resource Not Found", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
				Return False
			End If
		End Function

		Public Sub Play()
			If IsValid Then
				My.Computer.Audio.Play(_WaveBytes, AudioPlayMode.Background)
			End If
		End Sub

	End Class

#End Region

End Class

Open in new window

There is additional code in there for processing and display all the different messages, as well as more code to optionally flash the taskbar entry and/or play sounds when messages are received. You can test the application on a single system by simply connecting to your own computer name from the list. The application will happily open two chat windows and allow you to have a witty conversation with yourself:
Testing the chat application on a single computer.
I hope I've demystified how a simple chat application can be realized through the use of the TcpClient and TcpListener classes. These same basic concepts and techniques can be used to transfer other types of data such as images and files as well. Once you convert your information to byte arrays, and design a suitable communication protocol for them, the application can be more easily broken down into manageable pieces.

Happy chatting!

The full source code for this project can be downloaded here:
https://docs.google.com/file/d/0B0MXukqErympUGRFd2FjOGtRYzg/edit?usp=sharing
15
6 Comments
LVL 15

Expert Comment

by:Eric AKA Netminder
Idle_Mind,

Congratulations! Your article has been published.

ericpete
Page Editor
0
LVL 18

Expert Comment

by:John (Yiannis) Toutountzoglou
One more time a great ,simply and fully understanting article from Idle_Mind..

'The LabelStatus is actually something i was looking for (How it works?)'. Now it is clear
Thank you  for the Source Code

Excellent work ...!!


Finally ...Thank you once more Mike..........
0
LVL 86

Author Comment

by:Mike Tomlinson
If you need the full source code, click on the link I provided at the bottom of the article, then click on File --> Download.
0
OWASP Proactive Controls

Learn the most important control and control categories that every architect and developer should include in their projects.

LVL 1

Expert Comment

by:Kerr Heilee Almario
This is really great and useful, but there's one problem with the provided source code.
It says unavailable when trying to open the project on MVS 2k15

Excellent work on this program!
0

Expert Comment

by:aabrimpong
This is perfect...can u please tell me why it is detecting only two computers on my network?
0

Expert Comment

by:Mridul Anand
Hi Mike,

Thanks for posting the whole code along with logic well explained. I have a strange requirement, but I dare ask - I want to use this chatting feature be available from within Excel, i.e. whenever Excel is open, it should work as Listener and Server and be used for opening chat conversations with people on my network, provided they have the same program running in their active Excel application (possibly by putting this code inside PEROSNAL.XLSB. Possible?

Thanks a lot.
Mridul.
0

Featured Post

Exploring ASP.NET Core: Fundamentals

Learn to build web apps and services, IoT apps, and mobile backends by covering the fundamentals of ASP.NET Core and  exploring the core foundations for app libraries.

Wrapper-1-Query. Use an Excel function to calculate a column for an Access query. Part 1. Shows a query in Access that has a calculated column with the results of an Excel worksheet function. See how to call a wrapper function from a query, and …
Watch this simple and effective video tutorial to extract attachments from Outlook 2007 and try this easy method by yourself. No need to go anywhere, just watch the video and export attachments from Outlook in few simple steps. To know more, click h…

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month