引言:

前面专题中介绍了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的朋友进行聊天。

三、程序的实现

服务器端核心代码:

 // 启动服务器
// 根据博客中协议的设计部分
// 客户端先向服务器发送登录请求,然后通过服务器返回的端口号
// 再与服务器建立连接
// 所以启动服务按钮事件中有两个套接字:一个是接收客户端信息套接字和
// 监听客户端连接套接字
private void btnStart_Click(object sender, EventArgs e)
{
// 创建接收套接字
serverIp = IPAddress.Parse(txbServerIP.Text);
serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(txbServerport.Text));
receiveUdpClient = new UdpClient(serverIPEndPoint);
// 启动接收线程
Thread receiveThread = new Thread(ReceiveMessage);
receiveThread.Start();
btnStart.Enabled = false;
btnStop.Enabled = true; // 随机指定监听端口
Random random = new Random();
tcpPort = random.Next(port + , ); // 创建监听套接字
tcpListener = new TcpListener(serverIp, tcpPort);
tcpListener.Start(); // 启动监听线程
Thread listenThread = new Thread(ListenClientConnect);
listenThread.Start();
AddItemToListBox(string.Format("服务器线程{0}启动,监听端口{1}",serverIPEndPoint,tcpPort));
} // 接收客户端发来的信息
private void ReceiveMessage()
{
IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, );
while (true)
{
try
{
// 关闭receiveUdpClient时下面一行代码会产生异常
byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
string message = Encoding.Unicode.GetString(receiveBytes, , receiveBytes.Length); // 显示消息内容
AddItemToListBox(string.Format("{0}:{1}",remoteIPEndPoint,message)); // 处理消息数据
// 根据协议的设计部分,从客户端发送来的消息是具有一定格式的
// 服务器接收消息后要对消息做处理
string[] splitstring = message.Split(',');
// 解析用户端地址
string[] splitsubstring = splitstring[].Split(':');
IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitsubstring[]), int.Parse(splitsubstring[]));
switch (splitstring[])
{
// 如果是登录信息,向客户端发送应答消息和广播有新用户登录消息
case "login":
User user = new User(splitstring[], clientIPEndPoint);
// 往在线的用户列表添加新成员
userList.Add(user);
AddItemToListBox(string.Format("用户{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
string sendString = "Accept," + tcpPort.ToString();
// 向客户端发送应答消息
SendtoClient(user, sendString);
AddItemToListBox(string.Format("向{0}({1})发出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
for (int i = ; i < userList.Count; i++)
{
if (userList[i].GetName() != user.GetName())
{
// 给在线的其他用户发送广播消息
// 通知有新用户加入
SendtoClient(userList[i], message);
}
} AddItemToListBox(string.Format("广播:[{0}]", message));
break;
case "logout":
for (int i = ; i < userList.Count; i++)
{
if (userList[i].GetName() == splitstring[])
{
AddItemToListBox(string.Format("用户{0}({1})退出",userList[i].GetName(),userList[i].GetIPEndPoint()));
userList.RemoveAt(i); // 移除用户
}
}
for (int i = ; i < userList.Count; i++)
{
// 广播注销消息
SendtoClient(userList[i], message);
}
AddItemToListBox(string.Format("广播:[{0}]", message));
break;
}
}
catch
{
// 发送异常退出循环
break;
}
}
AddItemToListBox(string.Format("服务线程{0}终止", serverIPEndPoint));
} // 向客户端发送消息
private void SendtoClient(User user, string message)
{
// 匿名方式发送
sendUdpClient = new UdpClient();
byte[] sendBytes = Encoding.Unicode.GetBytes(message);
IPEndPoint remoteIPEndPoint =user.GetIPEndPoint();
sendUdpClient.Send(sendBytes,sendBytes.Length,remoteIPEndPoint);
sendUdpClient.Close();
} // 接受客户端的连接
private void ListenClientConnect()
{
TcpClient newClient = null;
while (true)
{
try
{
newClient = tcpListener.AcceptTcpClient();
AddItemToListBox(string.Format("接受客户端{0}的TCP请求",newClient.Client.RemoteEndPoint));
}
catch
{
AddItemToListBox(string.Format("监听线程({0}:{1})", serverIp, tcpPort));
break;
} Thread sendThread = new Thread(SendData);
sendThread.Start(newClient);
}
} // 向客户端发送在线用户列表信息
// 服务器通过TCP连接把在线用户列表信息发送给客户端
private void SendData(object userClient)
{
TcpClient newUserClient = (TcpClient)userClient;
userListstring = null;
for (int i = ; i < userList.Count; i++)
{
userListstring += userList[i].GetName() + ","
+ userList[i].GetIPEndPoint().ToString() + ";";
} userListstring += "end";
networkStream = newUserClient.GetStream();
binaryWriter = new BinaryWriter(networkStream);
binaryWriter.Write(userListstring);
binaryWriter.Flush();
AddItemToListBox(string.Format("向{0}发送[{1}]", newUserClient.Client.RemoteEndPoint, userListstring));
binaryWriter.Close();
newUserClient.Close();
}

客户端核心代码:

 // 登录服务器
private void btnlogin_Click(object sender, EventArgs e)
{
// 创建接受套接字
IPAddress clientIP = IPAddress.Parse(txtLocalIP.Text);
clientIPEndPoint = new IPEndPoint(clientIP, int.Parse(txtlocalport.Text));
receiveUdpClient = new UdpClient(clientIPEndPoint);
// 启动接收线程
Thread receiveThread = new Thread(ReceiveMessage);
receiveThread.Start(); // 匿名发送
sendUdpClient = new UdpClient();
// 启动发送线程
Thread sendThread = new Thread(SendMessage);
sendThread.Start(string.Format("login,{0},{1}", txtusername.Text, clientIPEndPoint)); btnlogin.Enabled = false;
btnLogout.Enabled = true;
this.Text = txtusername.Text;
} // 客户端接受服务器回应消息
private void ReceiveMessage()
{
IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any,);
while (true)
{
try
{
// 关闭receiveUdpClient时会产生异常
byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
string message = Encoding.Unicode.GetString(receiveBytes,,receiveBytes.Length); // 处理消息
string[] splitstring = message.Split(','); switch (splitstring[])
{
case "Accept":
try
{
tcpClient = new TcpClient();
tcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitstring[]));
if (tcpClient != null)
{
// 表示连接成功
networkStream = tcpClient.GetStream();
binaryReader = new BinaryReader(networkStream);
}
}
catch
{
MessageBox.Show("连接失败", "异常");
} Thread getUserListThread = new Thread(GetUserList);
getUserListThread.Start();
break;
case "login":
string userItem = splitstring[] + "," + splitstring[];
AddItemToListView(userItem);
break;
case "logout":
RemoveItemFromListView(splitstring[]);
break;
case "talk":
for (int i = ; i < chatFormList.Count; i++)
{
if (chatFormList[i].Text == splitstring[])
{
chatFormList[i].ShowTalkInfo(splitstring[], splitstring[], splitstring[]);
}
} break;
}
}
catch
{
break;
}
}
} // 从服务器获取在线用户列表
private void GetUserList()
{
while (true)
{
userListstring = null;
try
{
userListstring = binaryReader.ReadString();
if (userListstring.EndsWith("end"))
{
string[] splitstring = userListstring.Split(';');
for (int i = ; i < splitstring.Length - ; i++)
{
AddItemToListView(splitstring[i]);
} binaryReader.Close();
tcpClient.Close();
break;
}
}
catch
{
break;
}
}
}
// 发送登录请求
private void SendMessage(object obj)
{
string message = (string)obj;
byte[] sendbytes = Encoding.Unicode.GetBytes(message);
IPAddress remoteIp = IPAddress.Parse(txtserverIP.Text);
IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(txtServerport.Text));
sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);
sendUdpClient.Close();
}

