聊天 网络

Today we are going to work on a peer-to-peer, P2P, chat system. The approach that I'm taking requires that you have two computers on a local area network, or you could have one computer and use virtual machines. Basically, you can only run one peer on a single computer.

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 theForm1.cs that was generated and select Rename. Change the name of this form toChatForm.cs. 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 LabelText Box, and Button. My finished form in the designer looked like this. 

Attached Image

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 btnOKto OK. Now, open up the code for LoginForm and change it to the following.

01 using System;
02 using System.Collections.Generic;
03 using System.ComponentModel;
04 using System.Data;
05 using System.Drawing;
06 using System.Linq;
07 using System.Text;
08 using System.Windows.Forms;
09  
10 namespace PeerToPeerChat
11 {
12     public partial class LoginForm : Form
13     {
14         string userName = "";
15  
16         public string UserName
17         {
18             get return userName; }
19         }
20  
21         public LoginForm()
22         {
23             InitializeComponent();
24  
25             this.FormClosing += newFormClosingEventHandler(LoginForm_FormClosing);
26             btnOK.Click += new EventHandler(btnOK_Click);
27         }
28  
29         void btnOK_Click(object sender, EventArgs e)
30         {
31             userName = tbUserName.Text.Trim();
32  
33             if (string.IsNullOrEmpty(userName))
34             {
35                 MessageBox.Show("Please select a user name up to 32 characters.");
36                 return;
37             }
38  
39             this.FormClosing -= LoginForm_FormClosing;
40             this.Close();
41         }
42  
43         void LoginForm_FormClosing(object sender, FormClosingEventArgs e)
44         {
45             userName = "";
46         }
47     }
48 }



The first thing I did was add a field, userName 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 userName field to the Text property of tbUserNameand 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 theuserName 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.

Resized to 58% (was 866 x 526) - Click image to enlargeAttached Image



I first made the form much bigger. The Rich Text Box takes up most of the form. I set the following properties: (Name) to rtbChatBackColor property of it to WhiteReadOnly to True,TabIndex to 2, and TabStop to False. Under the Rich Text Box I added the Text Box and theButton. I lined things up to make it look fairly nice. I set the (Name) property of the Text Box totbSend, 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 btnSendTabStopproperty 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 forSystem.NetSystem.Net.Sockets, and System.Threading namespaces to bring classes into scope.

1 using System.Net;
2 using System.Net.Sockets;
3 using System.Threading;



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. Now you will want to add a few fields to the class. Add in these fields.

01 delegate void AddMessage(string message);
02  
03 string userName;
04  
05 const int port = 54545;
06 const string broadcastAddress = "255.255.255.255";
07  
08 UdpClient receivingClient;
09 UdpClient sendingClient;
10  
11 Thread receivingThread;



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 broadcastAddressport 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 receivingThreadand 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.

1 public ChatForm()
2 {
3     InitializeComponent();
4  
5     this.Load += new EventHandler(ChatForm_Load);
6     btnSend.Click += new EventHandler(btnSend_Click);
7 }



From the Load event handler I called two methods that I wrote InitializeSender andInitializeReceiver. Add in the following code under the constructor.

01 void ChatForm_Load(object sender, EventArgs e)
02 {
03     this.Hide();
04  
05     using (LoginForm loginForm = new LoginForm())
06     {
07         loginForm.ShowDialog();
08  
09         if (loginForm.UserName == "")
10             this.Close();
11         else
12         {
13             userName = loginForm.UserName;
14             this.Show();
15         }
16     }
17  
18     tbSend.Focus();
19  
20     InitializeSender();
21     InitializeReceiver();
22 }
23  
24 private void InitializeSender()
25 {
26     sendingClient = new UdpClient(broadcastAddress, port);
27     sendingClient.EnableBroadcast = true;
28 }
29  
30 private void InitializeReceiver()
31 {
32     receivingClient = new UdpClient(port);
33  
34     ThreadStart start = new ThreadStart(Receiver);
35     receivingThread = new Thread(start);
36     receivingThread.IsBackground = true;
37     receivingThread.Start();
38 }



In the event handler I first hide the form to display the login in 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 UserNameproperty 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 theEnableBroadcast 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.

01 void btnSend_Click(object sender, EventArgs e)
02 {
03     tbSend.Text = tbSend.Text.TrimEnd();
04  
05     if (!string.IsNullOrEmpty(tbSend.Text))
06     {
07         string toSend = userName + ":\n" + tbSend.Text;
08         byte[] data = Encoding.ASCII.GetBytes(toSend);
09         sendingClient.Send(data, data.Length);
10         tbSend.Text = "";
11     }
12  
13     tbSend.Focus();
14 }



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.

