Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1071345
  • 博文数量: 403
  • 博客积分: 10272
  • 博客等级: 上将
  • 技术积分: 4407
  • 用 户 组: 普通用户
  • 注册时间: 2012-02-24 14:22
文章分类

全部博文(403)

文章存档

2012年(403)

分类: 嵌入式

2012-03-26 17:30:14

该软件采用P2P方式,各个客户端之间直接发消息进行会话聊天,服务器在其中只扮演协调者的角色(混合型P2P)。

1.会话流程设计

当一个新用户通过自己的客户端登陆系统后,从服务器获取当前在线的用户信息列表,列表信息包括了系统中每个用户的地址。用户就可以开始独立工作,自主地向其他用户发送消息,而不经过服务器。每当有新用户加入或在线用户退出时,服务器都会及时发消息通知系统中的所有其他用户,以便它们实时地更新用户信息列表。

按照上述思路,设计系统会话流程如下:

(1)用户通过客户端进入系统,向服务器发出消息,请求登陆。

(2)服务器收到请求后,向客户端返回应答消息,表示同意接受该用户加入,并顺带将自己服务线程所在的监听端口号告诉用户。

(3)客户端按照服务器应答中给出的端口号与服务器建立稳定的连接。

(4)服务器通过该连接将当前在线用户的列表信息传给新加入的客户端。

(5)客户端获得了在线用户列表,就可以独立自主地与在线的其他用户通信了。

(6)当用户退出系统时要及时地通知服务器。

2.用户管理

系统中,无论是服务器还是客户端都保存一份在线用户列表,客户端的用户表在一开始登陆时从服务器索取获得。在程序运行的过程中,服务器负责实时地将系统内用户的变动情况及时地通知在线的每个成员用户。

新用户登录时,服务器将用户表传给他,同时向系统内每个成员广播“login”消息,各成员收到后更新自己的用户表。

同样,在有用户退出系统时,服务器也会及时地将这一消息传给各个用户,当然这也就要求每个用户在自己想要退出之前,必须要先告诉服务器。

3.协议设计

3.1 客户端与服务器会话

(1)登陆过程。

客户端用匿名UDP向服务器发送消息:

login,username,localIPEndPoint

消息内容包括3个字段,各字段之间用“,”分隔:“login”表示请求登陆;“username”为用户名;“localIPEndPoint”是客户端本地地址。

服务器收到后以匿名UDP返回如下消息:

Accept,port

其中,“Accept”表示服务器接受了请求;“port”是服务所在端口,服务线程在这个端口上监听可能的客户连接,该连接使用同步的TCP。

连上服务器,获取用户列表:

客户端从上一会话的“port”字段的值服务所在端口,于是向端口发起TCP连接,向服务器索取在线的用户列表,服务器接受连接后将用户列别传输给客户端。

用户列表格式如下:

username1,IPEndPoint1;username2,IPEndPoint2;.....;end

username1,username2.....为用户名,IPEndPoint1,IPEndPoint2....为它们对应的端点。每个用户的信息都有个“用户名+端点”组成,用户信息之间以“;”隔开,整个用户列表以“end”结尾。

3.1 服务器协调管理用户

(1)新用户加入通知。

由于系统中已存在的每个用户都有一份当前用户表,因此当有新成员加入时,服务器无需重复给系统中的每个成员再传送用户表,只要将新加入成员的信息告诉系统内的其他用户,再由他们各自更新自己的用户表就行了。

服务器向系统内用户广播发送如下消息:

端点字段写为“remoteIPEndPoint”,表示是远程某个用户终端登陆了,本地客户线程据此更新用户列表。其实,在这个过程中,服务器只是将受到的“login”消息简单地转发而已。

(2)用户退出。

与新成员加入时一样,服务器将用户退出的消息直接进行广播转发:

logout,username,remoteIPEndPoint

其中,“remoteIPEndPoint”为退出系统的远程用户终端的端点地址。

3.1 用户终端之间聊天

用户聊天时,他们各自的客户端之间是以P2P方式工作的,彼此地位对等,独立,不与服务器发生直接联系。

聊天时发送的信息格式为:

talk,longTime,selfUserName,message

“talk”表明这是聊天内容;“longTime”是长时间格式的当前系统时间;“selfUserName”为自己的用户名;“message”是聊天的内容。

4.系统实现

4.1 服务线程

系统运行后,先有服务器启动服务线程,只需单击“启动”按钮即可。

“启动”按钮的事件过程:

1 //点击开始事件处理函数
2 private void buttonStart_Click(object sender, EventArgs e)
3 {
4 //创建接收套接字
5 serverIp = IPAddress.Parse(textBoxServerIp.Text);
6 serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));
7 receiveUdpClient = new UdpClient(serverIPEndPoint);
8
9 //启动接收线程
10 Thread threadReceive = new Thread(ReceiveMessage);
11 threadReceive.Start();
12 buttonStart.Enabled = false;
13 buttonStop.Enabled = true;
14
15 //随机指定监听端口 N( P+1 ≤ N < 65536 )
16 Random random = new Random();
17 tcport = random.Next(port + 1, 65536);
18
19 //创建监听套接字
20 myTcpListener = new TcpListener(serverIp, tcport);
21 myTcpListener.Start();
22
23 //启动监听线程
24 Thread threadListen = new Thread(ListenClientConnect);
25 threadListen.Start();
26 AddItemToListBox(string.Format("服务线程({0})启动,监听端口{1}",serverIPEndPoint,tcport));
27 }

可以看到,服务器先后启动了两个线程:一个是接收线程threadReceive,它在一个实名UDP端口上,时刻准备着接收客户端发来的会话消息;另一个是监听线程threadListen,它在某个随机指定的端口上监听。

服务器接收线程关联的ReceiveMessage()方法:

View Code
1 //接收数据
2 private void ReceiveMessage()
3 {
4 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
5 while (true)
6 {
7 try
8 {
9 //关闭receiveUdpClient时此句会产生异常
10 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
11 string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
12
13 //显示消息内容
14 AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));
15
16 //处理消息数据
17 string[] splitString = message.Split(',');
18
19 //解析用户端地址
20 string[] splitSubString = splitString[2].Split(':'); //除去':'
21 IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitSubString[0]), int.Parse(splitSubString[1]));
22 switch (splitString[0])
23 {
24 //收到注册关键字"login"
25 case "login":
26 User user = new User(splitString[1], clientIPEndPoint);
27 userList.Add(user);
28 AddItemToListBox(string.Format("用户{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
29 string sendString = "Accept," + tcport.ToString();
30 SendtoClient(user, sendString); //向该用户发送同意关键字
31 AddItemToListBox(string.Format("向{0}({1})发出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
32 for (int i = 0; i < userList.Count; i++)
33 {
34 if (userList[i].GetName() != user.GetName())
35 {
36 //向除刚加入的所有用户发送更新消息
37 SendtoClient(userList[i], message);
38 }
39 }
40 AddItemToListBox(string.Format("广播:[{0}]", message));
41 break;
42
43 //收到关键字"logout"
44 case "logout":
45 for (int i = 0; i < userList.Count; i++)
46 {
47 if (userList[i].GetName() == splitString[1])
48 {
49 AddItemToListBox(string.Format("用户{0}({1})退出", userList[i].GetName(), userList[i].GetIPEndPoint()));
50 userList.RemoveAt(i);
51 }
52 }
53
54 //向所用用户发送更新消息
55 for (int i = 0; i < userList.Count; i++)
56 {
57 SendtoClient(userList[i], message);
58 }
59 AddItemToListBox(string.Format("广播:[{0}]", message));
60 break;
61 }
62 }
63 catch
64 {
65 break;
66 }
67 }
68 AddItemToListBox(string.Format("服务线程({0})终止", serverIPEndPoint));
69 }

接收线程执行该方法,进入while()循环,对每个收到的消息进行解析,根据消息头是“login”或“logout”转入相应的处理。

监听线程对应ListenClientConnect()方法:

1 //接受客户端连接
2 private void ListenClientConnect()
3 {
4 TcpClient newClient = null;
5 while (true)
6 {
7 try
8 {
9 //获得用于传递数据的TCP套接口
10 newClient = myTcpListener.AcceptTcpClient();
11 AddItemToListBox(string.Format("接受客户端{0}的 TCP 请求", newClient.Client.RemoteEndPoint));
12 }
13 catch
14 {
15 AddItemToListBox(string.Format("监听线程({0}:{1})终止", serverIp, tcport));
16 break;
17 }
18
19 //启动发送用户列表线程
20 Thread threadSend = new Thread(SendData);
21 threadSend.Start(newClient);
22 }
23 }

当客户端请求到达后,与之建立TCP连接,然后创建一个新的线程threadSend,他通过执行SendData()方法传送用户列表。

在服务器运行过程中,可随时通过点击“停止”按钮关闭服务线程。

”停止“按钮的事件过程:

1 //当点击关闭按钮的事件处理程序
2 private void buttonStop_Click(object sender, EventArgs e)
3 {
4 myTcpListener.Stop();
5 receiveUdpClient.Close();
6 buttonStart.Enabled = true;
7 buttonStop.Enabled = false;
8 }

这里myTcpListener是TCP监听套接字,而receiveUdpClient是UDP套接字。当执行Stop()方法关闭监听套接字时,myTcpListener.AcceptTcpClient()会产生异常。

运行服务器,先后单击”启动“和”停止“按钮,状态监控屏上就显示出服务线程的工作状态,如下图所示。

图1 服务线程的启动/停止状态

4.2 登陆/注销

(1) 用户对象

为了便于服务器对全体用户的管理,在服务器工程中添加自定义User类。代码如下:

1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 //添加的命名空间引用
7 using System.Net;
8
9 namespace Server
10 {
11 //用户信息类 蒲泓全(18/3/2012)
12 class User
13 {
14 private string userName; //用户名
15 private IPEndPoint userIPEndPoint; //用户地址
16 public User(string name, IPEndPoint ipEndPoint)
17 {
18 userName = name;
19 userIPEndPoint = ipEndPoint;
20 }
21 public string GetName()
22 {
23 return userName;
24 }
25 public IPEndPoint GetIPEndPoint()
26 {
27 return userIPEndPoint;
28 }
29 }
30 }

User类具有用户名和端点地址两个属性,这也正是用户列表中需要填写的信息项。
(2) 用户登录功能

当服务器的两个服务线程运行起来之后,各用户就可以通过客户端程序登录到系统了。用户在客户端上单击“登录”按钮后,客户端就向服务器发出“login”请求。

“登录”按钮的事件过程为:

View Code
private void buttonLogin_Click(object sender, EventArgs e)
{
//创建接收套接字
IPAddress clientIp = IPAddress.Parse(textBoxLocalIp.Text);
clientIPEndPoint = new IPEndPoint(clientIp, int.Parse(textBoxLocalPort.Text));
receiveUdpClient = new UdpClient(clientIPEndPoint);

//启动接收线程
Thread threadReceive = new Thread(ReceiveMessage);
threadReceive.Start();
AddItemToListBox(string.Format("客户线程({0})启动", clientIPEndPoint));

//匿名发送
sendUdpClient = new UdpClient(0);

//启动发送线程
Thread threadSend = new Thread(SendMessage);
threadSend.Start(string.Format("login,{0},{1}",textBoxUserName.Text,clientIPEndPoint));
AddItemToListBox(string.Format("发出:[login,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
buttonLogin.Enabled = false;
buttonLogout.Enabled = true;
this.Text = textBoxUserName.Text; //使当前窗体名字变为当前用户名
}

可以看到客户端在登录时也启动了两个线程,其中一个threadReceive是用实名UDP创建的接收线程,又称为客户线程,这是因为,它代表客户端程序处理与服务器的会话消息。另一个线程threadSend则是临时创建的,并以匿名UDP向服务器发出“login”消息。

登陆请求发出之后,客户线程就循环执行ReceiveMessage()方法,以随时接受和处理服务器的应答消息。

客户线程关联的ReceiveMessage()方法:

View Code
1 //接收数据
2 private void ReceiveMessage()
3 {
4 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
5 while (true)
6 {
7 try
8 {
9 //关闭receiveUdpClient时此句会产生异常
10 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
11 string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
12
13 //显示消息内容
14 AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));
15
16 //处理消息数据
17 string[] splitString = message.Split(','); //除去','
18 switch (splitString[0])
19 {
20 //若接收连接
21 case "Accept":
22 try
23 {
24 AddItemToListBox(string.Format("连接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));
25 myTcpClient = new TcpClient();
26 myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));
27 if (myTcpClient != null)
28 {
29 AddItemToListBox("连接成功!");
30 networkStream = myTcpClient.GetStream();
31 br = new BinaryReader(networkStream);
32 }
33 }
34 catch
35 {
36 AddItemToListBox("连接失败!");
37 }
38 Thread threadGetList = new Thread(GetUserList);
39 threadGetList.Start(); //请求获得用户列表信息线程启动
40 break;
41
42 //若收到注册关键字"login",代表有新的用户加入,并更新用户列表
43 case "login":
44 AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
45 string userItemInfo = splitString[1] + "," + splitString[2];
46 AddItemToListView(userItemInfo);
47 break;
48
49 //若收到注册关键字"logout",代表有用户退出,并更新用户列表
50 case "logout":
51 AddItemToListBox(string.Format("用户{0}({1})退出", splitString[1], splitString[2]));
52 RmvItemfromListView(splitString[1]);
53 break;
54
55 //若收到回话关键字"talk",则表明有用户发起回话,并开始准备回话
56 case "talk":
57 for (int i = 0; i < chatFormList.Count; i++)
58 {
59 if (chatFormList[i].Text == splitString[2])
60 {
61 chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]);
62 }
63 }
64 break;
65 }
66 }
67 catch
68 {
69 break;
70 }
71 }
72 AddItemToListBox(string.Format("客户线程({0})终止", clientIPEndPoint));
73 }

