引言:

前面专题中介绍了UDP、TCP和P2P编程,并且通过一些小的示例来让大家更好的理解它们的工作原理以及怎样.Net类库去实现它们的。为了让大家更好的理解我们平常中常见的软件QQ的工作原理,所以在本专题中将利用前面专题介绍的知识来实现一个类似QQ的聊天程序。

一、即时通信系统

在我们的生活中经常使用即时通信的软件,我们经常接触到的有:QQ、阿里旺旺、MSN等等。这些都是属于即时通信(Instant Messenger,IM)软件,IM是指所有能够即时发送和接收互联网消息的软件。

在前面专题P2P编程中介绍过P2P系统分两种类型——单纯型P2P和混合型P2P(QQ就是属于混合型的应用),混合型P2P系统中的服务器(也叫索引服务器)起到协调的作用。在文件共享类应用中,如果采用混合型P2P技术的话,索引服务器就保存着文件信息,这样就可能会造成版权的问题,然而在即时通信类的软件中, 因为客户端传递的都是简单的聊天文本而不是网络媒体资源,这样就不存在版权问题了,在这种情况下,就可以采用混合型P2P技术来实现我们的即时通信软件。前面已经讲了,腾讯的QQ就是属于混合型P2P的软件。

因此本专题要实现一个类似QQ的聊天程序,其中用到的P2P技术是属于混合型P2P,而不是前一专题中的采用的单纯型P2P技术,同时本程序的实现也会用到TCP、UDP编程技术。具体的相关内容大家可以查看本系列的相关专题的。

二、程序实现的详细设计

本程序采用P2P方式,各个客户端之间直接发消息进行聊天,服务器在其中只是起到协调的作用,下面先理清下程序的流程:

2.1 程序流程设计

当一个新用户通过客户端登陆系统后,从服务器获取当在线的用户信息列表,列表信息包括系统中每个用户的地址,然后用户就可以单独向其他发消息。如果有用户加入或者在线用户退出时,服务器就会及时发消息通知系统中的所有其他客户端,达到它们即时地更新用户信息列表。

根据上面大致的描述,我们可以把系统的流程分为下面几步来更好的理解(大家可以参考QQ程序将会更好的理解本程序的流程):

  1. 用户通过客户端进入系统,向服务器发出消息,请求登陆
  2. 服务器收到请求后,向客户端返回回应消息,表示同意接受该用户加入,并把自己(指的是服务器)所在监听的端口发送给客户端
  3. 客户端根据服务器发送过来的端口号和服务器建立连接
  4. 服务器通过该连接 把在线用户的列表信息发送给新加入的客户端。
  5. 客户端获得了在线用户列表后就可以自己选择在线用户聊天。(程序中另外设计一个类似QQ的聊天窗口来进行聊天)
  6. 当用户退出系统时也要及时通知服务器,服务器再把这个消息转发给每个在线的用户,使客户端及时更新本地的用户信息列表。

2.2 通信协议设计

所谓协议就是约定,即服务器和客户端之间会话信息的内容格式进行约定,使双方都可以识别,达到更好的通信。

下面就具体介绍下协议的设计:

1. 客户端和服务器之间的对话

(1)登陆过程

① 客户端用匿名UDP的方式向服务器发出下面的信息:

    login, username, localIPEndPoint

   消息内容包括三个字段,每个字段用 “,”分割,login表示的是请求登陆;username表示用户名;localIPEndPint表示客户端本地地址。

  ② 服务器收到后以匿名UDP返回下面的回应:

   Accept, port

  其中Accept表示服务器接受请求,port表示服务器所在的端口号,服务器监听着这个端口的客户端连接

  ③ 连接服务器,获取用户列表

  客户端从上一步获得了端口号,然后向该端口发起TCP连接,向服务器索取在线用户列表,服务器接受连接后将用户列表传输到客户端。用户列表信息格式如下:

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

  username1、username2表示用户名,IPEndPoint1,IPEndPoint2表示对应的端点,每个用户信息都是由"用户名+端点"组成,用户信息以“;”隔开,整个用户列表以“end”结尾。

(2)注销过程

  用户退出时,向服务器发送如下消息:

   logout,username,localIPEndPoint

  这条消息看字面意思大家都知道就是告诉服务器 username+localIPEndPoint这个用户要退出了。

2. 服务器管理用户

(1)新用户加入通知

