`
wbj0110
  • 浏览: 1548817 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

即时通信聊天工具的原理与设计

阅读更多

 该软件采用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()方法:

复制代码
 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”请求。

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

复制代码
  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()方法:

复制代码
 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”消息后,执行下面代码:

复制代码
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()方法显示聊天内容。

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

复制代码
 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 服务器端

复制代码
  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<User> userList = new List<User>(); //保存登录的所有用户
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 客户端

复制代码
  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<ChatForm> chatFormList = new List<ChatForm>(); //用户窗体列表
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 聊天子窗口的代码

复制代码
 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 }
复制代码

http://www.cnblogs.com/phquan/archive/2012/03/26/2417460.html

 

分享到:
评论

相关推荐

    基于java的网页即时聊天工具的设计与实现-毕业设计

    一些常用技术,编程实现基于 Java 的网络聊天工具,涉及的内容包括 web 通信,身份验 证和注册登陆,建立服务器,连接服务器以及数据接口实现等。 论文对当今聊天软件的发展情况、与该聊天软件相关的各种技术以及该...

    基于Java即时聊天系统的设计与实现毕业论文.pdf

    基于Java即时聊天系统的设计与实现 摘要:网络络聊天工具已经作为一种重要的信息交流工具,受到越来越多 的网民的青睐。目前,出现了很多非常不错的聊天工具,其中应用比较广泛的 有Netmeeting、腾讯QQ、MSN-Messager...

    网络课程设计报告

    网络课程设计报告主要介绍了基于P2P网络的聊天系统的设计原理与体系结构,并且根据研究的结果开发出了界面友好、方便易用的即时通讯聊天工具。本文首先介绍了软件开发技术基础,其次分析了该系统的功能构成与体系结构,...

    一个面向局域网、互联网的即时聊天程序源代码

    一个面向局域网、互联网的即时聊天工具,它专门针对学校及企业内部的网络通讯而开发的。 Messenger除了具有一般聊天工具都有的基础功能以外,还有自定义表情符等高级功能,使用简单,服务器无需特别数据库配置。 ...

    网络安全原理与应用:上网行为管理.pptx

    如何杜绝员工通过电子邮件、即时聊天工具等途径泄漏企业内部机密资料?上网行为管理系统,是能够对互联网行为提供有效的访问管理、内容监控、行为报警、行为控制功能。从管理层面提供互联网的有效监督,预防、制止...

    飞鸽传书源码详解pdf

    IPMessenger是一款局域网内部聊天、文件传输工具,具有很多优点,如数据通讯不需要建立服务器、直接在两台电脑间通信和数据传输,支持文件及文件目录的传输,安全快捷以及小巧方便等优异特点,因此很多公司都采用它...

    Android 3D游戏开发技术宝典-OpenGL ES 2.0 (吴亚峰) 源代码

    12.1.1 基本原理与核心代码 333 12.1.2 一个主次视角的简单案例 334 12.2 alpha测试 335 12.2.1 alpha测试基本原理 335 12.2.2 一个椭圆窗口的案例 335 12.3 模板测试 337 12.3.1 基本原理 337 ...

    基于SOCKET的网络编程

    通过套接字编程结合swing框架编写即时通信工具。在知道对方ip的前提下能够发送信息给对方,并得到发送的反馈,对方得到收到的消息。双方都能够实现既是的发和收。 二、代码设计 主要有两个类,一是服务端,负责接受...

    JAVA上百实例源码以及开源项目源代码

    Java Socket 聊天通信演示代码 2个目标文件,一个服务器,一个客户端。 Java Telnet客户端实例源码 一个目标文件,演示Socket的使用。 Java 组播组中发送和接受数据实例 3个目标文件。 Java读写文本文件的示例...

    JAVA上百实例源码以及开源项目

    Java Socket 聊天通信演示代码 2个目标文件,一个服务器,一个客户端。 Java Telnet客户端实例源码 一个目标文件,演示Socket的使用。 Java 组播组中发送和接受数据实例 3个目标文件。 Java读写文本文件的示例...

    Java开发技术大全 电子版

    8.4线程的通信与协调277 8.4.1线程的互斥277 8.4.2线程的同步279 8.4.3暂停、恢复和停止线程282 8.4.4生产者-消费者问题实例284 8.5本章小结287 第9章运行时类型识别288 9.1RTTI的作用288 9.2用Class类来...

    Android高级编程--源代码

     《android高级编程》首先简要介绍android软件栈,接着陈述为手机创建稳定可靠、赏心悦目的应用程序的基本原理。通过学习,您可以打下牢固的理论根基,了解使用当前android 1.0 sdk编写定制移动程序所需的知识,还...

    《Android高级编程》

    第9章 P2P通信 9.1 Android即时消息简介 9.1.1 使用GTalk服务 9.1.2 和GTalk服务绑定 9.1.3 建立GTalk连接,开始一个IM会话 9.1.4 在线状态和联系人列表简介 9.1.5 管理聊天会话 9.1.6 发送和接收数据信息 9.2 SMS...

Global site tag (gtag.js) - Google Analytics