I've seen many P2P chat systems around the web that use a client/server approach using TCP (Transmission Control Protocol). I'm going to take a pure peer-to-peer approach using UDP (User Datagram Protocol). In this system there is no server, each of the peers is equal to any other peer. Just a quick background on TCP and UDP. TCP and UDP are part of the TCP/IP protocol suite for networking. TCP is a connection based protocol where you need a connection between hosts. UDP is a connectionless protocol where a sender just sends out data and doesn't care if it is received or not.
To get started create a new Windows Forms application called PeerToPeerChat. Right click the Form1.cs that was generated and select Rename. Change the name of this form to ChatForm.vb. When it asks if you want to change the instances of the form in code say yes. I also added in a very simple form to log in a user. Right click the PeerToPeerChat project in the Solution Explorer and select Add|Windows Form. Name this new form LoginForm. Onto the form I dragged a Label, Text Box, and Button. My finished form in the designer looked like this.
The Text Box I named tbUserName and the Button btnOK. I set the MaximumLength property of tbUserName to 32 as you don't want very long user names and the Text property of btnOK to OK. Now, open up the code for LoginForm and change it to the following. Sorry for the C# accent here, I know I'm not using VB-ish names.
Public Class LoginForm Private user As String Public ReadOnly Property UserName() As String Get UserName = user End Get End Property Public Sub New() ' This call is required by the designer. InitializeComponent() AddHandler btnOK.Click, AddressOf btnOK_Click AddHandler Me.FormClosing, AddressOf LoginForm_Closing End Sub Private Sub btnOK_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) user = tbUserName.Text.Trim() If (String.IsNullOrEmpty(user)) Then MessageBox.Show("Please enter a user name up to 32 characters."); Exit Sub End If RemoveHandler Me.FormClosing, AddressOf LoginForm_Closing Me.Close() End Sub Private Sub LoginForm_Closing(ByVal sender As System.Object, ByVal e As System.EventArgs) user = "" End Sub End Class
The first thing I did was add a field, user and a public read only property, UserName to expose its value. This is one way of getting data from one form to another. It is safer than making a field public, in the sense that a public field can be assigned to outside of the form but a public read only property can't. In the constructor I wired handlers for the FormClosing event of the form and the Click event of btnOK.
In the handler for the Click event I set the user field to the Text property of tbUserName and remove the leading and trailing spaces using the Trim method. I then check to see if that value is null or empty. If it is I display a message box stating to enter a user name up to 32 characters and return out of the method. I then unsubscribe from the FormClosing event. I do that because if the user closes the form any way other than clicking the OK button I will set the userName field to the empty string. In the ChatForm if the userName is the empty string I will exit the application. I then finally close the form because we are done with it.
As I mentioned, in the FormClosing event handler I set the userName field to the empty string. This is a cancelable event. If you were to set the Cancel property of e to true the event would be canceled an the form wouldn't close.
Now let's turn our attention to the ChatForm. It is a rather basic form as well as this isn't a full featured chat program, just a rather basic one that sends messages between peers. It can be expanded to include many other feature that I won't be going into in this tutorial. The controls that I added are a Rich Text Box, a Text Box, and a Button. My finished form looked like the following in the designer.
I first made the form bigger. The Rich Text Box takes up most of the form. I set the following properties: (Name) to rtbChat, BackColor property of it to White, ReadOnly to True, TabIndex to 2, and TabStop to False. Under the Rich Text Box I added the Text Box and the Button. I lined things up to make it look fairly nice. I set the (Name) property of the Text Box to tbSend, the TabStop property to 0, and the MaximumLength property to 968. I chose the value for a specific reason. I will be broadcasting the sender's user name and their message so I wanted to limit the size of the message being sent to about 1024 bytes. The idea was to keep packets at a reasonable length. The Button I set the (Name) property to btnSend, TabStop property to 1, and the Text property to Send.
Now the interesting part, coding the actual chat peer. Open the code view for ChatForm. The first thing you are going to want to do is add in a few using statements. You will want them for System.Net, System.Net.Sockets, System.Threading, and System.Text namespaces to bring classes into scope.
Imports System.Net Imports System.Net.Sockets Imports System.Threading Imports System.Text
The first two are for network related things as you probably guessed. The third may not be so obvious. To receive messages you will want to use a separate thread or your GUI will become unresponsive. That is why that is there. The last is for encoding data from a string to an array of bytes and from an array of bytes to a string. Now you will want to add a few fields to the class. Add in these fields.
Delegate Sub AddMessage(ByRef message As String) Private userName As String Private Const port As Integer = 54545 Private Const broadcastAddress As String = "255.255.255.255" Private receivingClient As UdpClient Private sendingClient As UdpClient Private receivingThread As Thread
I declared a delegate that will be used to add another peer's message to rtbChat. This is because you can't modify the GUI from a separate thread. I'm not going totally into multithreading. I'd suggest reading up on it if it interests you. I'm just going to say that delegates are a way of calling a method without needing to know the name of that method. They are like function pointers in C/C++. Again, something that would be interesting to research.
You will want to keep track of the user's user name so there is a field for that, userName. There are then two constants, port and broadcastAddress. port is the port that we will be sending and receiving with. There are a total of 65,436 ports available. Ports 1 to 1024 are what are called well known ports and aren't allowed for use. I chose a rather strange number arbitrarily. I was tempted to choose 611, for obvious reasons, but that is part of the well known ports and can't be used. Instead I went with 54545, can you figure out why? The other constant, broadcastAddress, is an IPv4 TCP/IP address in the form of a string. This address is one of many special TCP/IP addresses. Data sent to this address is routed to all local hosts on a LAN. In this way a user's message will be sent to all peers on the network that are listening for messages. If they aren't listening the message will be ignored by them. This is where UDP differs from TCP. In TCP you must make a connection to a remote host. Data is then sent between the hosts using this connection. This address also isn't routed by a router externally so the data send won't be sent out over the Internet, it will stay on the local network.
The next two fields are UdpClient fields: receivingClient and sendingClient. The first will be used to receive messages and the second will be used to send them. The last field is receivingThread and is a Thread field. This field will be a separate thread that will run in the background listening for messages. When a message is received it will add the message to chat, including messages you send.
In the constructor of the form I wired a few event handlers. For the Load event of the form and the Click event of btnSend. Change constructor to the following.
Public Sub New() ' This call is required by the designer. InitializeComponent() AddHandler Me.Load, AddressOf ChatForm_Load AddHandler btnSend.Click, AddressOf btnSend_Click End Sub
From the Load event handler I called two methods that I wrote InitializeSender and InitializeReceiver. Add in the following code under the constructor.
Private Sub ChatForm_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Me.Hide() Using loginForm As New LoginForm loginForm.ShowDialog() If (String.IsNullOrEmpty(loginForm.UserName)) Then Me.Close() Else userName = loginForm.UserName Me.Show() End If tbSend.Focus() InitializeSender() InitializeReceiver() End Using End Sub Private Sub InitializeSender() sendingClient = New UdpClient(broadcastAddress, port) sendingClient.EnableBroadcast = True End Sub Private Sub InitializeReceiver() receivingClient = New UdpClient(port) Dim start As ThreadStart = New ThreadStart(AddressOf Receiver) receivingThread = New Thread(start) receivingThread.IsBackground = True receivingThread.Start() End Sub
In the event handler I first hide the form to display the login form. In a using statement I create a new LoginForm. What this does is dispose of the form right away when we are done with it. It's always nice to clean up after yourself when you can. I call the ShowDialog method on the form to display it in modal form, the user has to finish with the form before continuing. The next step is to check if there is a value in the UserName property of the form. If there isn't the user closed the login form and I close the chat form. Otherwise I set the userName field to the UserName property of the login form and show the chat form. I then set focus on the text box for entering messages and call the methods to initialize the UdpClient fields.
The InitializeSender method is rather simple. I create a UdpClient and tell it to send on the broadcast address, 255.255.255.255 and to the port defined earlier. I also set the EnableBroadcast property to true so that it will broadcast it's data to all machines on the local area network.
The InitializeReceiver method does a little more work. To receive you don't care who is sending the data, you just want to receive it. So I create the a UdpClient that listens in on the port defined earlier. The next step is to create and start a new thread. I create a ThreadStart object that is set to a method called Receiver that I haven't written yet. I set the receivingThread field to be a new thread passing in the ThreadStart I just created. I set the IsBackground property to true so the thread is executed in the background and then start the thread calling the Start method. I really recommend if you have problems with that to research threads more thoroughly.
The next thing I'm going to add is the logic for sending a message to the other clients. That is done in the Click handler of btnSend. Add in this handler.
Private Sub btnSend_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) tbSend.Text = tbSend.Text.TrimEnd() If (Not String.IsNullOrEmpty(tbSend.Text)) Then Dim toSend As String = userName + ":" + vbNewLine + tbSend.Text Dim data() As Byte = Encoding.ASCII.GetBytes(toSend) sendingClient.Send(data, data.Length) tbSend.Text = "" End If tbSend.Focus() End Sub
The first thing I do is remove any trailing spaces from the textbox, no point in sending them over the network. I then check to see if that is not null or empty. No need to send a blank line over the network either. I next create a string that holds the user's name and the trimmed message. To send data over the network I create an array of bytes that represents the string just created. Then I call the Send method of sendingClient to actually send the data over the network. I set the Text property of the text box back to the empty string and set it to be focused as well.
Next is the Receiver method and a method with the signature of the delegate created earlier to add incoming text. Add in the following two methods.
Private Sub Receiver() Dim endPoint As IPEndPoint = New IPEndPoint(IPAddress.Any, port) Dim messageDelegate As AddMessage = AddressOf MessageReceived While (True) Dim data() As Byte data = receivingClient.Receive(endPoint) Dim message As String = Encoding.ASCII.GetString(data) Invoke(messageDelegate, message) End While End Sub Private Sub MessageReceived(ByRef message As String) rtbChat.Text += message + vbNewLine End Sub
The first thing I do in the Receiver method is to create an IPEndPoint. This is used to define an IPAddress and Port to listen on. I use IPAddress.Any saying to listen to any incoming connection on the port. I then create an AddMessage delegate setting it to be the method MessageReceived. I would recommend researching delegates if this is hard to understand. There is then an infinite loop that will check for incoming data. This is why I had to use threads, otherwise your application would become unresponsive. The call to Receive is a blocking call. Nothing will happen until it receives data. It requires a ref parameter that is the IPEndpoint and returns an array of bytes. I then decode the array of bytes into a string. I then Invoke the delegate and pass in the message as a parameter to the delegate. The MessageReceived method just adds the message and a new line to rtbChat's Text property.
Well, that is a lot to digest and is a rather basic P2P chat program. There are many things that can be added to it but it is a good starting point.