因为系统中在线的每个用户都有一份当前在线用户表,因此当有新用户登录时,服务器不需要重复地给系统中的每个用户再发送所有用户信息,只需要将新加入用户的信息通知其他用户,其他用户再更新自己的用户列表。

服务器向系统中每个用户广播如下信息:

  login,username,remoteIPEndPoint

在这个过程中服务器只是负责将收到的"login"信息转发出去。

(2)用户退出

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

  logout,username,remoteIPEndPoint

3. 客户端之间聊天

用户进行聊天时,各自的客户端之间是以P2P方式进行工作的,不与服务器有直接联系,这也是P2P技术的特点。

聊天发送的消息格式如下:

  talk, longtime, selfUserName, message

其中,talk表明这是聊天内容的消息;longtime是长时间格式的当前系统时间;selfUserName为发送发的用户名;message表示消息的内容。

协议设计介绍完后,下面就进入本程序的具体实现的介绍的。

注:协议是本程序的核心,也是所有软件的核心,每个软件产品的协议都是不一样的,QQ有自己的一套协议,MSN又有另一套协议,所以使用的QQ的用户无法和用MSN的朋友进行聊天。

三、程序的实现

服务器端核心代码:

View Code

  1  // 启动服务器
  2         // 根据博客中协议的设计部分
  3         // 客户端先向服务器发送登录请求,然后通过服务器返回的端口号
  4         // 再与服务器建立连接
  5         // 所以启动服务按钮事件中有两个套接字:一个是接收客户端信息套接字和
  6         // 监听客户端连接套接字
  7         private void btnStart_Click(object sender, EventArgs e)
  8         {
  9             // 创建接收套接字
 10             serverIp = IPAddress.Parse(txbServerIP.Text);
 11             serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(txbServerport.Text));
 12             receiveUdpClient = new UdpClient(serverIPEndPoint);
 13             // 启动接收线程
 14             Thread receiveThread = new Thread(ReceiveMessage);
 15             receiveThread.Start();
 16             btnStart.Enabled = false;
 17             btnStop.Enabled = true;
 18
 19             // 随机指定监听端口
 20             Random random = new Random();
 21             tcpPort = random.Next(port + 1, 65536);
 22
 23             // 创建监听套接字
 24             tcpListener = new TcpListener(serverIp, tcpPort);
 25             tcpListener.Start();
 26
 27             // 启动监听线程
 28             Thread listenThread = new Thread(ListenClientConnect);
 29             listenThread.Start();
 30             AddItemToListBox(string.Format("服务器线程{0}启动,监听端口{1}",serverIPEndPoint,tcpPort));
 31         }
 32
 33         // 接收客户端发来的信息
 34         private void ReceiveMessage()
 35         {
 36             IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
 37             while (true)
 38             {
 39                 try
 40                 {
 41                     // 关闭receiveUdpClient时下面一行代码会产生异常
 42                     byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
 43                     string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
 44
 45                     // 显示消息内容
 46                     AddItemToListBox(string.Format("{0}:{1}",remoteIPEndPoint,message));
 47
 48                     // 处理消息数据
 49                     // 根据协议的设计部分,从客户端发送来的消息是具有一定格式的
 50                     // 服务器接收消息后要对消息做处理
 51                     string[] splitstring = message.Split(',');
 52                     // 解析用户端地址
 53                     string[] splitsubstring = splitstring[2].Split(':');
 54                     IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitsubstring[0]), int.Parse(splitsubstring[1]));
 55                     switch (splitstring[0])
 56                     {
 57                         // 如果是登录信息,向客户端发送应答消息和广播有新用户登录消息
 58                         case "login":
 59                             User user = new User(splitstring[1], clientIPEndPoint);
 60                             // 往在线的用户列表添加新成员
 61                             userList.Add(user);
 62                             AddItemToListBox(string.Format("用户{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
 63                             string sendString = "Accept," + tcpPort.ToString();
 64                             // 向客户端发送应答消息
 65                             SendtoClient(user, sendString);
 66                             AddItemToListBox(string.Format("向{0}({1})发出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
 67                             for (int i = 0; i < userList.Count; i++)
 68                             {
 69                                 if (userList[i].GetName() != user.GetName())
 70                                 {
 71                                     // 给在线的其他用户发送广播消息
 72                                     // 通知有新用户加入
 73                                     SendtoClient(userList[i], message);
 74                                 }
 75                             }
 76
 77                             AddItemToListBox(string.Format("广播:[{0}]", message));
 78                             break;
 79                         case "logout":
 80                             for (int i = 0; i < userList.Count; i++)
 81                             {
 82                                 if (userList[i].GetName() == splitstring[1])
 83                                 {
 84                                     AddItemToListBox(string.Format("用户{0}({1})退出",userList[i].GetName(),userList[i].GetIPEndPoint()));
 85                                     userList.RemoveAt(i); // 移除用户
 86                                 }
 87                             }
 88                             for (int i = 0; i < userList.Count; i++)
 89                             {
 90                                 // 广播注销消息
 91                                 SendtoClient(userList[i], message);
 92                             }
 93                             AddItemToListBox(string.Format("广播:[{0}]", message));
 94                             break;
 95                     }
 96                 }
 97                 catch
 98                 {
 99                     // 发送异常退出循环
100                     break;
101                 }
102             }
103             AddItemToListBox(string.Format("服务线程{0}终止", serverIPEndPoint));
104         }
105
106         // 向客户端发送消息
107         private void SendtoClient(User user, string message)
108         {
109             // 匿名方式发送
110             sendUdpClient = new UdpClient(0);
111             byte[] sendBytes = Encoding.Unicode.GetBytes(message);
112             IPEndPoint remoteIPEndPoint =user.GetIPEndPoint();
113             sendUdpClient.Send(sendBytes,sendBytes.Length,remoteIPEndPoint);
114             sendUdpClient.Close();
115         }
116
117         // 接受客户端的连接
118         private void ListenClientConnect()
119         {
120             TcpClient newClient = null;
121             while (true)
122             {
123                 try
124                 {
125                     newClient = tcpListener.AcceptTcpClient();
126                     AddItemToListBox(string.Format("接受客户端{0}的TCP请求",newClient.Client.RemoteEndPoint));
127                 }
128                 catch
129                 {
130                     AddItemToListBox(string.Format("监听线程({0}:{1})", serverIp, tcpPort));
131                     break;
132                 }
133
134                 Thread sendThread = new Thread(SendData);
135                 sendThread.Start(newClient);
136             }
137         }
138
139         // 向客户端发送在线用户列表信息
140         // 服务器通过TCP连接把在线用户列表信息发送给客户端
141         private void SendData(object userClient)
142         {
143             TcpClient newUserClient = (TcpClient)userClient;
144             userListstring = null;
145             for (int i = 0; i < userList.Count; i++)
146             {
147                 userListstring += userList[i].GetName() + ","
148                     + userList[i].GetIPEndPoint().ToString() + ";";
149             }
150
151             userListstring += "end";
152             networkStream = newUserClient.GetStream();
153             binaryWriter = new BinaryWriter(networkStream);
154             binaryWriter.Write(userListstring);
155             binaryWriter.Flush();
156             AddItemToListBox(string.Format("向{0}发送[{1}]", newUserClient.Client.RemoteEndPoint, userListstring));
157             binaryWriter.Close();
158             newUserClient.Close();
159         }

客户端核心代码:

View Code

  1  // 登录服务器
  2         private void btnlogin_Click(object sender, EventArgs e)
  3         {
  4             // 创建接受套接字
  5             IPAddress clientIP = IPAddress.Parse(txtLocalIP.Text);
  6             clientIPEndPoint = new IPEndPoint(clientIP, int.Parse(txtlocalport.Text));
  7             receiveUdpClient = new UdpClient(clientIPEndPoint);
  8             // 启动接收线程
  9             Thread receiveThread = new Thread(ReceiveMessage);
 10             receiveThread.Start();
 11
 12             // 匿名发送
 13             sendUdpClient = new UdpClient(0);
 14             // 启动发送线程
 15             Thread sendThread = new Thread(SendMessage);
 16             sendThread.Start(string.Format("login,{0},{1}", txtusername.Text, clientIPEndPoint));
 17
 18             btnlogin.Enabled = false;
 19             btnLogout.Enabled = true;
 20             this.Text = txtusername.Text;
 21         }
 22
 23         // 客户端接受服务器回应消息
 24         private void ReceiveMessage()
 25         {
 26             IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any,0);
 27             while (true)
 28             {
 29                 try
 30                 {
 31                     // 关闭receiveUdpClient时会产生异常
 32                     byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
 33                     string message = Encoding.Unicode.GetString(receiveBytes,0,receiveBytes.Length);
 34
 35                     // 处理消息
 36                     string[] splitstring = message.Split(',');
 37
 38                     switch (splitstring[0])
 39                     {
 40                         case "Accept":
 41                             try
 42                             {
 43                                 tcpClient = new TcpClient();
 44                                 tcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitstring[1]));
 45                                 if (tcpClient != null)
 46                                 {
 47                                     // 表示连接成功
 48                                     networkStream = tcpClient.GetStream();
 49                                     binaryReader = new BinaryReader(networkStream);
 50                                 }
 51                             }
 52                             catch
 53                             {
 54                                 MessageBox.Show("连接失败", "异常");
 55                             }
 56
 57                             Thread getUserListThread = new Thread(GetUserList);
 58                             getUserListThread.Start();
 59                             break;
 60                         case "login":
 61                             string userItem = splitstring[1] + "," + splitstring[2];
 62                             AddItemToListView(userItem);
 63                             break;
 64                         case "logout":
 65                             RemoveItemFromListView(splitstring[1]);
 66                             break;
 67                         case "talk":
 68                             for (int i = 0; i < chatFormList.Count; i++)
 69                             {
 70                                 if (chatFormList[i].Text == splitstring[2])
 71                                 {
 72                                     chatFormList[i].ShowTalkInfo(splitstring[2], splitstring[1], splitstring[3]);
 73                                 }
 74                             }
 75
 76                             break;
 77                     }
 78                 }
 79                 catch
 80                 {
 81                     break;
 82                 }
 83             }
 84         }
 85
 86         // 从服务器获取在线用户列表
 87         private void GetUserList()
 88         {
 89             while (true)
 90             {
 91                 userListstring = null;
 92                 try
 93                 {
 94                     userListstring = binaryReader.ReadString();
 95                     if (userListstring.EndsWith("end"))
 96                     {
 97                         string[] splitstring = userListstring.Split(';');
 98                         for (int i = 0; i < splitstring.Length - 1; i++)
 99                         {
100                             AddItemToListView(splitstring[i]);
101                         }
102
103                         binaryReader.Close();
104                         tcpClient.Close();
105                         break;
106                     }
107                 }
108                 catch
109                 {
110                     break;
111                 }
112             }
113         }
114    // 发送登录请求
115         private void SendMessage(object obj)
116         {
117             string message = (string)obj;
118             byte[] sendbytes = Encoding.Unicode.GetBytes(message);
119             IPAddress remoteIp = IPAddress.Parse(txtserverIP.Text);
120             IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(txtServerport.Text));
121             sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);
122             sendUdpClient.Close();
123         }