如下图所示:为第一个用户登陆系统时,从状态监控屏幕上看到的客户端与服务器程序的会话过程。

图2 登陆过程中双方的会话

(3) 用户注销

当用用户需要下线退出时,单击客户端界面上的“注销”按钮。

“注销”按钮的过程代码:

1 //当点击退出按钮的事件处理函数
2 private void buttonLogout_Click(object sender, EventArgs e)
3 {
4 //匿名发送
5 sendUdpClient = new UdpClient(0);
6
7 //启动发送线程
8 Thread threadSend = new Thread(SendMessage);
9 threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));
10 AddItemToListBox(string.Format("发出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
11 receiveUdpClient.Close();
12 listViewOnline.Items.Clear();
13 buttonLogin.Enabled = true;
14 buttonLogout.Enabled = false;
15 this.Text = "Client"; //恢复到原来的名字
16 }

注销操作很简单,凡要注销的用户只需向服务器发出“logou”消息,告知服务器就可以了,最好还要关闭客户端自身的UDP套接字。

服务器在收到“logout”消息后,执行下面代码:

View Code
for (int i = 0; i < userList.Count; i++)
{
if (userList[i].GetName() == splitString[1])
{
AddItemToListBox(string.Format("用户{0}({1})退出", userList[i].GetName(), userList[i].GetIPEndPoint()));
userList.RemoveAt(i);
}
}

//向所用用户发送更新消息
for (int i = 0; i < userList.Count; i++)
{
SendtoClient(userList[i], message);
}
AddItemToListBox(string.Format("广播:[{0}]", message));

服务程序在自己维护的User对象列表中删除这个用户,并且将这个消息广播给所有的用户。

在这个过程中,退出的客户端与服务器的会话记录如下图所示:

图3 注销时双方的会话

(3) 更新用户列表

系统内的在线用户收到服务器发来的消息后,实时地更新自己的用户列表。当服务器发来“login”消息时,说明有新成员加入,客户端执行下面的代码:

1 AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
2 string userItemInfo = splitString[1] + "," + splitString[2];
3 AddItemToListView(userItemInfo);
4 break;

若收到的是“logout”,则执行下面的代码:

1 AddItemToListBox(string.Format("用户{0}({1})退出", splitString[1], splitString[2]));
2 RmvItemfromListView(splitString[1]);
3 break;

为了是程序简单,客户端并没有使用特定的数据结构存储用户列表,而是直接将列表用ListView空间显示在界面上,并用委托机制定义了两个回调函数AddItemToListView()和RmvItemfromListView(),向空间中添加/删除用户信息。

4.3 及时聊天

带有聊天谈话内容的消息以“talk”为首部,采用点对点(P2P)方式发给对方。“talk”消息的发送,接收和显示都由专门的聊天子窗口负责,当客户端主程序收到“talk”的消息时,执行下面的代码:

1 for (int i = 0; i < chatFormList.Count; i++)
2 {
3 if (chatFormList[i].Text == splitString[2])
4 {
5 chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]);
6 }
7 }
8 break;

系统中的每个用户都对应一个聊天子窗体对象,上段代码的作用就是将一个“talk”消息定位到它的接受者的子窗体对象,再由该对象调用自身的ShwoTalkInfo()方法显示聊天内容。

要打开对应某个用户的子窗口,只需双击在线用户列表中的该用户项即可,代码如下:

View Code
1 //当点击两次发起回话的事件处理函数
2 private void listViewOnline_DoubleClick(object sender, EventArgs e)
3 {
4 string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;
5 if (peerName == textBoxUserName.Text)
6 {
7 return;
8 }
9 string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;
10 string[] splitString = ipendp.Split(':'); //除去':'
11 IPAddress peerIp = IPAddress.Parse(splitString[0]);
12 IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));
13 ChatForm dlgChatForm = new ChatForm();
14 dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);
15 dlgChatForm.Text = peerName;
16 chatFormList.Add(dlgChatForm);
17 dlgChatForm.Show();
18 }

其中,chatFormList是客户端程序定义的数据结构,用于保存每一个在线用户的子窗口列表,它与服务器端的userList结构是相对应的。每当用户双击了列表中的某个用户项时,程序就用该项的信息创建一个新的子窗体对象并添加到chatFormList表中。子窗体的初始化使用其自身的SetUserInfo()方法。

哈哈哈哈,现在整个通信聊天软件就完成了,我们看看下面运行的效果。

5. 运行效果

同时运行一个服务器(Server)程序和三个客户端(Client)程序,启动服务线程,在三个客户端分别以用户名“泓全”,“爱田”,“爱盼”登陆服务器。如下图所示:

图4 登陆服务器