程序的运行结果:

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

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

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

四、总结

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

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

  本程序的源代码链接:http://files.cnblogs.com/zhili/IM.zip 觉得有帮助的话还望推荐下,如果有任何意见可以留言。谢谢大家的支持

专题九:实现类似QQ的即时通信程序的更多相关文章

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

    转自:http://www.cnblogs.com/zhili/archive/2012/09/23/2666987.html 引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例 ...

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

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

  3. 详解C# 网络编程系列:实现类似QQ的即时通信程序

    https://www.jb51.net/article/101289.htm 引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例来让大家更好的理解它们的工作原理以及怎样.Net ...

  4. 实现类似QQ的即时通信程序(十一)

    此为网络编程的一个系列,后续会把内容补上....

  5. C语言 linux环境基于socket的简易即时通信程序

    转载请注明出处:http://www.cnblogs.com/kevince/p/3891033.html      ——By Kevince 最近在看linux网络编程相关,现学现卖,就写了一个简易 ...

  6. 一个基于python的即时通信程序

    5月17日更新: 广播信息.用户列表.信息确认列表以及通信信息,从原来的用字符串存储改为使用字典来存储,使代码更清晰,更容易扩展,具体更改的格式如下: 广播信息(上线): { 'status': 信息 ...

  7. QQ 腾讯QQ(简称“QQ”)是腾讯公司开发的一款基于Internet的即时通信(IM)软件

    QQ 编辑 腾讯QQ(简称“QQ”)是腾讯公司开发的一款基于Internet的即时通信(IM)软件.腾讯QQ支持在线聊天.视频通话.点对点断点续传文件.共享文件.网络硬盘.自定义面板.QQ邮箱等多种功 ...

  8. 结束QQ即时通信垄断,开辟即时通信互联互通instantnet时代

    结束QQ即时通信垄断,开辟即时通信互联互通instantnet时代 蓬勃发展的即时通信产业 即时通信(IM)是指可以即时发送和接收互联网消息等的业务. 即时通信.就是瞬间把信息发送给对方,假设不是即时 ...

  9. (转)基于即时通信和LBS技术的位置感知服务(一):提出问题及解决方案

    一.前言.提出问题 公司最近举行2011年度创新设计大赛,快年底了正打算写写2010年以来Android开发的心得与经验,正好同事出了个点子:假如A和B两个人分别在不同的地点,能不能实现这样的功能,让 ...

随机推荐

  1. 洛谷 P1065 作业调度方案

    P1065 作业调度方案 题目描述 我们现在要利用 mm 台机器加工 nn 个工件,每个工件都有 mm 道工序,每道工序都在不同的指定的机器上完成.每个工件的每道工序都有指定的加工时间. 每个工件的每 ...

  2. NOIP 2010 机器翻译

    P1540 机器翻译 题目背景 小晨的电脑上安装了一个机器翻译软件,他经常用这个软件来翻译英语文章. 题目描述 这个翻译软件的原理很简单,它只是从头到尾,依次将每个英文单词用对应的中文含义来替换.对于 ...

  3. 奥多朗WIFI 插座

    https://aoduolang.tmall.com/category-1089563810.htm?spm=a1z10.1-b.w11212542-12917613245.12.tTWFSc&am ...

  4. MongoDB小结12 - update【多文档更新】

    当一次更新一个文档无法满足我们的脚步时,我们可以选择一次更新多个文档,及在update的第四个参数的位置添上true,及做多文档更新,建议就算不做多文档更新也显式的在第四个参数上置false,这样明确 ...

  5. spring,spring mvc之所以起作用是因为开启了注解解释器,即spring的annotation on

    spring,spring mvc之所以起作用是因为开启了注解解释器,即spring的annotation on

  6. java代码判断文件类型(判断文件后缀名)

    1.两点需要注意 1.string.spilt("\\.")分割字符串成子字符串数组,以“.”分割,必须写成string.spilt("\\.")的方式,不能写 ...

  7. js算法:分治法-循环赛事日程表

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/ ...

  8. SpringMVC导出Excel

    import java.math.BigDecimal; import java.net.URLEncoder; import java.text.SimpleDateFormat; import j ...

  9. cocos2d-x 求相交矩阵

    cocos2d-x有推断矩阵相交的方法 CCRect::intersectsRect(CCRect& rect)但可惜没有提供求两个相交矩阵的方法,我作了总结,代码例如以下: CCRect T ...

  10. jquery 页面滚动tab自动定位,tab与内容对应

    直接上源码,基于jquery写的,可以直接跑起来. 原理是先计算出页面元素对应的高度.页面滚动时计算tab对应的高端区间,设置具体的tab.欢迎指正 下载地址 <!DOCTYPE html> ...