P2P编程

在C/S架构中,所有客户端都直接从服务器下载所有数据资源,这样势必会加重服务器的负担,而P2P则改变了以服务器为中心的状态,使每个节点可以先从服务器上个下载一部分,然后再相互从对方或者其他节点下载其余部分。采用这种方式,当大量客户端同时下载时,就不会形成网络堵塞现象了。
在这里插入图片描述
在这里插入图片描述
实现P2P应用程序
这里就简单实现了资源发现的一个程序。
核心代码:

对等名称的注册代码:
// 注册资源
private void btnRegister_Click(object sender, EventArgs e)
{
if (tbxResourceName.Text == “”)
{
MessageBox.Show(“请输入发布的资源名!”, “提示”);
return;
}

       // 将资源名注册到云中  
       // 具体资源名的结构在博客有介绍  
       PeerName resourceName = new PeerName(tbxResourceName.Text, PeerNameType.Unsecured);  
       // 用指定的名称和端口号初始化PeerNameRegistration类的实例  
       resourceNameReg[seedCount] = new PeerNameRegistration(resourceName, int.Parse(tbxlocalport.Text));  
       // 设置在云中注册的对等名对象的其他信息的注释  
       resourceNameReg[seedCount].Comment =resourceName.ToString();  
       // 设置PeerNameRegistration对象的应用程序定义的二进制数据  
       resourceNameReg[seedCount].Data = Encoding.UTF8.GetBytes(string.Format("{0}", DateTime.Now.ToString()));  
       // 在云中注册PeerName(对等名)  
       resourceNameReg[seedCount].Start();  
       seedCount++;  
       comboxSharelist.Items.Add(resourceName.ToString());  
       tbxResourceName.Text = "";  
   } 

名称解析代码(搜索资源):
// 搜索资源
private void btnSearch_Click(object sender, EventArgs e)
{
if (tbxSeed.Text == “”)
{
MessageBox.Show(“请先输入要寻找的种子资源名”, “提示”);
return;
}

      lstViewOnlinePeer.Items.Clear();  
      // 初始化要搜索的资源名  
      PeerName searchSeed = new PeerName("0." + tbxSeed.Text);  
      // PeerNameResolver类是将节点名解析为PeerNameRecord的值(即将通过资源名来查找资源名所在的地址,包括IP地址和端口号)  
      // PeerNameRecord用来定于云中的各个节点  
      PeerNameResolver myresolver = new PeerNameResolver();  

      // PeerNameRecordCollection表示PeerNameRecord元素的容器  
      // Resolve方法是同步的完成解析  
      // 使用同步方法可能会出现界面“假死”现象  
      // 解决界面假死现象可以采用多线程或异步的方式  
      // 关于多线程的知识可以参考本人博客中多线程系列我前面UDP编程中有所使用  
      // 在这里就不列出多线程的使用了,朋友可以自己实现,如果有问题可以留言给我一起讨论  
      PeerNameRecordCollection recordCollection = myresolver.Resolve(searchSeed);  
      foreach (PeerNameRecord record in recordCollection)  
      {  
          foreach(IPEndPoint endpoint in record.EndPointCollection)  
          {  
              if (endpoint.AddressFamily.Equals(AddressFamily.InterNetwork))  
              {  
                  ListViewItem item = new ListViewItem();     
                  item.SubItems.Add(endpoint.ToString());  
                  item.SubItems.Add(Encoding.UTF8.GetString(record.Data));  
                  lstViewOnlinePeer.Items.Add(item);  
              }  
          }  
      }  
  } 

在这里插入图片描述
微软帮我们已经封装好了对PNRP协议的实现,这些类在System.Net.PeerToPeer命名空间里

一、即时通信系统

在我们的生活中经常使用即时通信的软件,我们经常接触到的有: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这个用户要退出了。

  1. 服务器管理用户

(1)新用户加入通知

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

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

login,username,remoteIPEndPoint

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

(2)用户退出

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

logout,username,remoteIPEndPoint
  1. 客户端之间聊天

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

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

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

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

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

三、程序的实现

服务器端核心代码:
View Code
// 启动服务器
// 根据博客中协议的设计部分
// 客户端先向服务器发送登录请求,然后通过服务器返回的端口号
// 再与服务器建立连接
// 所以启动服务按钮事件中有两个套接字:一个是接收客户端信息套接字和
// 监听客户端连接套接字
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())  
                             {  
                                 // 给在线的其他用户发送广播消息  
                                 // 通知有新用户加入  
                                 SendtoClient(userList[i], message);  
                             }  
                         }  

                         AddItemToListBox(string.Format("广播:[{0}]", message));  
                         break;  
                     case "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));  
                         break;  
                 }  
             }  
             catch 
             {  
                 // 发送异常退出循环  
                 break;  
             }  
         }  
         AddItemToListBox(string.Format("服务线程{0}终止", serverIPEndPoint));  
     }  

     // 向客户端发送消息  
     private void SendtoClient(User user, string message)  
     {  
         // 匿名方式发送  
         sendUdpClient = new UdpClient(0);  
         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 = 0; 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();  
     } 

View Code
// 登录服务器
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(0);  
         // 启动发送线程  
         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,0);  
         while (true)  
         {  
             try 
             {  
                 // 关闭receiveUdpClient时会产生异常  
                 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);  
                 string message = Encoding.Unicode.GetString(receiveBytes,0,receiveBytes.Length);  

                 // 处理消息  
                 string[] splitstring = message.Split(',');  

                 switch (splitstring[0])  
                 {  
                     case "Accept":  
                         try 
                         {  
                             tcpClient = new TcpClient();  
                             tcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitstring[1]));  
                             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[1] + "," + splitstring[2];  
                         AddItemToListView(userItem);  
                         break;  
                     case "logout":  
                         RemoveItemFromListView(splitstring[1]);  
                         break;  
                     case "talk":  
                         for (int i = 0; i < chatFormList.Count; i++)  
                         {  
                             if (chatFormList[i].Text == splitstring[2])  
                             {  
                                 chatFormList[i].ShowTalkInfo(splitstring[2], splitstring[1], splitstring[3]);  
                             }  
                         }  

                         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 = 0; i < splitstring.Length - 1; 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();  
     } 

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值