此时,每个在线用户两两之间就都可以即时聊天了。不过,在聊天之前,聊天双方要先打开对方的子窗口,操作方法很简单,只要在客户端界面上双击“在线”列表中相应的用户项即可。如下图所示:

图5 在线交谈

6. 源代码

6.1 服务器端

View Code
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Text;
8 using System.Windows.Forms;
9
10 //添加的命名空间引用
11 using System.Net;
12 using System.Net.Sockets;
13 using System.Threading;
14 using System.IO;
15
16 namespace Server
17 {
18 public partial class MainForm : Form
19 {
20 private List userList = new List(); //保存登录的所有用户
21 int port; //服务端口
22 int tcport; //监听端口
23 private UdpClient sendUdpClient; //匿名发送套接口
24 private UdpClient receiveUdpClient; //实名接收套接口
25 private IPEndPoint serverIPEndPoint; //服务器地址
26 private TcpListener myTcpListener; //服务器监听套接口
27 private IPAddress serverIp; //服务器IP
28 private NetworkStream networkStream; //网络流
29 private BinaryWriter bw; //避免出现网络边界问题的写入流
30 string userListString; //用户列表串
31 public MainForm()
32 {
33 InitializeComponent();
34
35 //服务器IP
36 IPAddress[] ServerIP = Dns.GetHostAddresses("");
37 IPAddress address = IPAddress.Any;
38 for (int i = 0; i < ServerIP.Length; i++ )
39 {
40 if (ServerIP[i].AddressFamily == AddressFamily.InterNetwork)
41 {
42 address = ServerIP[i];
43 break;
44 }
45 }
46 textBoxServerIp.Text = address.ToString();
47
48 //随机选择服务端口 Port( Port > 1024 )
49 port = new Random().Next(1024, 65535);
50 textBoxServerPort.Text = port.ToString();
51 buttonStop.Enabled = false;
52 }
53
54 //点击开始事件处理函数
55 private void buttonStart_Click(object sender, EventArgs e)
56 {
57 //创建接收套接字
58 serverIp = IPAddress.Parse(textBoxServerIp.Text);
59 serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));
60 receiveUdpClient = new UdpClient(serverIPEndPoint);
61
62 //启动接收线程
63 Thread threadReceive = new Thread(ReceiveMessage);
64 threadReceive.Start();
65 buttonStart.Enabled = false;
66 buttonStop.Enabled = true;
67
68 //随机指定监听端口 N( P+1 ≤ N < 65536 )
69 Random random = new Random();
70 tcport = random.Next(port + 1, 65536);
71
72 //创建监听套接字
73 myTcpListener = new TcpListener(serverIp, tcport);
74 myTcpListener.Start();
75
76 //启动监听线程
77 Thread threadListen = new Thread(ListenClientConnect);
78 threadListen.Start();
79 AddItemToListBox(string.Format("服务线程({0})启动,监听端口{1}",serverIPEndPoint,tcport));
80 }
81
82
83 //接受客户端连接
84 private void ListenClientConnect()
85 {
86 TcpClient newClient = null;
87 while (true)
88 {
89 try
90 {
91 //获得用于传递数据的TCP套接口
92 newClient = myTcpListener.AcceptTcpClient();
93 AddItemToListBox(string.Format("接受客户端{0}的 TCP 请求", newClient.Client.RemoteEndPoint));
94 }
95 catch
96 {
97 AddItemToListBox(string.Format("监听线程({0}:{1})终止", serverIp, tcport));
98 break;
99 }
100
101 //启动发送用户列表线程
102 Thread threadSend = new Thread(SendData);
103 threadSend.Start(newClient);
104 }
105 }
106
107 //向客户端发送在线用户列表信息
108 private void SendData(object userClient)
109 {
110 TcpClient newUserClient = (TcpClient)userClient;
111 userListString = null;
112 for (int i = 0; i < userList.Count; i++)
113 {
114 userListString += userList[i].GetName() + "," + userList[i].GetIPEndPoint().ToString() + ";";
115 }
116 userListString += "end";
117 networkStream = newUserClient.GetStream();
118 bw = new BinaryWriter(networkStream);
119 bw.Write(userListString);
120 bw.Flush(); //不保留现在写入的数据
121 AddItemToListBox(string.Format("向{0}传送:[{1}]", newUserClient.Client.RemoteEndPoint, userListString));
122 bw.Close();
123 newUserClient.Close();
124 }
125
126 //接收数据
127 private void ReceiveMessage()
128 {
129 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
130 while (true)
131 {
132 try
133 {
134 //关闭receiveUdpClient时此句会产生异常
135 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
136 string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
137
138 //显示消息内容
139 AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));
140
141 //处理消息数据
142 string[] splitString = message.Split(',');
143
144 //解析用户端地址
145 string[] splitSubString = splitString[2].Split(':'); //除去':'
146 IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitSubString[0]), int.Parse(splitSubString[1]));
147 switch (splitString[0])
148 {
149 //收到注册关键字"login"
150 case "login":
151 User user = new User(splitString[1], clientIPEndPoint);
152 userList.Add(user);
153 AddItemToListBox(string.Format("用户{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
154 string sendString = "Accept," + tcport.ToString();
155 SendtoClient(user, sendString); //向该用户发送同意关键字
156 AddItemToListBox(string.Format("向{0}({1})发出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
157 for (int i = 0; i < userList.Count; i++)
158 {
159 if (userList[i].GetName() != user.GetName())
160 {
161 //向除刚加入的所有用户发送更新消息
162 SendtoClient(userList[i], message);
163 }
164 }
165 AddItemToListBox(string.Format("广播:[{0}]", message));
166 break;
167
168 //收到关键字"logout"
169 case "logout":
170 for (int i = 0; i < userList.Count; i++)
171 {
172 if (userList[i].GetName() == splitString[1])
173 {
174 AddItemToListBox(string.Format("用户{0}({1})退出", userList[i].GetName(), userList[i].GetIPEndPoint()));
175 userList.RemoveAt(i);
176 }
177 }
178
179 //向所用用户发送更新消息
180 for (int i = 0; i < userList.Count; i++)
181 {
182 SendtoClient(userList[i], message);
183 }
184 AddItemToListBox(string.Format("广播:[{0}]", message));
185 break;
186 }
187 }
188 catch
189 {
190 break;
191 }
192 }
193 AddItemToListBox(string.Format("服务线程({0})终止", serverIPEndPoint));
194 }
195
196 private void SendtoClient(User user, string message)
197 {
198 //匿名发送
199 sendUdpClient = new UdpClient(0);
200 byte[] sendbytes = Encoding.Unicode.GetBytes(message);
201 IPEndPoint remoteIPEndPoint = user.GetIPEndPoint();
202 sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);
203 sendUdpClient.Close();
204 }
205
206 //当点击关闭按钮的事件处理程序
207 private void buttonStop_Click(object sender, EventArgs e)
208 {
209 myTcpListener.Stop();
210 receiveUdpClient.Close();
211 buttonStart.Enabled = true;
212 buttonStop.Enabled = false;
213 }
214
215 //用委托机制解决显示问题
216 private delegate void AddItemToListBoxDelegate(string str);
217 private void AddItemToListBox(string str)
218 {
219 if (listBoxStatus.InvokeRequired)
220 {
221 AddItemToListBoxDelegate d = AddItemToListBox;
222 listBoxStatus.Invoke(d, str);
223 }
224 else
225 {
226 listBoxStatus.Items.Add(str);
227 listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;
228 listBoxStatus.ClearSelected();
229 }
230 }
231 }
232 }