程序的运行结果:

首先先运行服务器窗口,在服务器窗口点击“启动”按钮来启动服务器,然后客户端首先指定服务器的端口号,修改用户名(这里也可以不修改,使用默认的也可以),然后点击“登录”按钮来登陆服务器(也就是告诉服务器本地的客户端地址),然后从服务器端获得在线用户列表,界面演示如下:

然后用户可以双击在线用户进行聊天(此程序支持与多人进行聊天),下面是功能的演示图片:

双方进行聊天时,这里没有实现像QQ一样,有人发信息来在对应的客户端就有消息提醒的功能的, 所以双方进行聊天的过程中,每个客户端都需要在在线用户列表中点击聊天的对象来激活聊天对话框(意思就是从图片中可以看出“天涯”客户端想和剑痴聊天的话,就在“在线用户”列表双击剑痴来激活聊天窗口,同时“剑痴”客户端也必须双击“天涯”来激活聊天窗口,这样双方就看到对方发来的信息了,(不激活窗口,也是发送了信息,只是没有一个窗口来进行显示)),而且从图片中也可以看出——此程序支持与多人聊天,即天涯同时与“剑痴”和"大地"同时聊天。

四、总结

本专题介绍了如何去实现一个类似QQ的聊天程序,一方面让大家可以巩固前面专题的内容,另一方面让大家更好的理解即时通信软件(腾讯QQ)的工作原理和软件协议的设计。