01 private void Receiver()
02 {
03     IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, port);
04     AddMessage messageDelegate = MessageReceived;
05  
06     while (true)
07     {
Today we are going to work on a peer-to-peer, P2P, chat system. The approach that I'm taking requires that you have two computers on a local area network, or you could have one computer and use virtual machines. Basically, you can only run one peer on a single computer.

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 theForm1.cs that was generated and select Rename. Change the name of this form toChatForm.cs. 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 LabelText Box, and Button. My finished form in the designer looked like this. 

Attached Image

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 btnOKto OK. Now, open up the code for LoginForm and change it to the following.

01 using System;
02 using System.Collections.Generic;
03 using System.ComponentModel;
04 using System.Data;
05 using System.Drawing;
06 using System.Linq;
07 using System.Text;
08 using System.Windows.Forms;
09  
10 namespace PeerToPeerChat
11 {
12     public partial class LoginForm : Form
13     {
14         string userName = "";
15  
16         public string UserName
17         {
18             get return userName; }
19         }
20  
21         public LoginForm()
22         {
23             InitializeComponent();
24  
25             this.FormClosing += newFormClosingEventHandler(LoginForm_FormClosing);
26             btnOK.Click += new EventHandler(btnOK_Click);
27         }
28  
29         void btnOK_Click(object sender, EventArgs e)
30         {
31             userName = tbUserName.Text.Trim();
32  
33             if (string.IsNullOrEmpty(userName))
34             {
35                 MessageBox.Show("Please select a user name up to 32 characters.");
36                 return;
37             }
38  
39             this.FormClosing -= LoginForm_FormClosing;
40             this.Close();
41         }
42  
43         void LoginForm_FormClosing(object sender, FormClosingEventArgs e)
44         {
45             userName = "";
46         }
47     }
48 }


The first thing I did was add a field, userName 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 userName field to the Text property of tbUserNameand 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 theuserName 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.

Resized to 58% (was 866 x 526) - Click image to enlargeAttached Image


I first made the form much bigger. The Rich Text Box takes up most of the form. I set the following properties: (Name) to rtbChatBackColor property of it to WhiteReadOnly to True,TabIndex to 2, and TabStop to False. Under the Rich Text Box I added the Text Box and theButton. I lined things up to make it look fairly nice. I set the (Name) property of the Text Box totbSend, 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 btnSendTabStopproperty 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 forSystem.NetSystem.Net.Sockets, and System.Threading namespaces to bring classes into scope.

1 using System.Net;
2 using System.Net.Sockets;
3 using System.Threading;


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. Now you will want to add a few fields to the class. Add in these fields.

01 delegate void AddMessage(string message);
02  
03 string userName;
04  
05 const int port = 54545;
06 const string broadcastAddress = "255.255.255.255";
07  
08 UdpClient receivingClient;
09 UdpClient sendingClient;
10  
11 Thread receivingThread;


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 broadcastAddressport 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 receivingThreadand 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.

1 public ChatForm()
2 {
3     InitializeComponent();
4  
5     this.Load += new EventHandler(ChatForm_Load);
6     btnSend.Click += new EventHandler(btnSend_Click);
7 }


From the Load event handler I called two methods that I wrote InitializeSender andInitializeReceiver. Add in the following code under the constructor.

01 void ChatForm_Load(object sender, EventArgs e)
02 {
03     this.Hide();
04  
05     using (LoginForm loginForm = new LoginForm())
06     {
07         loginForm.ShowDialog();
08  
09         if (loginForm.UserName == "")
10             this.Close();
11         else
12         {
13             userName = loginForm.UserName;
14             this.Show();
15         }
16     }
17  
18     tbSend.Focus();
19  
20     InitializeSender();
21     InitializeReceiver();
22 }
23  
24 private void InitializeSender()
25 {
26     sendingClient = new UdpClient(broadcastAddress, port);
27     sendingClient.EnableBroadcast = true;
28 }
29  
30 private void InitializeReceiver()
31 {
32     receivingClient = new UdpClient(port);
33  
34     ThreadStart start = new ThreadStart(Receiver);
35     receivingThread = new Thread(start);
36     receivingThread.IsBackground = true;
37     receivingThread.Start();
38 }


In the event handler I first hide the form to display the login in 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 UserNameproperty 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 theEnableBroadcast 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.

01 void btnSend_Click(object sender, EventArgs e)
02 {
03     tbSend.Text = tbSend.Text.TrimEnd();
04  
05     if (!string.IsNullOrEmpty(tbSend.Text))
06     {
07         string toSend = userName + ":\n" + tbSend.Text;
08         byte[] data = Encoding.ASCII.GetBytes(toSend);
09         sendingClient.Send(data, data.Length);
10         tbSend.Text = "";
11     }
12  
13     tbSend.Focus();
14 }


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.

01 private void Receiver()
02 {
03     IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, port);
04     AddMessage messageDelegate = MessageReceived;
05  
06     while (true)
07     {
08         byte[] data = receivingClient.Receive(ref endPoint);
09         string message = Encoding.ASCII.GetString(data);
10         Invoke(messageDelegate, message);
11     }
12 }
13  
14 private void MessageReceived(string message)
15 {
16     rtbChat.Text += message + "\n";
17 }


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. 
08         byte[] data = receivingClient.Receive(ref endPoint);
09         string message = Encoding.ASCII.GetString(data);
10         Invoke(messageDelegate, message);
11     }
12 }
13  
14 private void MessageReceived(string message)
15 {
16     rtbChat.Text += message + "\n";
17 }



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. 

posted @ 2015-08-20 09:20  applekingghfhfhbr  阅读(175)  评论(0编辑  收藏  举报