6.2 客户端

View Code
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Text;
8 using System.Windows.Forms;
9
10 //添加的命名空间引用
11 using System.Net;
12 using System.Net.Sockets;
13 using System.Threading;
14 using System.IO;
15
16 namespace Client
17 {
18 public partial class MainForm : Form
19 {
20 int port; //端口号
21 private UdpClient sendUdpClient; //匿名发送套接口
22 private UdpClient receiveUdpClient; //实名接收套接口
23 private IPEndPoint clientIPEndPoint; //客户端地址
24 private TcpClient myTcpClient; //TCP套接字
25 private NetworkStream networkStream; //网络流
26 private BinaryReader br; //避免网络边界问题的读数据流
27 string userListString; //用户名字串
28 private List chatFormList = new List(); //用户窗体列表
29 public MainForm()
30 {
31 InitializeComponent();
32
33 //本地IP和端口号的初始化
34 IPAddress[] LocalIP = Dns.GetHostAddresses("");
35 IPAddress address = IPAddress.Any;
36 for (int i = 0; i < LocalIP.Length; i++)
37 {
38 if (LocalIP[i].AddressFamily == AddressFamily.InterNetwork)
39 {
40 address = LocalIP[i];
41 break;
42 }
43 }
44 textBoxServerIp.Text = address.ToString();
45 textBoxLocalIp.Text = address.ToString();
46
47 //获得随机端口号
48 port = new Random().Next(1024, 65535);
49 textBoxLocalPort.Text = port.ToString();
50
51 //随机生成用户名
52 Random r = new Random((int)DateTime.Now.Ticks); //类似于与C++中的种子
53 textBoxUserName.Text = "user" + r.Next(100, 999);
54 buttonLogout.Enabled = false;
55 }
56
57 private void buttonLogin_Click(object sender, EventArgs e)
58 {
59 //创建接收套接字
60 IPAddress clientIp = IPAddress.Parse(textBoxLocalIp.Text);
61 clientIPEndPoint = new IPEndPoint(clientIp, int.Parse(textBoxLocalPort.Text));
62 receiveUdpClient = new UdpClient(clientIPEndPoint);
63
64 //启动接收线程
65 Thread threadReceive = new Thread(ReceiveMessage);
66 threadReceive.Start();
67 AddItemToListBox(string.Format("客户线程({0})启动", clientIPEndPoint));
68
69 //匿名发送
70 sendUdpClient = new UdpClient(0);
71
72 //启动发送线程
73 Thread threadSend = new Thread(SendMessage);
74 threadSend.Start(string.Format("login,{0},{1}",textBoxUserName.Text,clientIPEndPoint));
75 AddItemToListBox(string.Format("发出:[login,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
76 buttonLogin.Enabled = false;
77 buttonLogout.Enabled = true;
78 this.Text = textBoxUserName.Text; //使当前窗体名字变为当前用户名
79 }
80
81 //发送数据
82 private void SendMessage(object obj)
83 {
84 string message = (string)obj;
85 byte[] sendbytes = Encoding.Unicode.GetBytes(message);
86
87 //服务器端的IP和端口号
88 IPAddress remoteIp = IPAddress.Parse(textBoxServerIp.Text);
89 IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(textBoxServerPort.Text));
90 sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint); //匿名发送
91 sendUdpClient.Close();
92 }
93
94 //接收数据
95 private void ReceiveMessage()
96 {
97 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
98 while (true)
99 {
100 try
101 {
102 //关闭receiveUdpClient时此句会产生异常
103 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
104 string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
105
106 //显示消息内容
107 AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));
108
109 //处理消息数据
110 string[] splitString = message.Split(','); //除去','
111 switch (splitString[0])
112 {
113 //若接收连接
114 case "Accept":
115 try
116 {
117 AddItemToListBox(string.Format("连接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));
118 myTcpClient = new TcpClient();
119 myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));
120 if (myTcpClient != null)
121 {
122 AddItemToListBox("连接成功!");
123 networkStream = myTcpClient.GetStream();
124 br = new BinaryReader(networkStream);
125 }
126 }
127 catch
128 {
129 AddItemToListBox("连接失败!");
130 }
131 Thread threadGetList = new Thread(GetUserList);
132 threadGetList.Start(); //请求获得用户列表信息线程启动
133 break;
134
135 //若收到注册关键字"login",代表有新的用户加入,并更新用户列表
136 case "login":
137 AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
138 string userItemInfo = splitString[1] + "," + splitString[2];
139 AddItemToListView(userItemInfo);
140 break;
141
142 //若收到注册关键字"logout",代表有用户退出,并更新用户列表
143 case "logout":
144 AddItemToListBox(string.Format("用户{0}({1})退出", splitString[1], splitString[2]));
145 RmvItemfromListView(splitString[1]);
146 break;
147
148 //若收到回话关键字"talk",则表明有用户发起回话,并开始准备回话
149 case "talk":
150 for (int i = 0; i < chatFormList.Count; i++)
151 {
152 if (chatFormList[i].Text == splitString[2])
153 {
154 chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]);
155 }
156 }
157 break;
158 }
159 }
160 catch
161 {
162 break;
163 }
164 }
165 AddItemToListBox(string.Format("客户线程({0})终止", clientIPEndPoint));
166 }
167
168 //获得用户列表
169 private void GetUserList()
170 {
171 while (true)
172 {
173 userListString = null;
174 try
175 {
176 userListString = br.ReadString();
177 if (userListString.EndsWith("end"))
178 {
179 AddItemToListBox(string.Format("收到:[{0}]", userListString));
180 string[] splitString = userListString.Split(';');
181 for (int i = 0; i < splitString.Length - 1; i++)
182 {
183 AddItemToListView(splitString[i]);
184 }
185 br.Close();
186 myTcpClient.Close();
187 break;
188 }
189 }
190 catch
191 {
192 break;
193 }
194 }
195 }
196
197 //当点击退出按钮的事件处理函数
198 private void buttonLogout_Click(object sender, EventArgs e)
199 {
200 //匿名发送
201 sendUdpClient = new UdpClient(0);
202
203 //启动发送线程
204 Thread threadSend = new Thread(SendMessage);
205 threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));
206 AddItemToListBox(string.Format("发出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
207 receiveUdpClient.Close();
208 listViewOnline.Items.Clear();
209 buttonLogin.Enabled = true;
210 buttonLogout.Enabled = false;
211 this.Text = "Client"; //恢复到原来的名字
212 }
213
214 //当点击两次发起回话的事件处理函数
215 private void listViewOnline_DoubleClick(object sender, EventArgs e)
216 {
217 string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;
218 if (peerName == textBoxUserName.Text)
219 {
220 return;
221 }
222 string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;
223 string[] splitString = ipendp.Split(':'); //除去':'
224 IPAddress peerIp = IPAddress.Parse(splitString[0]);
225 IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));
226 ChatForm dlgChatForm = new ChatForm();
227 dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);
228 dlgChatForm.Text = peerName;
229 chatFormList.Add(dlgChatForm);
230 dlgChatForm.Show();
231 }
232
233 //利用委托机制显示信息
234 private delegate void AddItemToListBoxDelegate(string str);
235 private void AddItemToListBox(string str)
236 {
237 if (listBoxStatus.InvokeRequired)
238 {
239 AddItemToListBoxDelegate d = AddItemToListBox;
240 listBoxStatus.Invoke(d, str);
241 }
242 else
243 {
244 listBoxStatus.Items.Add(str);
245 listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;
246 listBoxStatus.ClearSelected();
247 }
248 }
249
250 private delegate void AddItemToListViewDelegate(string str);
251 private void AddItemToListView(string str)
252 {
253 if (listViewOnline.InvokeRequired)
254 {
255 AddItemToListViewDelegate d = AddItemToListView;
256 listViewOnline.Invoke(d, str);
257 }
258 else
259 {
260 string[] splitString = str.Split(',');
261 ListViewItem item = new ListViewItem();
262 item.SubItems.Add(splitString[0]);
263 item.SubItems.Add(splitString[1]);
264 listViewOnline.Items.Add(item);
265 }
266 }
267
268 private delegate void RmvItemfromListViewDelegate(string str);
269 private void RmvItemfromListView(string str)
270 {
271 if (listViewOnline.InvokeRequired)
272 {
273 RmvItemfromListViewDelegate d = RmvItemfromListView;
274 listViewOnline.Invoke(d, str);
275 }
276 else
277 {
278 for (int i = 0; i < listViewOnline.Items.Count; i++)
279 {
280 if (listViewOnline.Items[i].SubItems[1].Text == str)
281 {
282 listViewOnline.Items[i].Remove();
283 }
284 }
285 }
286 }
287 }
288 }

