引言:
前面专题中介绍了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 + 1, 65536);
// 创建监听套接字
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, 0);
while (true)
{
try
{
// 关闭receiveUdpClient时下面一行代码会产生异常
byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
// 显示消息内容
AddItemToListBox(string.Format("{0}:{1}",remoteIPEndPoint,message));
// 处理消息数据
// 根据协议的设计部分,从客户端发送来的消息是具有一定格式的
// 服务器接收消息后要对消息做处理
string[] splitstring = message.Split(',');
// 解析用户端地址
string[] splitsubstring = splitstring[2].Split(':');
IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitsubstring[0]), int.Parse(splitsubstring[1]));
switch (splitstring[0])
{
// 如果是登录信息,向客户端发送应答消息和广播有新用户登录消息
case "login":
User user = new User(splitstring[1], 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 = 0; i < userList.Count; i++)
{
if (userList[i].GetName() != user.GetName())
{
// 给在线的其他用户发