后面一专题将介绍如何去实现邮件系统中常用的功能——实现一个简单的邮件应用。

[C# 网络编程系列]专题九:实现类似QQ的即时通信程序相关推荐

  1. 转:【专题九】实现类似QQ的即时通信程序

    引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例来让大家更好的理解它们的工作原理以及怎样.Net类库去实现它们的.为了让大家更好的理解我们平常中常见的软件QQ的工作原理,所以在 ...

  2. [C# 网络编程系列]专题十二:实现一个简单的FTP服务器

    引言: 休息一个国庆节后好久没有更新文章了,主要是刚开始休息完心态还没有调整过来的, 现在差不多进入状态了, 所以继续和大家分享下网络编程的知识,在本专题中将和大家分享如何自己实现一个简单的FTP服务 ...

  3. [C# 网络编程系列]专题五:TCP编程

    前言 前面专题的例子都是基于应用层上的HTTP协议的介绍, 现在本专题来介绍下传输层协议--TCP协议,主要介绍下TCP协议的工作过程和基于TCP协议的一个简单的通信程序,下面就开始本专题的正文了. ...

  4. [C# 网络编程系列]专题十一:实现一个基于FTP协议的程序——文件上传下载器...

    引言: 在这个专题将为大家揭开下FTP这个协议的面纱,其实学习知识和生活中的例子都是很相通的,就拿这个专题来说,要了解FTP协议然后根据FTP协议实现一个文件下载器,就和和追MM是差不多的过程的,相信 ...

  5. [C# 网络编程系列]专题四:自定义Web浏览器

    前言: 前一个专题介绍了自定义的Web服务器,然而向Web服务器发出请求的正是本专题要介绍的Web浏览器,本专题通过简单自定义一个Web浏览器来简单介绍浏览器的工作原理,以及帮助一些初学者揭开浏览器这 ...

  6. [C# 网络编程系列]专题六:UDP编程

    引言: 前一个专题简单介绍了TCP编程的一些知识,UDP与TCP地位相当的另一个传输层协议,它也是当下流行的很多主流网络应用(例如QQ.MSN和Skype等一些即时通信软件传输层都是应用UDP协议的) ...

  7. [C#]网络编程系列专题二:HTTP协议详解

    转自:http://www.cnblogs.com/zhili/archive/2012/08/18/2634475.html 我们在用Asp.net技术开发Web应用程序后,当用户在浏览器输入一个网 ...

  8. [C# 网络编程系列]专题十:实现简单的邮件收发器

    引言: 在我们的平常工作中,邮件的发送和接收应该是我们经常要使用到的功能的.因此知道电子邮件的应用程序的原理也是非常有必要的,在这一个专题中将介绍电子邮件应用程序的原理.电子邮件应用程序中涉及的协议和 ...

  9. [C# 网络编程系列]专题七:UDP编程补充——UDP广播程序的实现

    上次因为时间的关系,所以把上一个专题遗留下的一个问题在本专题中和大家分享下,本专题主要介绍下如何实现UDP广播的程序,下面就直接介绍实现过程和代码以及运行的结果. 一.程序实现 UDP广播程序的实现代 ...

最新文章

  1. PicoBlaze 指令存储器配置方式
  2. 案例 | 锋芒易商如何做到年省 50 人天?
  3. 日常生活 -- 开博二周年
  4. 【转】libpcap实现机制及接口函数
  5. WinForm中DataGridView的TextBoxColumm换行
  6. 华为服务器虚拟化断电,服务器断电日志查看
  7. android功耗优化(2)--对齐唤醒
  8. hnu 暑期实训之Maya历法
  9. MySQL替换函数REPLACE替换字符串方法
  10. 教你用C调1645和弦,写出不一样的歌
  11. 深入浅出PID控制算法(二)——PID算法离散化和增量式PID算法原理及Matlab实现
  12. 在同一局域网连接其他电脑的MySQL数据库
  13. DNF盗号木马之突破令牌密保
  14. Mysql报错:SQLIntegrityConstraintViolationException: Duplicate entry ‘xxx‘ for key ‘PRIMARY‘
  15. 各大浏览器清除缓存(cache)详细步骤
  16. Matlab中interp2和interpn的差异
  17. Uedit32设置字体大小
  18. 微信网页授权的2种方式
  19. Win10 下使用Telnet命令,打开telnet功能
  20. GitHub学生开发包

热门文章

  1. 视频:如何把应用通过vmc部署到cloud foundry上
  2. HBase中数据的多版本特性潜在的意外
  3. 软件设计方法和设计决策
  4. Yahoo,希望你和微软Bing能过的幸福
  5. Redis—字符串和SDS
  6. 第6章 数据库索引优化
  7. 收获,不止SQL优化——抓住SQL的本质--第十一章
  8. golang nil 不等于 nil的问题
  9. 高效实用Kafka-Kafka是什么
  10. HashMap面试指南