`
wbj0110
  • 浏览: 1610617 次
  • 性别: 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

 

分享到:
评论

相关推荐

    中南大学通信网原理课程设计即时通信和网络分析ns2

    本项目以中南大学通信网原理课程设计为背景,通过实际操作和编程实践,深入探讨了即时通信的实现以及网络模拟工具ns2在网络性能分析中的应用。 即时通信(Instant Messaging,IM)是一种实时的通信方式,它允许用户...

    网络聊天工具 C# 即时通信

    本文所介绍的网络聊天工具,结合了C#编程、WinForms界面设计、C/S架构和ADO.NET数据库操作,实现了类似QQ的即时通信功能。其人性化的设计和稳定的性能,不仅满足了现代用户的需求,也为同类项目的开发提供了有价值的...

    linux下即时通信工具

    总的来说,这个基于Linux和MySQL的即时通信工具项目涵盖了网络编程、数据库设计、用户界面开发等多个方面,是学习和实践全栈开发的良好案例。通过研究和实践,开发者不仅可以提升自己的Linux技能,还可以深入理解...

    Java即时通信工具(JICQ).rar_java 即时通信_jicq_即时通_即时通信_通信工具

    6. **UI设计**:用户界面是即时通信工具的重要组成部分,Java Swing或JavaFX可以用来构建图形用户界面,提供友好且直观的交互体验。 7. **数据库存储**:用户信息、好友列表、聊天记录等数据通常会存储在数据库中,...

    基于Java即时聊天系统的设计与实现doc

    总之,基于Java的即时聊天系统设计与实现,涉及了网络编程、数据库管理、安全策略等多个方面的知识,通过合理的系统架构和精心的设计,可以构建出高效、安全的即时通信平台,满足用户的实时交流需求。

    C#局域网聊天工具的设计与实现

    《C#局域网聊天工具的设计与实现》 在当今数字化时代,通信技术日新月异,局域网内的即时通讯工具已经成为了许多工作、学习场景中的必需品。本项目以C#编程语言为基础,利用TCP/IP协议栈,设计并实现了这样一个功能...

    java socket 即时聊天工具(含设计文档)

    总的来说,这个“java socket 即时聊天工具”项目涵盖了Java网络编程的基础,对理解TCP Socket通信机制,以及如何用Java构建实际应用具有很大的帮助。同时,设计文档和源代码为学习者提供了动手实践和自我提升的机会...

    QQ即时通信模拟

    QQ即时通信模拟是一个基于C#编程语言,采用经典三层架构设计的项目,旨在帮助开发者理解和实践即时通信系统的基本原理和实现方法。这个项目包含了全套的程序源码和数据库文件,为学习者提供了一个完整的开发环境,...

    web即时聊天工具

    在本项目中,"web即时聊天工具"是基于VS2008(Visual Studio 2008)使用C#语言开发的,并且与SQL Server 2005数据库相结合,实现了一个网页版的即时通讯系统。 1. **C#编程语言**:C#是微软推出的一种面向对象的...

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

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

    简单即时通信软件

    即时通信(Instant Messaging,IM)软件是一种允许用户实时交流的工具,它通常包含了文本聊天、文件传输、语音和视频通话等功能。"简单即时通信软件"的开发涉及到的关键技术主要是TCP/IP流协议和Socket编程。 TCP...

    局域网即时通信工具------飞鸽源代码

    飞鸽是一款广受欢迎的局域网即时通信工具,它的特点是能够在本地网络中提供快速、便捷的文件传输和实时聊天功能,无需依赖互联网连接。这款工具的设计理念是为了解决办公室、学校或家庭内部网络环境中的通信需求,...

    iWebIM 即时聊天工具

    【iWebIM即时聊天工具】是一款专为网站设计的在线交流解决方案,允许用户在浏览网页的同时与客服或其他用户进行实时沟通。它以其轻量级、易集成的特点,深受开发者和网站管理员的喜爱。这款工具的核心功能是提供一个...

    基于JAVA的即时通信

    在本教程中,我们将探讨如何使用Java语言构建一个基础的即时通信系统,这对于初学者理解网络编程原理和技术是极好的学习材料。 首先,我们需要了解Java中的网络编程基础。Java提供了丰富的API来处理网络通信,如...

    Java即时通信工具(JICQ)设计文档(PPT)

    Java即时通信工具(JICQ)设计文档主要涵盖了Java在实现即时通信系统时的关键技术与设计思路。这个PPT文档很可能会包含以下几个方面的内容: 1. **基础架构**:JICQ可能基于客户端-服务器(C/S)架构,其中客户端用于...

    聊天工具源代码

    【聊天工具源代码】是一个专为初学者设计的学习资源,旨在帮助他们理解并掌握聊天工具的开发技术。这个源代码库可能包含了一个类似QQ的即时通讯应用的核心功能,如用户注册、登录、发送和接收消息、好友管理等。通过...

    基于Java的即时通信系统工程文件

    总的来说,这个基于Java的即时通信系统工程文件是一个综合性的项目,它涵盖了网络编程、用户界面设计、并发处理、数据库操作、安全性等多个方面的知识。通过学习和分析这个工程,开发者不仅可以提升Java编程技能,还...

    Android 开发即时聊天工具 YQ 1.3

    总的来说,"Android开发即时聊天工具YQ 1.3"是一个综合性的项目,涵盖了Android应用开发的多个重要方面,包括网络通信、数据库操作、UI设计和后台服务等。开发者通过这个项目不仅能够掌握即时通讯的技术细节,还能...

    局域网内即时通信工具的开发.docx

    这篇本科毕业设计论文的主题聚焦于局域网内即时通信工具的开发,旨在设计并实现一个用户界面简洁、操作简便的通信工具,具备一对一聊天、文件传输和远程桌面控制等功能。 1. 引言 局域网内的即时通信工具相比于...

Global site tag (gtag.js) - Google Analytics