6.3 聊天子窗口的代码

View Code
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Text;
8 using System.Windows.Forms;
9
10 //添加的命名空间引用
11 using System.Net;
12 using System.Net.Sockets;
13 using System.Threading;
14
15 namespace Client
16 {
17 public partial class ChatForm : Form
18 {
19 private string selfUserName; //自己的用户名
20 private string peerUserName; //对方的用户名
21 private IPEndPoint peerUserIPEndPoint; //对方的地址
22 private UdpClient sendUdpClient; //匿名发送套接口
23 public ChatForm()
24 {
25 InitializeComponent();
26 }
27
28 //类似于构造函数
29 public void SetUserInfo(string selfName,string peerName,IPEndPoint peerIPEndPoint)
30 {
31 selfUserName = selfName;
32 peerUserName = peerName;
33 peerUserIPEndPoint = peerIPEndPoint;
34 }
35
36 //点击发送按钮的事件处理程序
37 private void buttonSend_Click(object sender, EventArgs e)
38 {
39 //匿名发送
40 sendUdpClient = new UdpClient(0);
41
42 //启动发送线程
43 Thread threadSend = new Thread(SendMessage);
44 threadSend.Start(string.Format("talk,{0},{1},{2}", DateTime.Now.ToLongTimeString(), selfUserName, textBoxSend.Text));
45 richTextBoxTalkInfo.AppendText(selfUserName + "" + DateTime.Now.ToLongTimeString() + Environment.NewLine + textBoxSend.Text);
46 richTextBoxTalkInfo.AppendText(Environment.NewLine);
47 richTextBoxTalkInfo.ScrollToCaret();
48 textBoxSend.Text = "";
49 textBoxSend.Focus();
50 }
51
52 //数据发送函数
53 private void SendMessage(object obj)
54 {
55 string message = (string)obj;
56 byte[] sendbytes = Encoding.Unicode.GetBytes(message);
57 sendUdpClient.Send(sendbytes, sendbytes.Length, peerUserIPEndPoint);
58 sendUdpClient.Close();
59 }
60
61 //显示通话内容
62 public void ShowTalkInfo(string peerName, string time, string content)
63 {
64 richTextBoxTalkInfo.AppendText(peerName + "" + time + Environment.NewLine + content);
65 richTextBoxTalkInfo.AppendText(Environment.NewLine);
66 richTextBoxTalkInfo.ScrollToCaret();
67 }
68
69 //当点击关闭时的事件处理程序
70 private void buttonClose_Click(object sender, EventArgs e)
71 {
72 this.Close();
73 }
74 }
75 }




阅读(1373) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~